Open Closed

IdentitySessionCleanupBackgroundWorker never deletes tenant sessions — AbpSessions grows without bound #10866


User avatar
0
pablo@ccalp.net created

IdentitySessionCleanupBackgroundWorker never deletes tenant sessions — AbpSessions grows without bound

ABP version: 10.4.1 (Commercial) UI / DB / Tiered: Angular / EF Core + SQL Server / separate database per tenant Modules: Identity Pro (Session Management), Audit Logging

Summary

IdentitySessionCleanupBackgroundWorker runs on schedule and reports success, but it only ever deletes host-owned sessions. Every tenant's sessions accumulate for ever, because the worker never changes the tenant context and ABP's multi-tenant query filter therefore restricts the delete to TenantId IS NULL.

This is inconsistent with your own ExpiredAuditLogDeleterService, which handles the identical problem correctly.

Evidence: the SQL the worker actually emits

Captured from EF Core command logging with the worker running normally:

DELETE FROM [a]
FROM [AbpSessions] AS [a]
WHERE [a].[TenantId] IS NULL AND ([a].[LastAccessed] IS NULL OR [a].[LastAccessed] < @Subtract)

TenantId IS NULL is the multi-tenant global filter resolving against a null ICurrentTenant.Id, because background workers run in host context. IdentitySession implements IMultiTenant, so every tenant-owned row is invisible to this statement.

Observed effect in production

| Database | Rows with TenantId IS NULL (deletable) | Rows with TenantId set (never deleted) | |---|---:|---:| | Host | 4 | 437, across 26 tenants | | One tenant DB | 0 | 9,877 |

The host figure shows the worker is functioning perfectly for the rows it can see — only 4 remain and all are within the retention window. In one tenant database, zero rows have a null TenantId, so the statement above can never match anything there. Its oldest session dates from the day that database was created.

The IdentitySessionCleanupBackgroundWorker Hangfire job records Succeeded on every run.

This is not a general ABP design stance — your audit module gets it right

ExpiredAuditLogDeleterService solves exactly this problem, for an entity that is also IMultiTenant:

  • TenantStore.GetListAsync(false) to enumerate tenants
  • CurrentTenant.Change(tenant.Id) for a per-tenant pass
  • CurrentTenant.Change(null) for an explicit host pass
  • retention read through ISettingProvider inside each context, so per-tenant overrides are honoured

and it emits two statements per run:

-- host pass
WHERE [a].[TenantId] IS NULL AND ... AND [a].[ExecutionTime] < @minDate
-- per-tenant pass
WHERE [a].[TenantId] = @ef_filter__CurrentTenantId AND [a].[TenantId] = @CurrentTenant_Id AND [a].[ExecutionTime] < @minDate

IdentitySessionCleanupService.CleanAsync() takes only IdentitySessionManager and the options — no ITenantStore, no ICurrentTenant, no tenant loop.

Your own support answer on question #9458 ("Is BackgroundWorker Tenant Safe?") states that workers run in host context unless the tenant context is changed explicitly, and prescribes the ITenantStore + ICurrentTenant.Change pattern. The audit deleter follows it; the session cleanup does not.

Expected vs actual

Expected: sessions inactive beyond IdentitySessionCleanupOptions.InactiveTimeSpan are removed for the host and every tenant.

Actual: only host sessions are removed. Tenant sessions are retained indefinitely, in shared-database and database-per-tenant deployments alike — the filter behaves the same way in both.

Suggested fix

Give IdentitySessionCleanupService the same treatment as ExpiredAuditLogDeleterService: enumerate tenants via ITenantStore, and run the existing DeleteAllAsync(inactiveTimeSpan) once per tenant inside ICurrentTenant.Change(tenant.Id), plus one host pass.

Question

Is this a known issue, and is a fix planned? If not, is a custom worker that loops tenants and calls IIdentitySessionRepository.DeleteAllAsync(inactiveTimeSpan) per tenant the supported workaround, or would you recommend overriding IdentitySessionCleanupService instead?

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

