Open Closed

AIManagement: workspace configuration cache is never invalidated when the reader's ApplicationName differs from the entity's ApplicationName #10790


User avatar
0
nacho created

AIManagement: workspace configuration cache is never invalidated when the reader's ApplicationName differs from the entity's ApplicationName

Module: Volo.AIManagement (ABP Commercial) Version: observed on 10.4.0 (code paths unchanged in current source) Database provider: any (reproduced with MongoDB)

Summary

WorkspaceConfigurationStore and WorkspaceChangedHandler build the distributed cache key from different sources, so editing a workspace does not invalidate the cached configuration that other applications are actually reading. Those applications keep serving the stale provider/model/API key until the cache entry expires on its own.

  • Read path — WorkspaceConfigurationStore.GetOrNullAsync caches under the current application's name:

    // Volo.AIManagement.Domain/Volo/AIManagement/Workspaces/Configuration/WorkspaceConfigurationStore.cs
    var cacheKey = WorkspaceConfigurationCacheKeyGenerator.Create(
        ApplicationInfoAccessor.ApplicationName!, name);
    
  • Invalidation path — WorkspaceChangedHandler removes the key built from the entity's ApplicationName:

    // Volo.AIManagement.Domain/Volo/AIManagement/Workspaces/WorkspaceChangedHandler.cs
    return _cache.RemoveAsync(
        WorkspaceConfigurationCacheKeyGenerator.Create(
            eventData.Entity.ApplicationName ?? string.Empty,
            eventData.Entity.Name));
    

Whenever Workspace.ApplicationName ≠ the reading process's IApplicationInfoAccessor.ApplicationName, the handler removes a key nobody reads, and the key everybody reads is never removed.

This is easy to hit in a normal multi-application ABP solution because ApplicationWorkspaceManager.CreateAsync stamps the workspace with the ApplicationName of the process that created it:

// Volo.AIManagement.Domain/Volo/AIManagement/Workspaces/ApplicationWorkspaceManager.cs
newWorkspace.ApplicationName = ApplicationInfoAccessor.ApplicationName ?? string.Empty;

A workspace seeded by the DbMigrator carries the migrator's ApplicationName forever, while the Blazor host, the HttpApi host, etc. each cache it under their own ApplicationName.

Steps to reproduce

  1. Solution with separate DbMigrator and web host (standard ABP tiered/multi-app template), distributed cache backed by Redis.
  2. Seed a workspace from the DbMigrator (e.g. via ApplicationWorkspaceManager.CreateAsync("MyWorkspace", "OpenAI", "model-a")).
  3. From the web host, resolve it once (IChatClientResolver.ResolveAsync("MyWorkspace") or IWorkspaceConfigurationStore.GetOrNullAsync("MyWorkspace")) → the configuration is cached under (webHostAppName, "MyWorkspace").
  4. Edit the workspace from the AI Management UI (change ModelName to model-b) → WorkspaceChangedHandler removes (dbMigratorAppName, "MyWorkspace").
  5. Resolve again from the web host.

Expected

The resolver returns a chat client configured with model-b (admin edits take effect on the next call).

Actual

The resolver keeps returning model-a until the cache entry expires (or the host restarts). With a long default cache expiration this looks like "workspace edits are silently ignored".

Suggested fix

Make both sides derive the key from the same source. Options:

  • Key the cache by the entity's ApplicationName on the read path too (requires a lookup, or dropping ApplicationName from the key entirely since Workspace.Name is already unique), or
  • On change, invalidate all application-scoped keys for that workspace name.

Related observation

AIManagementChatClient<T> resolves the inner client once and reuses it for the instance's lifetime (EnsureClientCreatedAsync), so long-lived consumers (Blazor Server circuits, background workers) additionally pin the configuration snapshot from their first call even after the cache is fixed. Consider re-resolving when the underlying configuration changes.

Markdown supported.
Copy, paste, or drag & drop images and files (max 100 MB per file, 100 MB total per post)

