refactor: streamline agents logic and remove unnecessary caching (#4298)

* refactor: enhance agent loading with structured data

Introduced a new struct, EnabledAgent, to encapsulate agent name and type
information (plugin or built-in). Updated the getEnabledAgentNames function
to return a slice of EnabledAgent instead of a slice of strings, allowing
for more detailed agent management. This change improves the clarity and
maintainability of the code by providing a structured approach to handling
enabled agents and their types.

Signed-off-by: Deluan <deluan@navidrome.org>

* refactor: remove agent caching logic

Eliminated the caching mechanism for agents, including the associated
data structures and methods. This change simplifies the agent loading
process by directly retrieving agents without caching, which is no longer
necessary for the current implementation. The removal of this logic helps
reduce complexity and improve maintainability of the codebase.

Signed-off-by: Deluan <deluan@navidrome.org>

* refactor: replace range with slice.Contains

Signed-off-by: Deluan <deluan@navidrome.org>

* test: simplify agent name extraction in tests

Signed-off-by: Deluan <deluan@navidrome.org>

---------

Signed-off-by: Deluan <deluan@navidrome.org>
This commit is contained in:
Deluan Quintão
2025-07-05 10:11:35 -03:00
committed by GitHub
parent 66eaac2762
commit f1f1fd2007
3 changed files with 151 additions and 141 deletions
+50 -100
View File
@@ -4,7 +4,6 @@ import (
"context" "context"
"slices" "slices"
"strings" "strings"
"sync"
"time" "time"
"github.com/navidrome/navidrome/conf" "github.com/navidrome/navidrome/conf"
@@ -23,54 +22,9 @@ type PluginLoader interface {
LoadMediaAgent(name string) (Interface, bool) LoadMediaAgent(name string) (Interface, bool)
} }
type cachedAgent struct {
agent Interface
expiration time.Time
}
// Encapsulates agent caching logic
// agentCache is a simple TTL cache for agents
// Not exported, only used by Agents
type agentCache struct {
mu sync.Mutex
items map[string]cachedAgent
ttl time.Duration
}
// TTL for cached agents
const agentCacheTTL = 5 * time.Minute
func newAgentCache(ttl time.Duration) *agentCache {
return &agentCache{
items: make(map[string]cachedAgent),
ttl: ttl,
}
}
func (c *agentCache) Get(name string) Interface {
c.mu.Lock()
defer c.mu.Unlock()
cached, ok := c.items[name]
if ok && cached.expiration.After(time.Now()) {
return cached.agent
}
return nil
}
func (c *agentCache) Set(name string, agent Interface) {
c.mu.Lock()
defer c.mu.Unlock()
c.items[name] = cachedAgent{
agent: agent,
expiration: time.Now().Add(c.ttl),
}
}
type Agents struct { type Agents struct {
ds model.DataStore ds model.DataStore
pluginLoader PluginLoader pluginLoader PluginLoader
cache *agentCache
} }
// GetAgents returns the singleton instance of Agents // GetAgents returns the singleton instance of Agents
@@ -85,18 +39,24 @@ func createAgents(ds model.DataStore, pluginLoader PluginLoader) *Agents {
return &Agents{ return &Agents{
ds: ds, ds: ds,
pluginLoader: pluginLoader, pluginLoader: pluginLoader,
cache: newAgentCache(agentCacheTTL),
} }
} }
// getEnabledAgentNames returns the current list of enabled agent names, including: // enabledAgent represents an enabled agent with its type information
type enabledAgent struct {
name string
isPlugin bool
}
// getEnabledAgentNames returns the current list of enabled agents, including:
// 1. Built-in agents and plugins from config (in the specified order) // 1. Built-in agents and plugins from config (in the specified order)
// 2. Always include LocalAgentName // 2. Always include LocalAgentName
// 3. If config is empty, include ONLY LocalAgentName // 3. If config is empty, include ONLY LocalAgentName
func (a *Agents) getEnabledAgentNames() []string { // Each enabledAgent contains the name and whether it's a plugin (true) or built-in (false)
func (a *Agents) getEnabledAgentNames() []enabledAgent {
// If no agents configured, ONLY use the local agent // If no agents configured, ONLY use the local agent
if conf.Server.Agents == "" { if conf.Server.Agents == "" {
return []string{LocalAgentName} return []enabledAgent{{name: LocalAgentName, isPlugin: false}}
} }
// Get all available plugin names // Get all available plugin names
@@ -108,19 +68,13 @@ func (a *Agents) getEnabledAgentNames() []string {
configuredAgents := strings.Split(conf.Server.Agents, ",") configuredAgents := strings.Split(conf.Server.Agents, ",")
// Always add LocalAgentName if not already included // Always add LocalAgentName if not already included
hasLocalAgent := false hasLocalAgent := slices.Contains(configuredAgents, LocalAgentName)
for _, name := range configuredAgents {
if name == LocalAgentName {
hasLocalAgent = true
break
}
}
if !hasLocalAgent { if !hasLocalAgent {
configuredAgents = append(configuredAgents, LocalAgentName) configuredAgents = append(configuredAgents, LocalAgentName)
} }
// Filter to only include valid agents (built-in or plugins) // Filter to only include valid agents (built-in or plugins)
var validNames []string var validAgents []enabledAgent
for _, name := range configuredAgents { for _, name := range configuredAgents {
// Check if it's a built-in agent // Check if it's a built-in agent
isBuiltIn := Map[name] != nil isBuiltIn := Map[name] != nil
@@ -128,39 +82,35 @@ func (a *Agents) getEnabledAgentNames() []string {
// Check if it's a plugin // Check if it's a plugin
isPlugin := slices.Contains(availablePlugins, name) isPlugin := slices.Contains(availablePlugins, name)
if isBuiltIn || isPlugin { if isBuiltIn {
validNames = append(validNames, name) validAgents = append(validAgents, enabledAgent{name: name, isPlugin: false})
} else if isPlugin {
validAgents = append(validAgents, enabledAgent{name: name, isPlugin: true})
} else { } else {
log.Warn("Unknown agent ignored", "name", name) log.Warn("Unknown agent ignored", "name", name)
} }
} }
return validNames return validAgents
} }
func (a *Agents) getAgent(name string) Interface { func (a *Agents) getAgent(ea enabledAgent) Interface {
// Check cache first if ea.isPlugin {
agent := a.cache.Get(name) // Try to load WASM plugin agent (if plugin loader is available)
if agent != nil { if a.pluginLoader != nil {
return agent agent, ok := a.pluginLoader.LoadMediaAgent(ea.name)
} if ok && agent != nil {
return agent
// Try to get built-in agent }
constructor, ok := Map[name]
if ok {
agent := constructor(a.ds)
if agent != nil {
a.cache.Set(name, agent)
return agent
} }
log.Debug("Built-in agent not available. Missing configuration?", "name", name) } else {
} // Try to get built-in agent
constructor, ok := Map[ea.name]
// Try to load WASM plugin agent (if plugin loader is available) if ok {
if a.pluginLoader != nil { agent := constructor(a.ds)
agent, ok := a.pluginLoader.LoadMediaAgent(name) if agent != nil {
if ok && agent != nil { return agent
a.cache.Set(name, agent) }
return agent log.Debug("Built-in agent not available. Missing configuration?", "name", ea.name)
} }
} }
@@ -179,8 +129,8 @@ func (a *Agents) GetArtistMBID(ctx context.Context, id string, name string) (str
return "", nil return "", nil
} }
start := time.Now() start := time.Now()
for _, agentName := range a.getEnabledAgentNames() { for _, enabledAgent := range a.getEnabledAgentNames() {
ag := a.getAgent(agentName) ag := a.getAgent(enabledAgent)
if ag == nil { if ag == nil {
continue continue
} }
@@ -208,8 +158,8 @@ func (a *Agents) GetArtistURL(ctx context.Context, id, name, mbid string) (strin
return "", nil return "", nil
} }
start := time.Now() start := time.Now()
for _, agentName := range a.getEnabledAgentNames() { for _, enabledAgent := range a.getEnabledAgentNames() {
ag := a.getAgent(agentName) ag := a.getAgent(enabledAgent)
if ag == nil { if ag == nil {
continue continue
} }
@@ -237,8 +187,8 @@ func (a *Agents) GetArtistBiography(ctx context.Context, id, name, mbid string)
return "", nil return "", nil
} }
start := time.Now() start := time.Now()
for _, agentName := range a.getEnabledAgentNames() { for _, enabledAgent := range a.getEnabledAgentNames() {
ag := a.getAgent(agentName) ag := a.getAgent(enabledAgent)
if ag == nil { if ag == nil {
continue continue
} }
@@ -271,8 +221,8 @@ func (a *Agents) GetSimilarArtists(ctx context.Context, id, name, mbid string, l
overLimit := int(float64(limit) * conf.Server.DevExternalArtistFetchMultiplier) overLimit := int(float64(limit) * conf.Server.DevExternalArtistFetchMultiplier)
start := time.Now() start := time.Now()
for _, agentName := range a.getEnabledAgentNames() { for _, enabledAgent := range a.getEnabledAgentNames() {
ag := a.getAgent(agentName) ag := a.getAgent(enabledAgent)
if ag == nil { if ag == nil {
continue continue
} }
@@ -304,8 +254,8 @@ func (a *Agents) GetArtistImages(ctx context.Context, id, name, mbid string) ([]
return nil, nil return nil, nil
} }
start := time.Now() start := time.Now()
for _, agentName := range a.getEnabledAgentNames() { for _, enabledAgent := range a.getEnabledAgentNames() {
ag := a.getAgent(agentName) ag := a.getAgent(enabledAgent)
if ag == nil { if ag == nil {
continue continue
} }
@@ -338,8 +288,8 @@ func (a *Agents) GetArtistTopSongs(ctx context.Context, id, artistName, mbid str
overLimit := int(float64(count) * conf.Server.DevExternalArtistFetchMultiplier) overLimit := int(float64(count) * conf.Server.DevExternalArtistFetchMultiplier)
start := time.Now() start := time.Now()
for _, agentName := range a.getEnabledAgentNames() { for _, enabledAgent := range a.getEnabledAgentNames() {
ag := a.getAgent(agentName) ag := a.getAgent(enabledAgent)
if ag == nil { if ag == nil {
continue continue
} }
@@ -364,8 +314,8 @@ func (a *Agents) GetAlbumInfo(ctx context.Context, name, artist, mbid string) (*
return nil, ErrNotFound return nil, ErrNotFound
} }
start := time.Now() start := time.Now()
for _, agentName := range a.getEnabledAgentNames() { for _, enabledAgent := range a.getEnabledAgentNames() {
ag := a.getAgent(agentName) ag := a.getAgent(enabledAgent)
if ag == nil { if ag == nil {
continue continue
} }
@@ -391,8 +341,8 @@ func (a *Agents) GetAlbumImages(ctx context.Context, name, artist, mbid string)
return nil, ErrNotFound return nil, ErrNotFound
} }
start := time.Now() start := time.Now()
for _, agentName := range a.getEnabledAgentNames() { for _, enabledAgent := range a.getEnabledAgentNames() {
ag := a.getAgent(agentName) ag := a.getAgent(enabledAgent)
if ag == nil { if ag == nil {
continue continue
} }
+99 -39
View File
@@ -5,6 +5,7 @@ import (
"github.com/navidrome/navidrome/conf" "github.com/navidrome/navidrome/conf"
"github.com/navidrome/navidrome/model" "github.com/navidrome/navidrome/model"
"github.com/navidrome/navidrome/utils/slice"
. "github.com/onsi/ginkgo/v2" . "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega" . "github.com/onsi/gomega"
) )
@@ -73,8 +74,10 @@ var _ = Describe("Agents with Plugin Loading", func() {
mockLoader.pluginNames = append(mockLoader.pluginNames, "plugin_agent", "another_plugin") mockLoader.pluginNames = append(mockLoader.pluginNames, "plugin_agent", "another_plugin")
// Should only include the local agent // Should only include the local agent
agentNames := agents.getEnabledAgentNames() enabledAgents := agents.getEnabledAgentNames()
Expect(agentNames).To(HaveExactElements(LocalAgentName)) Expect(enabledAgents).To(HaveLen(1))
Expect(enabledAgents[0].name).To(Equal(LocalAgentName))
Expect(enabledAgents[0].isPlugin).To(BeFalse()) // LocalAgent is built-in, not plugin
}) })
It("should NOT include plugin agents when no config is specified", func() { It("should NOT include plugin agents when no config is specified", func() {
@@ -85,9 +88,10 @@ var _ = Describe("Agents with Plugin Loading", func() {
mockLoader.pluginNames = append(mockLoader.pluginNames, "plugin_agent") mockLoader.pluginNames = append(mockLoader.pluginNames, "plugin_agent")
// Should only include the local agent // Should only include the local agent
agentNames := agents.getEnabledAgentNames() enabledAgents := agents.getEnabledAgentNames()
Expect(agentNames).To(HaveExactElements(LocalAgentName)) Expect(enabledAgents).To(HaveLen(1))
Expect(agentNames).NotTo(ContainElement("plugin_agent")) Expect(enabledAgents[0].name).To(Equal(LocalAgentName))
Expect(enabledAgents[0].isPlugin).To(BeFalse()) // LocalAgent is built-in, not plugin
}) })
It("should include plugin agents in the enabled agents list ONLY when explicitly configured", func() { It("should include plugin agents in the enabled agents list ONLY when explicitly configured", func() {
@@ -96,14 +100,24 @@ var _ = Describe("Agents with Plugin Loading", func() {
// With no config, should not include plugin // With no config, should not include plugin
conf.Server.Agents = "" conf.Server.Agents = ""
agentNames := agents.getEnabledAgentNames() enabledAgents := agents.getEnabledAgentNames()
Expect(agentNames).To(HaveExactElements(LocalAgentName)) Expect(enabledAgents).To(HaveLen(1))
Expect(agentNames).NotTo(ContainElement("plugin_agent")) Expect(enabledAgents[0].name).To(Equal(LocalAgentName))
// When explicitly configured, should include plugin // When explicitly configured, should include plugin
conf.Server.Agents = "plugin_agent" conf.Server.Agents = "plugin_agent"
agentNames = agents.getEnabledAgentNames() enabledAgents = agents.getEnabledAgentNames()
var agentNames []string
var pluginAgentFound bool
for _, agent := range enabledAgents {
agentNames = append(agentNames, agent.name)
if agent.name == "plugin_agent" {
pluginAgentFound = true
Expect(agent.isPlugin).To(BeTrue()) // plugin_agent is a plugin
}
}
Expect(agentNames).To(ContainElements(LocalAgentName, "plugin_agent")) Expect(agentNames).To(ContainElements(LocalAgentName, "plugin_agent"))
Expect(pluginAgentFound).To(BeTrue())
}) })
It("should only include configured plugin agents when config is specified", func() { It("should only include configured plugin agents when config is specified", func() {
@@ -114,9 +128,19 @@ var _ = Describe("Agents with Plugin Loading", func() {
conf.Server.Agents = "plugin_one" conf.Server.Agents = "plugin_one"
// Verify only the configured one is included // Verify only the configured one is included
agentNames := agents.getEnabledAgentNames() enabledAgents := agents.getEnabledAgentNames()
Expect(agentNames).To(ContainElement("plugin_one")) var agentNames []string
var pluginOneFound bool
for _, agent := range enabledAgents {
agentNames = append(agentNames, agent.name)
if agent.name == "plugin_one" {
pluginOneFound = true
Expect(agent.isPlugin).To(BeTrue()) // plugin_one is a plugin
}
}
Expect(agentNames).To(ContainElements(LocalAgentName, "plugin_one"))
Expect(agentNames).NotTo(ContainElement("plugin_two")) Expect(agentNames).NotTo(ContainElement("plugin_two"))
Expect(pluginOneFound).To(BeTrue())
}) })
It("should load plugin agents on demand", func() { It("should load plugin agents on demand", func() {
@@ -140,31 +164,6 @@ var _ = Describe("Agents with Plugin Loading", func() {
Expect(mockLoader.pluginCallCount["plugin_agent"]).To(Equal(1)) Expect(mockLoader.pluginCallCount["plugin_agent"]).To(Equal(1))
}) })
It("should cache plugin agents", func() {
ctx := context.Background()
// Configure to use our plugin
conf.Server.Agents = "plugin_agent"
// Add a plugin agent
mockLoader.pluginNames = append(mockLoader.pluginNames, "plugin_agent")
mockLoader.loadedAgents["plugin_agent"] = &MockAgent{
name: "plugin_agent",
mbid: "plugin-mbid",
}
// Call multiple times
_, err := agents.GetArtistMBID(ctx, "123", "Artist")
Expect(err).ToNot(HaveOccurred())
_, err = agents.GetArtistMBID(ctx, "123", "Artist")
Expect(err).ToNot(HaveOccurred())
_, err = agents.GetArtistMBID(ctx, "123", "Artist")
Expect(err).ToNot(HaveOccurred())
// Should only load once
Expect(mockLoader.pluginCallCount["plugin_agent"]).To(Equal(1))
})
It("should try both built-in and plugin agents", func() { It("should try both built-in and plugin agents", func() {
// Create a mock built-in agent // Create a mock built-in agent
Register("built_in", func(ds model.DataStore) Interface { Register("built_in", func(ds model.DataStore) Interface {
@@ -188,8 +187,23 @@ var _ = Describe("Agents with Plugin Loading", func() {
} }
// Verify that both are in the enabled list // Verify that both are in the enabled list
agentNames := agents.getEnabledAgentNames() enabledAgents := agents.getEnabledAgentNames()
Expect(agentNames).To(ContainElements("built_in", "plugin_agent")) var agentNames []string
var builtInFound, pluginFound bool
for _, agent := range enabledAgents {
agentNames = append(agentNames, agent.name)
if agent.name == "built_in" {
builtInFound = true
Expect(agent.isPlugin).To(BeFalse()) // built-in agent
}
if agent.name == "plugin_agent" {
pluginFound = true
Expect(agent.isPlugin).To(BeTrue()) // plugin agent
}
}
Expect(agentNames).To(ContainElements("built_in", "plugin_agent", LocalAgentName))
Expect(builtInFound).To(BeTrue())
Expect(pluginFound).To(BeTrue())
}) })
It("should respect the order specified in configuration", func() { It("should respect the order specified in configuration", func() {
@@ -212,10 +226,56 @@ var _ = Describe("Agents with Plugin Loading", func() {
conf.Server.Agents = "plugin_y,agent_b,plugin_x,agent_a" conf.Server.Agents = "plugin_y,agent_b,plugin_x,agent_a"
// Get the agent names // Get the agent names
agentNames := agents.getEnabledAgentNames() enabledAgents := agents.getEnabledAgentNames()
// Extract just the names to verify the order
agentNames := slice.Map(enabledAgents, func(a enabledAgent) string { return a.name })
// Verify the order matches configuration, with LocalAgentName at the end // Verify the order matches configuration, with LocalAgentName at the end
Expect(agentNames).To(HaveExactElements("plugin_y", "agent_b", "plugin_x", "agent_a", LocalAgentName)) Expect(agentNames).To(HaveExactElements("plugin_y", "agent_b", "plugin_x", "agent_a", LocalAgentName))
}) })
It("should NOT call LoadMediaAgent for built-in agents", func() {
ctx := context.Background()
// Create a mock built-in agent
Register("builtin_agent", func(ds model.DataStore) Interface {
return &MockAgent{
name: "builtin_agent",
mbid: "builtin-mbid",
}
})
defer func() {
delete(Map, "builtin_agent")
}()
// Configure to use only built-in agents
conf.Server.Agents = "builtin_agent"
// Call GetArtistMBID which should only use the built-in agent
mbid, err := agents.GetArtistMBID(ctx, "123", "Artist")
Expect(err).ToNot(HaveOccurred())
Expect(mbid).To(Equal("builtin-mbid"))
// Verify LoadMediaAgent was NEVER called (no plugin loading for built-in agents)
Expect(mockLoader.pluginCallCount).To(BeEmpty())
})
It("should NOT call LoadMediaAgent for invalid agent names", func() {
ctx := context.Background()
// Configure with an invalid agent name (not built-in, not a plugin)
conf.Server.Agents = "invalid_agent"
// This should only result in using the local agent (as the invalid one is ignored)
_, err := agents.GetArtistMBID(ctx, "123", "Artist")
// Should get ErrNotFound since only local agent is available and it returns not found for this operation
Expect(err).To(MatchError(ErrNotFound))
// Verify LoadMediaAgent was NEVER called for the invalid agent
Expect(mockLoader.pluginCallCount).To(BeEmpty())
})
}) })
}) })
+2 -2
View File
@@ -56,8 +56,8 @@ var _ = Describe("Agents", func() {
It("does not register disabled agents", func() { It("does not register disabled agents", func() {
var ags []string var ags []string
for _, name := range ag.getEnabledAgentNames() { for _, enabledAgent := range ag.getEnabledAgentNames() {
agent := ag.getAgent(name) agent := ag.getAgent(enabledAgent)
if agent != nil { if agent != nil {
ags = append(ags, agent.AgentName()) ags = append(ags, agent.AgentName())
} }