Open Closed

ABP Upgrade from 8 to 10 - long transaction #10574


User avatar
0
DominaTang created

After upgrade and deploy the application (EF core, Micro serivce) , the app data access layer throw timeout exception, here is one of example:

(@p6 uniqueidentifier,@p7 nvarchar(40),@p4 nvarchar(40),@p5 datetime2(7))UPDATE [Agents] SET [ConcurrencyStamp] = @p4, [LastModificationTime] = @p5 OUTPUT 1 WHERE [Id] = @p6 AND [ConcurrencyStamp] = @p7

The agents table has 900+ records only. Azure SQL server's compatible level is SQL Server 2019.

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

27 Answer(s)
  • User Avatar
    0
    AI-Bot created
    Support Team ABP Agent
    • The SQL you posted is an optimistic concurrency update generated by EF Core (ABP uses ConcurrencyStamp on aggregate roots like IdentityUser, etc.). A timeout on a single-row UPDATE ... WHERE Id AND ConcurrencyStamp with ~900 rows is almost never caused by table size; it’s typically caused by blocking/locking (another transaction holds a lock on that row/table) or resource pressure (DTU/vCore throttling) on Azure SQL after the upgrade.

    • With ABP 10 you are very likely also on a newer .NET/EF Core version than ABP 8 (EF Core 8 vs EF Core 6/7 depending on your starting point). That doesn’t “break” this statement, but it can change overall behavior (more queries, different batching, different execution plans), which can expose existing locking issues under load.

    Do these ABP-specific fixes/mitigations (safe and commonly used):

    1. Increase EF Core command timeout for ABP DbContexts Configure it in your *.EntityFrameworkCore module where you call Configure<AbpDbContextOptions>:
    using Volo.Abp.EntityFrameworkCore;
    
       public override void ConfigureServices(ServiceConfigurationContext context)
       {
           Configure<AbpDbContextOptions>(options =>
           {
               options.Configure(cfg =>
               {
                   // Increase as needed (seconds)
                   cfg.DbContextOptionsBuilder.CommandTimeout(180);
               });
           });
       }
    

    This doesn’t solve blocking, but prevents “false failures” during short spikes/throttling.

    1. Identify and remove blocking transactions (most common root cause) In Azure SQL, check for blocking at the time of the issue (Query Store / “Blocked process” views / running requests). The symptom you see (timeout on a simple update with concurrency check) is consistent with:

      • a long-running UOW/transaction holding locks,
      • a background worker / migration / seed running concurrently,
      • or multiple services updating the same “Agent” row(s) frequently.

      ABP microservices commonly use distributed events/outbox patterns; if you have a consumer that updates the same entity repeatedly inside a long unit-of-work, it can block other writers.

    2. Ensure your Azure SQL tier/performance didn’t effectively downgrade After upgrade, overall DB activity usually increases (more features/modules, more permission/setting/cache reads/writes). On low tiers, Azure SQL may throttle and cause timeouts even on trivial statements.

    3. Make sure SQL Server compatibility is supported ABP 8 migration notes remind that starting with EF Core 8.0, generated SQL requires newer SQL Server versions (SQL Server 14+). Azure SQL with compat level 2019 is fine, but this confirms the “version support” angle is not your problem here—again pointing back to blocking/throttling.

    If you apply (1) and still see timeouts, treat it as a blocking/throttling problem rather than an ABP/EF bug: the specific statement is a single-row concurrency update, so it should complete instantly unless it’s waiting.

    Links (verified):

    • https://abp.io/docs/latest/release-info/migration-guides/abp-8-0
    • https://learn.microsoft.com/en-us/ef/core/what-is-new/ef-core-8.0/breaking-changes#high-impact-changes

    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
    DominaTang created

    Is it possible because of ConcurrencyStamp? For above simple statement, it should not take more than 30 seconds;

    Markdown supported.
    Copy, paste, or drag & drop images and files (max 100 MB per file, 100 MB total per post)
  • User Avatar
    0
    AI-Bot created
    Support Team ABP Agent
    • No—a timeout is not caused by ConcurrencyStamp itself. ConcurrencyStamp is just an optimistic concurrency token (a string) that EF Core includes in the WHERE clause to ensure “update only if nobody else changed it”. If the stamp doesn’t match, you get an AbpDbConcurrencyException / DbUpdateConcurrencyException (0 rows affected), not a 30s timeout.

    • A 30-second timeout on this statement almost always means SQL Server is waiting on locks (blocking) or being throttled. Even a single-row update can sit and wait if:

      • another transaction holds a lock on that row (or a broader lock due to escalation),
      • a long-running transaction/UOW is open in your service,
      • you have multiple microservice instances/background workers updating the same row(s),
      • Azure SQL is under resource pressure.

    What to do (actionable, ABP-relevant):

    1. Confirm it’s blocking (most likely) When the timeout happens, you should see the session in a WAIT state (e.g., LCK_M_...) on Azure SQL. If it’s blocking, the fix is to find and eliminate the long transaction / hot row contention (not to change concurrency stamp behavior).

    2. Increase the EF Core command timeout (mitigation) Configure ABP’s DbContext options to raise command timeout so transient blocking/throttling doesn’t fail at 30 seconds:

    using Volo.Abp.EntityFrameworkCore;
    
       Configure<AbpDbContextOptions>(options =>
       {
           options.Configure(cfg =>
           {
               cfg.DbContextOptionsBuilder.CommandTimeout(180);
           });
       });
    
    1. If the same “Agent” row is frequently updated, prevent multi-instance contention In microservices with multiple replicas, if many workers update the same entity (same Id) you can get lock queues and timeouts. In such cases, you typically need to redesign to reduce hot-row updates (e.g., avoid repeatedly updating audit/concurrency fields for “heartbeat”-like writes), or use a distributed locking / coordination approach around that critical section.

    (Concurrency token docs, validated)

    • https://abp.io/docs/latest/framework/infrastructure/concurrency-check
    • https://abp.io/community/articles/jlkc3w8f

    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
    maliming created
    Support Team Fullstack Developer

    Hi,

    based on what you've described, the timeout on a simple single-row UPDATE ... WHERE Id AND ConcurrencyStamp with only ~900 records is very unlikely to be a code issue — that statement should complete in milliseconds under normal conditions.

    The issue might not be related to your code or ABP at all. Could you try reproducing it against a local SQL Server instance with the same data to help narrow down the root cause?

    Thanks

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

    I notice in an AppService Get method, it has code call repository update method too, I remember ABP automatically add unit of work to App Service public methods, does it apply to get method too?

    Markdown supported.
    Copy, paste, or drag & drop images and files (max 100 MB per file, 100 MB total per post)
  • User Avatar
    0
    AI-Bot created
    Support Team ABP Agent
    • Yes. ABP automatically applies Unit of Work (UOW) to all public application service methods by convention, regardless of whether the method is “Get”/read-like or “Create/Update/Delete”. So your Get... method is inside a UOW scope too.

    • However, being inside a UOW doesn’t always mean a DB transaction is started. With the default UnitOfWorkTransactionBehavior.Auto behavior for HTTP requests, GET HTTP requests don’t start a transactional UOW by default. That said, you can still write to the database inside a GET method, and it can still cause locks/contention and timeouts (especially if that GET endpoint is hit frequently).

    What you should do in your case (since your GET calls UpdateAsync)

    1. Don’t update data inside a GET. Move that update logic to a separate command endpoint (POST/PUT), or a background job/event handler.
    2. If you must keep it temporarily, explicitly control the UOW/transaction behavior:
      • Disable UOW for that GET if it should be pure read:
    [UnitOfWork(IsDisabled = true)]
         public virtual async Task<AgentDto> GetAsync(Guid id)
         {
             // read-only code only
         }
    
    • Or if it truly must update, make it a non-GET endpoint and/or force transactional UOW explicitly (so the behavior is clear and consistent).

    This pattern (UOW on application service methods + GET requests not starting transactions by default) is documented in ABP’s Unit of Work docs.

    Sources:

    • https://abp.io/docs/latest/framework/architecture/domain-driven-design/unit-of-work
    • https://abp.io/community/articles/0r248xsr

    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
    maliming created
    Support Team Fullstack Developer

    Hi,

    Yes, ABP automatically wraps all public AppService methods in a Unit of Work, including Get methods. However, for methods whose names start with Get, the UoW is non-transactional by default (no explicit DB
    transaction is created).

    That said, SaveChangesAsync() is still called at the end of the method. So if your Get method also calls a repository update, the changes will be persisted — and that would generate the UPDATE ... SET ConcurrencyStamp ... WHERE Id AND ConcurrencyStamp SQL you're seeing.

    Could you share more details about what the Get method is doing? Specifically, why is it calling a repository update method? That would help us understand the root cause.

    Also, could you enable EF Core debug logging to see the actual SQL being executed? This will help us understand the full picture. You can refer to this guide:
    https://abp.io/support/questions/8622/How-to-enable-Debug-logs-for-troubleshoot-problems

    Thanks

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

    AI gives this hint, we will check if this is the reason of it: In EF Core 8 and earlier, when you called SaveChangesAsync(), it would generate and execute SQL statements one at a time:

    In EF Core 9, it introduced command batching — multiple statements are combined into a single SET NOCOUNT ON; ... batch sent to SQL Server in one round trip:

    In EF Core 8, even if a transaction leaked, the individual autoSave commits had already released locks progressively. With EF Core 9 batching, all the locks are held simultaneously until the entire batch completes — so a leaked transaction holds locks on Clients + Documents + DocumentVersions + Files all at once, for the entire duration.

    Markdown supported.
    Copy, paste, or drag & drop images and files (max 100 MB per file, 100 MB total per post)
  • User Avatar
    0
    AI-Bot created
    Support Team ABP Agent
    • ConcurrencyStamp is not the reason for the timeout. If the ConcurrencyStamp mismatches, EF Core/ABP would typically result in a concurrency exception (0 rows affected), not a “wait 30s then timeout”. A timeout on that statement means SQL Server is waiting (usually on locks) or the DB is throttling.

    • Yes, ABP applies Unit of Work to all public Application Service methods, including Get... methods.
      Additionally, by default ABP’s UOW transaction behavior is Auto, and HTTP GET requests don’t start a transactional UOW (no explicit DB transaction is created). But the method is still inside a UOW pipeline, and if you do writes (like UpdateAsync) inside a GET, it will still persist changes (via SaveChanges at the end of the UOW / repository autoSave, depending on your code).

    • About the “EF Core 9 command batching holds locks longer” hint: I can’t verify that claim from ABP/EF sources in the available context here, so I won’t confirm it. What is verifiable and relevant in ABP is: doing UpdateAsync inside a frequently-called GET method can create unexpected write load and lock contention, which does match your symptom.

    What to do (definitive ABP guidance):

    1. Remove any update call from GET methods. Split into:

      • Get... (read-only)
      • Update... (POST/PUT) for writes
        This alone often eliminates these “random timeouts”.
    2. If you must keep the GET read-only and want to enforce it, disable UOW or at least make it explicitly non-transactional and don’t call update:

    using Volo.Abp.Uow;
    
    [UnitOfWork(IsDisabled = true)]
    public virtual async Task<AgentDto> GetAsync(Guid id)
    {
        // read-only
    }
    
    1. If the method truly must write (not recommended for GET), make it a non-GET endpoint and (optionally) force a transactional UOW explicitly so behavior is consistent:
    [UnitOfWork(IsTransactional = true)]
    public virtual async Task<AgentDto> TouchAndGetAsync(Guid id)
    {
        // do update + read
    }
    

    (verified references)

    • https://abp.io/docs/latest/framework/architecture/domain-driven-design/unit-of-work
    • https://abp.io/community/articles/0r248xsr

    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
    maliming created
    Support Team Fullstack Developer

    Hi,

    EF Core has actually had command batching since version 1.0, so the batching behavior itself hasn't changed between EF Core 8 and EF Core 9/10. The timeout is more likely related to lock contention — especially if your Get method is calling a repository update, which would trigger SaveChangesAsync and generate that UPDATE ... SET ConcurrencyStamp SQL on every request.

    Could you enable EF Core debug logging, reproduce the issue, and share the logs with us? The debug logs will show the actual SQL queries and updates being executed, which will help us pinpoint what's happening. Please clear/delete old log files before reproducing to keep the log size manageable.

    You can follow this guide to enable debug logging: https://abp.io/support/questions/8622/How-to-enable-Debug-logs-for-troubleshoot-problems

    Thanks

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

    I feel like it is our code base issue, some code hold long transactions

    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

    Alright, if you run into any other issues, just let me know.

    Thanks.

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

    I think it might be our code base issue, though it kind of working in ABP 8, but now with ABP 10 we see quite more chances of Zombie transactions session from SQL server, I do find some code using a unit of work, and call await uow.SaveChangesAsync(); without call await uow.CompleteAsync(); Is it the cause? With ABP 10 has unit of work enhancement?

    Markdown supported.
    Copy, paste, or drag & drop images and files (max 100 MB per file, 100 MB total per post)
  • User Avatar
    0
    AI-Bot created
    Support Team ABP Agent
    • Yes — not calling CompleteAsync() after SaveChangesAsync() in a manually started Unit of Work is a real bug and can absolutely contribute to “zombie”/long-running transactions and blocking. In ABP, SaveChangesAsync() flushes changes to the database within the current UOW, but the transaction is only permanently committed when CompleteAsync() is successfully executed. If CompleteAsync() is never called, the UOW is not considered completed; if it’s transactional, it won’t commit (and may hold resources until disposed), and ABP also won’t run the “end-of-UOW” pipeline (events, cleanup, etc.).

    • This is consistent with your symptom: a simple concurrency update (... WHERE Id AND ConcurrencyStamp) timing out because it’s blocked by another session/UOW that is kept open too long.

    What you should change in your code (manual UOW usage)

    1. Always structure manual UOW like this:
    using Volo.Abp.Uow;
    
    using (var uow = _unitOfWorkManager.Begin(isTransactional: true))
    {
        // ... do repository/DbContext operations ...
    
        await uow.SaveChangesAsync();   // optional (flush mid-way)
        await uow.CompleteAsync();      // mandatory (commit + finalize UOW)
    }
    
    1. If you don’t actually need an explicit transaction (common for imports/logging), start a non-transactional UOW to reduce locking risk:
    using (var uow = _unitOfWorkManager.Begin(isTransactional: false))
    {
        // operations
        await uow.CompleteAsync();
    }
    

    Is this “more visible” in ABP 10 because of UOW enhancements?

    • ABP’s guidance/semantics around UOW are the same: CompleteAsync() is the method that finalizes a UOW (commit if transactional + executes UOW end tasks). If your code previously “seemed to work” in ABP 8, it was still incorrect; ABP 10 + newer EF Core / higher load patterns can make the consequences (blocking/timeouts) show up more often.

    Verified reference

    • https://abp.io/docs/latest/framework/architecture/domain-driven-design/unit-of-work

    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
    DominaTang created

    what about code like this style: using unit of work

    some db operations try { some other db operations } catch { swallow the exception } other db operations unit of work complete

    Markdown supported.
    Copy, paste, or drag & drop images and files (max 100 MB per file, 100 MB total per post)
  • User Avatar
    0
    AI-Bot created
    Support Team ABP Agent
    • This style is dangerous in a transactional Unit of Work, and it can definitely be a root cause of the blocking/timeouts you’re seeing.

    What happens in ABP UOW when you “swallow” exceptions

    • If you do DB operations inside a try { ... } catch { } and swallow the exception, then execution continues and you later call await uow.CompleteAsync().
    • ABP will then try to commit the transaction (and run end-of-UOW actions) as if everything was successful.
    • Meanwhile, the caught exception may indicate:
      • the DbContext is in a problematic state for the remainder of the UOW,
      • or you partially failed an operation and continued doing more writes,
      • or you created contention by doing extra work inside the same long transaction.

    So this pattern can lead to longer transactions and more lock holding, which matches “zombie” sessions / timeouts.

    Correct patterns

    A) If an exception means the business operation should fail → do NOT swallow

    using (var uow = _unitOfWorkManager.Begin(isTransactional: true))
    {
        // db ops...
    
        try
        {
            // db ops...
        }
        catch
        {
            // optionally log
            throw; // important: abort the operation
        }
    
        // db ops...
        await uow.CompleteAsync();
    }
    

    B) If you intentionally want “best effort” and still commit other changes Then don’t keep everything in one big transactional UOW. Either:

    • use a non-transactional UOW for the best-effort part, or
    • use a separate requiresNew: true UOW for the part that can fail independently.

    Example (separate UOW for the optional part):

    using (var uow = _unitOfWorkManager.Begin(isTransactional: true))
    {
        // main db ops...
    
        try
        {
            using (var innerUow = _unitOfWorkManager.Begin(requiresNew: true, isTransactional: true))
            {
                // optional db ops...
                await innerUow.CompleteAsync();
            }
        }
        catch
        {
            // swallow only if it is truly optional (but log it)
        }
    
        // continue main db ops...
        await uow.CompleteAsync();
    }
    

    Key rule (ABP-specific)

    • In a transactional UOW, SaveChangesAsync() / autoSave does not finalize changes permanently until CompleteAsync() succeeds. So swallowing exceptions and still calling CompleteAsync() can cause “commit despite internal failures” patterns and longer locking windows.
    • If you manually begin a UOW, CompleteAsync() is mandatory to properly finish it.

    Links (validated):

    • https://abp.io/docs/latest/framework/architecture/domain-driven-design/unit-of-work
    • https://abp.io/community/articles/0r248xsr

    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
    DominaTang created

    what about code like this, would it be a is transaction true? would the Unit of work complete gracefully or roll back properly when there is exception? [UnitOfWork] public async Task Create30DaysReminderAsync(Guid fileId) { try { some Db operations; await CurrentUnitOfWork.SaveChangesAsync(); } catch { throw; } }

    Markdown supported.
    Copy, paste, or drag & drop images and files (max 100 MB per file, 100 MB total per post)
  • User Avatar
    0
    AI-Bot created
    Support Team ABP Agent
    • With [UnitOfWork] on the method, ABP will create/participate in a UOW scope for that method (assuming interception works: the method should be virtual if needed per ABP rules). Whether it is transactional depends on ABP’s transaction behavior (default is Auto for HTTP requests: GET is non-transactional, others transactional). If you want it guaranteed transactional, specify it:
    [UnitOfWork(IsTransactional = true)]
    public virtual async Task Create30DaysReminderAsync(Guid fileId)
    {
        // ...
    }
    
    • In your exact code:
    [UnitOfWork]
    public async Task Create30DaysReminderAsync(Guid fileId)
    {
        try
        {
            // some Db operations;
            await CurrentUnitOfWork.SaveChangesAsync();
        }
        catch
        {
            throw;
        }
    }
    

    Rollback / commit behavior:

    • If an exception is thrown out of the method (and you throw; does exactly that), then CompleteAsync() will not be successfully executed, so:

      • If the UOW is transactional: the transaction will roll back.
      • If the UOW is non-transactional: changes already flushed by SaveChangesAsync() are already persisted and cannot be rolled back (by definition of non-transactional UOW).
    • Your try/catch { throw; } is effectively redundant (same as not catching), but it does not harm rollback behavior.

    • Important nuance from ABP docs: SaveChangesAsync() (and repository autoSave) flushes changes, but they are not permanently committed until CompleteAsync() is called successfully in a transactional UOW. If CompleteAsync() is not called (or the method exits by exception), ABP rolls back the transaction.

    Validated source:

    • https://abp.io/docs/latest/framework/architecture/domain-driven-design/unit-of-work

    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
    DominaTang created

    when a method has [UnitOfWork], should it call unit of work complete automatically by ABP?

    Markdown supported.
    Copy, paste, or drag & drop images and files (max 100 MB per file, 100 MB total per post)
  • User Avatar
    0
    AI-Bot created
    Support Team ABP Agent
    • Yes. When you use the [UnitOfWork] attribute, ABP handles the Unit of Work lifecycle for you via interceptors: it begins the UOW, runs your method, and if the method finishes successfully it calls CompleteAsync() automatically. If the method throws an exception, ABP does not complete the UOW; if it was transactional, it will be rolled back when the UOW is disposed.

    • You should not manually call CompleteAsync() in a method that is already managed by [UnitOfWork] (or by the application service UOW convention). You only call CompleteAsync() when you start a UOW manually with IUnitOfWorkManager.Begin(...).

    • Important: for [UnitOfWork] to work, the method generally must be interceptable (commonly: public virtual in a DI-resolved class).

    Sources (validated):

    • https://abp.io/docs/latest/framework/architecture/domain-driven-design/unit-of-work
    • https://abp.io/community/articles/0r248xsr

    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
    DominaTang created

    What about code style like below, when there is exception, could unit of work roll back or terminate gracefully? [UnitOfWork] public virtual async Task<SaveResult> UpdateDocumentsAsync(Guid id, Guid[] checklistDocumentIds) { try { some db operations

    } catch (Exception ex) { _logger.LogException(ex, LogLevel.Error); } }

    Markdown supported.
    Copy, paste, or drag & drop images and files (max 100 MB per file, 100 MB total per post)
  • User Avatar
    0
    AI-Bot created
    Support Team ABP Agent
    • With [UnitOfWork], ABP will automatically call CompleteAsync() if your method returns successfully (i.e., no exception escapes the method). If an exception escapes, ABP will not complete it; if it was transactional, it will roll back when disposed.

    • In your code, the exception is swallowed, so from ABP’s perspective the method completed successfully. That means:

      • ABP will proceed to complete/commit the UOW (if it is transactional).
      • So it will not roll back due to that exception (because you didn’t let the exception propagate).
      • Any changes already made in the UOW may still be persisted (depending on what actually succeeded before the exception).

    So: this does terminate “gracefully” from ABP’s viewpoint, but it does not roll back. It commits whatever is in the UOW unless you explicitly roll it back or rethrow.

    Correct options

    Option A (recommended): log + rethrow

    [UnitOfWork]
    public virtual async Task<SaveResult> UpdateDocumentsAsync(Guid id, Guid[] checklistDocumentIds)
    {
        try
        {
            // db operations
            return SaveResult.Success();
        }
        catch (Exception ex)
        {
            _logger.LogException(ex, LogLevel.Error);
            throw; // ensure ABP doesn't complete/commit the UOW
        }
    }
    

    Option B: if you intentionally want to swallow, explicitly rollback (only meaningful if the UOW is transactional)

    using Volo.Abp.Uow;
    
    [UnitOfWork]
    public virtual async Task<SaveResult> UpdateDocumentsAsync(Guid id, Guid[] checklistDocumentIds)
    {
        try
        {
            // db operations
            return SaveResult.Success();
        }
        catch (Exception ex)
        {
            _logger.LogException(ex, LogLevel.Error);
            await CurrentUnitOfWork.RollbackAsync(); // prevents commit
            return SaveResult.Failed(ex.Message);
        }
    }
    

    Source (verified)

    • https://abp.io/docs/latest/framework/architecture/domain-driven-design/unit-of-work

    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
    DominaTang created

    if the code doesn't roll back, would it creates zombie transaction?

    Markdown supported.
    Copy, paste, or drag & drop images and files (max 100 MB per file, 100 MB total per post)
  • User Avatar
    0
    AI-Bot created
    Support Team ABP Agent
    • Not rolling back does not, by itself, create a “zombie transaction”. A “zombie” session/transaction (long-running sleeping session holding locks, blocking others) is typically caused by a transaction that remains open for a long time (e.g., long-running UOW, dead code path that never completes, waiting on external I/O inside a transactional UOW, etc.).

    • In the pattern you showed earlier (catch + swallow inside a [UnitOfWork] method), what usually happens is:

      • The exception is swallowed, so ABP thinks the method succeeded and will call CompleteAsync() automatically.
      • If the UOW is transactional, CompleteAsync() will commit. That means the transaction ends, so it should not become a zombie because of the swallow.
    • The swallow pattern can still indirectly contribute to long blocking because it may:

      • keep the method doing more work after a failure (making the UOW longer),
      • or leave the DbContext in a bad state causing retries/slow operations,
      • or cause repeated failures under load (more contention), but the key is still how long the transaction stays open, not “no rollback”.

    What does commonly create zombie/long transactions in ABP

    1. Manual UOW started but never completed

      • Calling SaveChangesAsync() but forgetting CompleteAsync() is a classic cause of “open transaction until dispose/timeout”.
      • ABP docs explicitly state that changes are only permanently committed after CompleteAsync() in a transactional UOW, and if CompleteAsync() is not called the transaction is rolled back when it ends—however, if the scope is kept alive too long, it can hold locks too long.
    2. Long-running work inside a transactional UOW

      • Doing network calls, file I/O, heavy loops, waiting for other services, etc. inside a transactional UOW keeps locks longer.
    3. Calling UpdateAsync from a frequently hit GET

      • Even if it’s non-transactional by default, it creates constant writes and can amplify contention.

    What to enforce in code

    • For [UnitOfWork] methods: do not swallow exceptions unless the failure is truly optional; if you swallow, consider explicitly calling await CurrentUnitOfWork.RollbackAsync() to guarantee no commit when transactional.
    • For manual UOW: always:
    using (var uow = _unitOfWorkManager.Begin(isTransactional: true))
    {
        // db work
        await uow.CompleteAsync();
    }
    

    Reference (verified):

    • https://abp.io/docs/latest/framework/architecture/domain-driven-design/unit-of-work

    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
    maliming created
    Support Team Fullstack Developer

    Hi,

    Let me address your questions one by one:

    1. Calling uow.SaveChangesAsync() without uow.CompleteAsync()

    This is a bug. SaveChangesAsync() only flushes pending changes to the database within the current transaction — it does NOT commit. Only CompleteAsync() commits the transaction. If you skip CompleteAsync(), the using block will dispose the UoW and the transaction will be rolled back. So your changes are lost, but it's not a zombie transaction.

    2. [UnitOfWork] attribute behavior

    Yes, ABP automatically calls CompleteAsync() for you when the method returns successfully. You should NOT manually call CompleteAsync() in a [UnitOfWork] method. You only call it when you manually start a UoW via _unitOfWorkManager.Begin(...).

    3. [UnitOfWork] + try/catch/throw

    [UnitOfWork]
    public virtual async Task Create30DaysReminderAsync(Guid fileId)
    {
        try { /* db ops */ }
        catch { throw; }
    }
    

    When the exception propagates out of the method, ABP won't call CompleteAsync(), so the transaction rolls back correctly. The try/catch/throw is redundant here (same as not catching), but it does not break rollback behavior.

    4. [UnitOfWork] + swallow exception (no rethrow)

    [UnitOfWork]
    public virtual async Task<SaveResult> UpdateDocumentsAsync(...)
    {
        try { /* db ops */ }
        catch (Exception ex) { _logger.LogException(ex, LogLevel.Error); } // swallowed
    }
    

    Because the exception is swallowed, ABP thinks the method succeeded and calls CompleteAsync(), which commits the transaction. This means partial/inconsistent data can be committed. If you really want to swallow, you should explicitly call await CurrentUnitOfWork.RollbackAsync() inside the catch block, or rethrow the exception.

    5. Will it create a zombie transaction?

    No. As long as the UoW is wrapped in a using block (or managed by [UnitOfWork]), it will always be disposed properly — the underlying DbTransaction is released either via commit or rollback on disposal. A true zombie/orphan transaction would only happen if you bypass ABP's UoW (e.g., calling DbContext.Database.BeginTransaction() directly without disposing) or if the process crashes mid-transaction. What you're seeing is more likely long-held locks from long-running transactions, not zombie transactions.

    I wrote an article that explains all of this in detail, please take a look:

    https://abp.io/community/articles/understanding-transactions-in-abp-unit-of-work-0r248xsr

    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.