6 Answer(s)
  • User Avatar
    0
    AI-Bot created
    Support Team ABP Agent
    • Your analysis is correct: the module’s documented cache key format is application-scoped (WorkspaceConfiguration:{ApplicationName}:{WorkspaceName}), so invalidating with a different ApplicationName will not evict what other processes have cached. In a typical tiered/multi-app solution (DbMigrator, HttpApi.Host, Blazor/Mvc UI, etc.), this can easily produce stale workspace configuration until TTL expires.

    • Immediate workaround (no module source change): make ApplicationName consistent across all processes that must share the same workspace configuration cache entries.

      1. In every host (DbMigrator, HttpApi.Host, Blazor/Mvc host, AuthServer if applicable), set the same application name (whatever your solution uses for app config / application configuration caching scope).
      2. Recreate/seed workspaces after aligning names (or update existing workspace records’ ApplicationName if you control the data), so reads and invalidations converge on the same key space.
    • If you maintain a fork / can patch the module: the safest fix is to ensure read and invalidation paths derive the cache key from the same source (either always the current app name, or always the workspace’s application name, or remove application name from the key if workspace name is globally unique in your design). Without official upstream code available here to reference precisely, I can’t provide a verified patch diff, but the mismatch you pointed out is the root cause.

    • About AIManagementChatClient<T> lifetime pinning: even with correct distributed cache invalidation, any service that creates and holds an inner client instance for a long time will keep using the old config until it recreates the client. The practical mitigation is to register/consume such clients with a lifetime that matches your desired refresh behavior (e.g., avoid singleton/long-lived reuse if you expect admin edits to apply immediately), or implement a change-token/version check to rebuild the inner client when configuration changes.

    (Reference used below is only for the key format; it doesn’t cover the bug itself.)

    Sources:

    • https://abp.io/docs/10.4//modules/ai-management/ (validated)

    This response was generated by AI. If it didn’t solve your problem, feel free to post a new answer — our support team will jump in to help you soon.

    Markdown supported.
    Copy, paste, or drag & drop images and files (max 100 MB per file, 100 MB total per post)
  • User Avatar
    0
    nacho created

    Just posting a comment so ABP team is aware of this issue.

    Markdown supported.
    Copy, paste, or drag & drop images and files (max 100 MB per file, 100 MB total per post)
  • User Avatar
    0
    maliming created
    Support Team Fullstack Developer

    Hi,

    You're right, this is a bug. WorkspaceConfigurationStore builds the cache key from the reading process's ApplicationName, while WorkspaceChangedHandler removes the key built from the entity's ApplicationName. When those differ (a workspace seeded by the DbMigrator but read from another host), the edit removes the entity-scoped key rather than the one the reading host actually uses, so the configuration stays cached until it expires.

    This is a confirmed bug on our side and we'll get it fixed. I'll follow up here with the release once it's available.

    For a stopgap in the meantime, you can key the cache by workspace name only. The store extension points this needs are virtual only from 10.4.1 onward, so you'd need to update to 10.4.1 or later first. Then replace the store:

    using System.Threading.Tasks;
    using Volo.Abp;
    using Volo.Abp.Caching;
    using Volo.Abp.DependencyInjection;
    using Volo.AIManagement.Factory;
    using Volo.AIManagement.Workspaces;
    using Volo.AIManagement.Workspaces.Configuration;
    
    [Dependency(ReplaceServices = true)]
    [ExposeServices(typeof(WorkspaceConfigurationStore), typeof(IWorkspaceConfigurationStore))]
    public class MyWorkspaceConfigurationStore : WorkspaceConfigurationStore
    {
        public MyWorkspaceConfigurationStore(
            IWorkspaceRepository workspaceRepository,
            IDistributedCache<ChatClientCreationConfiguration> singleItemCache,
            IApplicationInfoAccessor applicationInfoAccessor)
            : base(workspaceRepository, singleItemCache, applicationInfoAccessor)
        {
        }
    
        public override async Task<ChatClientCreationConfiguration?> GetOrNullAsync(string name)
        {
    #pragma warning disable CS8603
            var cacheKey = $"WorkspaceConfiguration:{name}";
            return await SingleItemCache.GetOrAddAsync(cacheKey, async () => await GetFromRepositoryAsync(name));
    #pragma warning restore CS8603
        }
    }
    

    And add a handler that invalidates the same name-only key (the built-in handler still runs and removes the old application-scoped key, which is harmless):

    using System.Threading.Tasks;
    using Volo.Abp.Caching;
    using Volo.Abp.DependencyInjection;
    using Volo.Abp.Domain.Entities.Events;
    using Volo.Abp.EventBus;
    using Volo.AIManagement.Factory;
    using Volo.AIManagement.Workspaces;
    
    public class MyWorkspaceCacheInvalidator :
        ILocalEventHandler<EntityChangedEventData<Workspace>>,
        ITransientDependency
    {
        private readonly IDistributedCache<ChatClientCreationConfiguration> _cache;
    
        public MyWorkspaceCacheInvalidator(IDistributedCache<ChatClientCreationConfiguration> cache)
        {
            _cache = cache;
        }
    
        public Task HandleEventAsync(EntityChangedEventData<Workspace> eventData)
        {
            return _cache.RemoveAsync($"WorkspaceConfiguration:{eventData.Entity.Name}", considerUow: true);
        }
    }
    

    Deployment:

    • Put the store replacement in every server process that reads workspace configuration from the database (the ones resolving through the local WorkspaceConfigurationStore), and the invalidator in every server process that updates workspaces through the app service/repository.
    • They must use the same distributed cache backend and the same AbpDistributedCacheOptions.KeyPrefix.
    • A migrator that only seeds and never reads afterward doesn't need it.
    • If you run a tiered setup where some hosts fetch configuration over HTTP, the client process itself doesn't need the replacement, but the AI Management host that serves that endpoint does — otherwise those clients still get stale data.

    A couple of boundaries: it covers edits that keep the same workspace name — a rename also needs the old name's entry removed, so hold off on renames until the fix ships. And the cache key is tenant-scoped by default, so keep both the resolve and the edit in the host context for now.

    On your related note: AIManagementChatClient<T> resolves the inner client once and reuses it, and IChatClient<T> is registered transient, so a Blazor circuit or a long-lived worker that holds the instance keeps the configuration from its first call even after the cache is fixed. Injecting it in a short-lived request scope is fine. For long-lived consumers, create a DI scope per operation (per chat call or per job run) with IServiceScopeFactory.CreateAsyncScope(), resolve IChatClient<T> inside it, and dispose the scope afterward — don't keep the resolved instance in a circuit or singleton field.

    Thanks

    Markdown supported.
    Copy, paste, or drag & drop images and files (max 100 MB per file, 100 MB total per post)
  • User Avatar
    0
    maliming created
    Support Team Fullstack Developer

    Hi,

    We've refunded the credit for this question. Thanks again for the clear write-up — it made confirming the issue straightforward.

    Thanks

    Markdown supported.
    Copy, paste, or drag & drop images and files (max 100 MB per file, 100 MB total per post)
  • User Avatar
    0
    nacho created

    Hi Maliming, have you created a ticket we can follow up?

    Markdown supported.
    Copy, paste, or drag & drop images and files (max 100 MB per file, 100 MB total per post)
  • User Avatar
    0
    maliming created
    Support Team Fullstack Developer

    Hi,

    Yes — it's already fixed and merged, and it'll ship in the next patch release. Thanks again for the detailed report.

    Thanks

    Markdown supported.
    Copy, paste, or drag & drop images and files (max 100 MB per file, 100 MB total per post)
Boost Your Development
ABP Live Training
Packages
See Trainings
Mastering ABP Framework Book
The Official Guide
Mastering
ABP Framework
Learn More
Mastering ABP Framework Book
Made with ❤️ on ABP v10.8.0-preview. Updated on September 28, 2026, 11:44
1
ABP Assistant
🔐 You need to be logged in to use the chatbot. Please log in first.