Skip to content

Replace single global app lock with per-table scoped locks (#984)#1212

Open
thecrockster wants to merge 1 commit intoAzure:mainfrom
thecrockster:thecrockster/perTableAppLock
Open

Replace single global app lock with per-table scoped locks (#984)#1212
thecrockster wants to merge 1 commit intoAzure:mainfrom
thecrockster:thecrockster/perTableAppLock

Conversation

@thecrockster
Copy link

@thecrockster thecrockster commented Feb 11, 2026

Fixes #984

Replaces the single global application lock (_az_func_Trigger) with a two-level locking scheme so that functions monitoring different tables can process changes in parallel:

Global lock (_az_func_Trigger): Used only during startup for DDL operations (schema creation, GlobalState table creation)
Per-table lock (az_func_TT{userTableId}): Used for all runtime operations (change detection, lease management, metrics)
Previously, all trigger functions shared a single sp_getapplock resource (_az_func_Trigger), meaning functions monitoring completely independent tables would block each other. This change scopes runtime locks to the table level using the SQL Server OBJECT_ID, while keeping the global lock only for startup DDL that touches shared objects.

Files changed:

SqlTriggerConstants.cs — Replaced AppLockResource/AppLockStatements with GlobalAppLockResource/GlobalAppLockStatements + GetTableScopedAppLockStatements(int userTableId)
SqlTriggerListener.cs — CreateSchemaAsync and CreateGlobalStateTableAsync use global lock; InsertGlobalStateTableRowAsync and CreateLeasesTableAsync use table-scoped lock
SqlTableChangeMonitor.cs — All 7 runtime SQL command builders use pre-computed table-scoped lock
SqlTriggerMetricsProvider.cs — BuildGetUnprocessedChangesCommand uses table-scoped lock

Code Changes

  • Unit tests are added, if possible
  • Integration tests are added if the change is modifying existing behavior of one or more of the bindings
  • New or changed code follows the C# style guidelines defined in .editorconfig
  • All changes MUST be backwards compatible and changes to the shared az_func.GlobalState table must be compatible with all prior versions of the extension
  • Use the ILogger instance to log relevant information, especially information useful for debugging or troubleshooting
  • Use async and await for all long-running operations
  • Ensure proper usage and propagation of CancellationToken
  • T-SQL is safe from SQL Injection attacks through the use of SqlParameters and proper escaping/sanitization of input

Dependencies

Documentation

  • Add samples if the change is modifying or adding functionality
  • Update relevant documentation in the docs

@thecrockster
Copy link
Author

thecrockster commented Feb 11, 2026 via email

@thecrockster thecrockster force-pushed the thecrockster/perTableAppLock branch from 051a4a8 to 06bfde1 Compare February 14, 2026 18:40
- Split AppLockResource/AppLockStatements into two levels:
  - GlobalAppLockStatements: used only for startup DDL (schema + GlobalState table creation)
  - GetTableScopedAppLockStatements(userTableId): used for all runtime operations
- Table-scoped lock resource format: _az_func_TT_{userTableId}
- Functions monitoring different tables can now process changes in parallel
- Added unit tests for SqlTriggerConstants lock generation
- Updated TriggerBinding.md documentation
@thecrockster thecrockster force-pushed the thecrockster/perTableAppLock branch from 06bfde1 to c6cd4a7 Compare February 19, 2026 18:30
@Charles-Gagnon
Copy link
Contributor

Thank you for submitting this @thecrockster !

The changes overall look reasonable, so there's no immediate concerns there.

But given the scope of this change we're going to hold off on fully reviewing & merging this until we have a bigger release planned. The app locking is a critical part of the app functionality, and so we want to make sure we're releasing in a way that will have the lowest risk for our users - likely as part of a major or minor release (instead of the patch releases we're currently doing).

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This pull request addresses issue #984 by replacing the single global application lock with a two-level locking scheme that enables parallel processing of changes for functions monitoring different tables while maintaining deadlock prevention for functions monitoring the same table.

Changes:

  • Introduced per-table scoped application locks using the table's SQL Server OBJECT_ID, allowing functions monitoring different tables to process changes concurrently
  • Retained the global lock only for DDL operations during startup (schema creation, GlobalState table creation)
  • Pre-computed table-scoped lock statements in SqlTableChangeMonitor for runtime efficiency

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated no comments.

Show a summary per file
File Description
src/TriggerBinding/SqlTriggerConstants.cs Added GlobalAppLockResource, TableAppLockResourcePrefix constants and GetTableScopedAppLockStatements method to generate table-scoped lock SQL
src/TriggerBinding/SqlTriggerListener.cs Updated DDL operations to use global lock, runtime operations to use table-scoped locks; added userTableId parameter to CreateLeasesTableAsync
src/TriggerBinding/SqlTableChangeMonitor.cs Pre-computes table-scoped lock statements in constructor; uses cached statements in all 7 runtime SQL command builders
src/TriggerBinding/SqlTriggerMetricsProvider.cs Updated BuildGetUnprocessedChangesCommand to use table-scoped lock
test/Unit/TriggerBinding/SqlTriggerConstantsTests.cs Added comprehensive unit tests covering lock statement generation, resource name uniqueness, and separation of global vs table-scoped locks
docs/TriggerBinding.md Updated documentation to explain the two-level locking scheme and provided guidance for custom synchronization

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Don't use single global app lock resource

3 participants