3 Answer(s)
  • User Avatar
    0
    AI-Bot created
    Support Team ABP Agent
    • In ABP 10.4.1, IdentitySessionCleanupBackgroundWorker / session cleanup runs in host context and (as you observed from the generated SQL) the multi-tenant filter limits the delete to TenantId IS NULL. So tenant-owned rows are not deleted unless the worker explicitly changes the tenant context.
    • I couldn’t verify the current implementation in source (SourceBot repository access wasn’t available in this environment), but based on the official module doc and ABP’s established multi-tenancy background-worker guidance, what you’re seeing is consistent with “runs on host unless you change tenant context”.

    Supported workaround (recommended): implement your own cleanup worker/service that loops tenants and runs the delete inside ICurrentTenant.Change(...). This is the same pattern ABP support recommends for background workers.

    Example implementation (per-tenant + host pass)

    using System;
    using System.Threading.Tasks;
    using Microsoft.Extensions.Options;
    using Volo.Abp.BackgroundWorkers;
    using Volo.Abp.MultiTenancy;
    using Volo.Abp.TenantManagement; // ITenantRepository (or use ITenantStore if you prefer)
    using Volo.Abp.Domain.Repositories;
    
    public class TenantAwareIdentitySessionCleanupWorker : AsyncPeriodicBackgroundWorkerBase
    {
        private readonly ICurrentTenant _currentTenant;
        private readonly ITenantRepository _tenantRepository;
        private readonly IIdentitySessionRepository _identitySessionRepository;
        private readonly IdentitySessionCleanupOptions _options;
    
        public TenantAwareIdentitySessionCleanupWorker(
            AbpAsyncTimer timer,
            ICurrentTenant currentTenant,
            ITenantRepository tenantRepository,
            IIdentitySessionRepository identitySessionRepository,
            IOptions<IdentitySessionCleanupOptions> options)
            : base(timer)
        {
            _currentTenant = currentTenant;
            _tenantRepository = tenantRepository;
            _identitySessionRepository = identitySessionRepository;
            _options = options.Value;
    
            Timer.Period = (int)_options.CleanupPeriod.TotalMilliseconds; // or your desired period
        }
    
        protected override async Task DoWorkAsync(PeriodicBackgroundWorkerContext workerContext)
        {
            // 1) Host cleanup
            using (_currentTenant.Change(null))
            {
                await _identitySessionRepository.DeleteAllAsync(_options.InactiveTimeSpan);
            }
    
            // 2) Tenant cleanup (important for shared-db and database-per-tenant)
            var tenants = await _tenantRepository.GetListAsync(includeDetails: false);
    
            foreach (var tenant in tenants)
            {
                using (_currentTenant.Change(tenant.Id))
                {
                    await _identitySessionRepository.DeleteAllAsync(_options.InactiveTimeSpan);
                }
            }
        }
    }
    

    Should you override IdentitySessionCleanupService instead?

    • If your goal is only to fix cleanup behavior, the safest/least invasive approach is a custom worker (as above) and disable/remove the default one if it’s registered by the module (so you don’t run two cleanups).
    • Overriding/replacing the built-in cleanup service can also work, but it’s more likely to be impacted by internal changes in future module updates. A custom worker that calls the repository is usually the most stable workaround.

    Is a fix planned / known issue?

    • No verified source available here indicating an official bug report or a scheduled fix for 10.4.1 specifically (I couldn’t access ABP commercial source code or a public issue/PR that confirms it). Practically, you should treat it as a gap and implement the tenant-aware worker.

    (links validated)

    • https://abp.io/docs/10.4/modules/identity/session-management
    • https://abp.io/support/questions/2007

    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
    pablo@ccalp.net created

    Thanks — the workaround is correct. I have implemented and verified it, so we are not blocked.

    I would still like a maintainer's answer on the framework question, because the workaround does not address it.

    The workaround works

    A worker that enumerates tenants and calls IIdentitySessionRepository.DeleteAllAsync(inactiveTimeSpan) inside ICurrentTenant.Change(tenant.Id). Verified against a real environment: 13 tenants processed, 0 failures, 5,842 rows deleted on the first pass, and the emitted SQL now carries the per-tenant predicate:

    WHERE [a].[TenantId] = @ef_filter__CurrentTenantId AND ([a].[LastAccessed] IS NULL OR [a].[LastAccessed] < @Subtract)
    

    The question is whether this is a defect you intend to fix

    This is not a general "background workers are host-context" limitation, because the Audit Logging module already solves it. ExpiredAuditLogDeleterService takes ITenantStore, enumerates tenants, and emits two statements per run:

    -- host pass
    WHERE [a].[TenantId] IS NULL AND ... AND [a].[ExecutionTime] < @minDate
    -- per-tenant pass
    WHERE [a].[TenantId] = @ef_filter__CurrentTenantId AND [a].[TenantId] = @CurrentTenant_Id AND [a].[ExecutionTime] < @minDate
    

    IdentitySessionCleanupService.CleanAsync() takes only IdentitySessionManager and its options. No ITenantStore, no ICurrentTenant, no tenant loop.

    So two ABP modules, both cleaning an IMultiTenant entity on a schedule, behave differently. One retains tenant data for ever; the other does not. That reads as an oversight rather than a design decision, and it affects every multi-tenant deployment — shared database or database per tenant, since the filter behaves identically in both.

    Concretely:

    1. Is this accepted as a bug in the Identity Pro session module?
    2. If so, is a fix planned, and for which version?
    3. If it is intended behaviour, could the Session Management documentation state that tenant sessions are never cleaned automatically? Right now it describes IdentitySessionCleanupBackgroundWorker as removing inactive sessions, with no indication that this applies to the host only — which is why we did not catch it until AbpSessions had grown to five figures in a single tenant.

    One correction to the sample, for anyone else who finds this

    The sample resolves its dependencies once and reuses them across the tenant loop. ICurrentTenant.Change only affects services resolved after it, and in a database-per-tenant deployment the DbContext binds its connection string on resolution — so a reused scope binds every iteration to the first tenant's database, and the delete runs against the wrong database.

    This matches the guidance in your own support question 9458: "Always create a new DI scope for each tenant to ensure correct service resolution." Our implementation creates a scope per tenant and resolves everything inside the Change block. It would be worth correcting the sample, since on a shared database the bug is invisible and on a separate-database setup it deletes from the wrong place.

    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,

    IdentitySessionCleanupService runs the delete once in the host context, so the multi-tenant filter limits it to TenantId IS NULL and tenant sessions are never removed. It should loop the tenants the same way ExpiredAuditLogDeleterService does. We'll fix this in 10.7, and your ticket credit has been refunded.

    For now, override the service instead of adding another worker. The built-in worker resolves IdentitySessionCleanupService from DI, so it picks up the override and you keep the distributed lock and the IdentitySessionCleanupOptions settings:

    using System.Threading.Tasks;
    using Microsoft.Extensions.Options;
    using Volo.Abp.Data;
    using Volo.Abp.DependencyInjection;
    using Volo.Abp.Identity;
    using Volo.Abp.Identity.Session;
    using Volo.Abp.MultiTenancy;
    
    namespace MyCompanyName.MyProjectName;
    
    [Dependency(ReplaceServices = true)]
    [ExposeServices(typeof(IdentitySessionCleanupService))]
    public class TenantAwareIdentitySessionCleanupService : IdentitySessionCleanupService
    {
        protected ITenantStore TenantStore { get; }
        protected ICurrentTenant CurrentTenant { get; }
        protected IDataFilter DataFilter { get; }
    
        public TenantAwareIdentitySessionCleanupService(
            IOptionsMonitor&lt;IdentitySessionCleanupOptions&gt; cleanupOptions,
            IdentitySessionManager identitySessionManager,
            ITenantStore tenantStore,
            ICurrentTenant currentTenant,
            IDataFilter dataFilter)
            : base(cleanupOptions, identitySessionManager)
        {
            TenantStore = tenantStore;
            CurrentTenant = currentTenant;
            DataFilter = dataFilter;
        }
    
        public override async Task CleanAsync()
        {
            using (CurrentTenant.Change(null))
            using (DataFilter.Disable&lt;IMultiTenant&gt;())
            {
                await base.CleanAsync();
            }
    
            foreach (var tenant in await TenantStore.GetListAsync())
            {
                using (CurrentTenant.Change(tenant.Id))
                {
                    await base.CleanAsync();
                }
            }
        }
    }
    

    Put it in your .Domain project and remove it when you upgrade to 10.7. The host pass runs with the multi-tenant filter disabled, so the tenant rows sitting in your host database are removed too, and the per-tenant pass covers the separate tenant databases.

    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 21, 2026, 06:18
1
ABP Assistant
🔐 You need to be logged in to use the chatbot. Please log in first.