Reimaginate.ProcessingLockService 3.1.0

dotnet add package Reimaginate.ProcessingLockService --version 3.1.0
                    
NuGet\Install-Package Reimaginate.ProcessingLockService -Version 3.1.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Reimaginate.ProcessingLockService" Version="3.1.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Reimaginate.ProcessingLockService" Version="3.1.0" />
                    
Directory.Packages.props
<PackageReference Include="Reimaginate.ProcessingLockService" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Reimaginate.ProcessingLockService --version 3.1.0
                    
#r "nuget: Reimaginate.ProcessingLockService, 3.1.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Reimaginate.ProcessingLockService@3.1.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Reimaginate.ProcessingLockService&version=3.1.0
                    
Install as a Cake Addin
#tool nuget:?package=Reimaginate.ProcessingLockService&version=3.1.0
                    
Install as a Cake Tool

Reimaginate.ProcessingLockService

Runtime processing lock service implementation, dependency injection registration, and in-memory or Redis-backed repositories for coordinating exclusive processing work.

Optional contention diagnostics (3.1.0)

Contention diagnostics collect evidence about slow WaitForLockAsync, WaitForLocksAsync, WaitForTenantLockAsync and WaitForTenantLocksAsync calls. They do not change acquisition, ownership, renewal, release, polling, cancellation or timeout semantics. Direct acquisition calls are observed only when they are part of one of these wait operations.

Diagnostics are disabled by default. Existing constructors, interfaces, custom repositories and AddProcessingLockService registrations continue working. Both packages are 3.1.0, following the repository's coordinated release process; the Abstractions API is unchanged. Both assembly versions remain 3.0.0.0 for compiled callers.

QPAC application configuration

Bind configuration in the QPAC application, using its existing repository configuration:

builder.Services.AddProcessingLockService(options =>
{
    // Keep QPAC's existing repository and lock/wait timeout configuration here.
    // For example: options.WithRedisRepository(existingRedisClient);
    builder.Configuration.GetSection("ProcessingLocks:ContentionDiagnostics")
        .Bind(options.ContentionDiagnostics);
});

The existing registration uses the host's ILoggerFactory when available. Direct construction can supply ProcessingLockServiceOptions.LoggerFactory. A factory is optional: without one, locking still works and no diagnostic observer starts. The library does not own or dispose the logger factory.

Example configuration for a shared tenant-aware instance:

{
  "ProcessingLocks": {
    "ContentionDiagnostics": {
      "Enabled": true,
      "TenantIds": ["<QPAC tenant ID as passed to the tenant-aware API>"],
      "SlowWaitThreshold": "00:01:00",
      "ProgressThresholds": ["00:01:00", "00:03:00"],
      "MaxEventsPerWait": 3,
      "MaxSampledLocks": 3,
      "IncludeGuidOwnerIds": false
    }
  },
  "Logging": {
    "LogLevel": { "Default": "Error" },
    "Console": {
      "LogLevel": {
        "Default": "Error",
        "Reimaginate.ProcessingLockService.Contention": "Information"
      }
    }
  }
}

Also supply ProcessingLocks__ContentionDiagnostics__FingerprintKey through QPAC's secret configuration. It must be a base64-encoded random secret of at least 32 bytes. Generate it once using a cryptographic random generator and keep it stable for the investigation; never commit it to application settings or log it. Rotating it changes all fingerprints.

For a service instance used exclusively by QPAC, use "TenantIds": [] to enable collection for that entire instance, including the legacy APIs. An empty list is instance-wide; use an explicit allowlist whenever the instance serves multiple customers. Matching is ordinal and case-sensitive. Legacy APIs use ProcessingLockTenants.LegacyGlobal (__legacy_global__), so an allowlist containing only an explicit tenant does not enable legacy calls. If several customers share legacy calls on one instance, those calls cannot be distinguished by tenant: use separate instances or the existing tenant-aware API.

Options are snapshotted at service construction; changing configuration requires recreating the service (normally an application restart). Enabled: false is the collection kill switch. It causes no diagnostic logging, repository reads, worker startup or timer work. A logging override controls visibility independently and does not enable collection. When logging is filtered out, there are still no diagnostic Redis reads; an enabled observer may still run.

Limits and delivery

Setting Default Allowed values
Enabled false Explicit instance opt-in
TenantIds Empty Instance-wide when empty, otherwise tenant allowlist
SlowWaitThreshold 60 seconds Greater than zero, at most one day
ProgressThresholds 60, 180 seconds At most 16 entries, each between the slow threshold and one day; sorted and deduplicated
MaxEventsPerWait 3 1–3, including one reserved final event
MaxSampledLocks 3 0–3 per event
IncludeGuidOwnerIds false Opt in only after confirming GUID owners are safe request correlation IDs
FingerprintKey Unset Required when enabled; base64 secret of at least 32 bytes

Invalid enabled configuration fails at construction without exposing the secret value. Disabled configuration is not validated and does not require logging or a fingerprint key. Thresholds use monotonic elapsed time; they are approximate and subject to scheduling delays. Missed thresholds are coalesced instead of producing a burst. Fast waits emit no events. For the defaults, a slow wait can enqueue progress at 60 and 180 seconds and one final event.

Each enabled wait has one one-shot timer, only while further progress events are possible. Cancellation and completion stop it, including cancellation while a repository ignores its token. This does not force the repository operation itself to stop or shorten its timeout.

Log delivery runs outside request processing and the acquisition semaphore. Each enabled service has at most one logging worker and a queue bounded to 128 events. A full queue drops new events instead of delaying locking. Throwing providers are isolated; a blocked provider can stall this worker but cannot block acquisition, release or disposal. Delivery is best effort: queue pressure, process exit or provider failure can lose progress or final events. Dispose closes the queue for a best effort drain without waiting for a provider. The library does not automatically enable console logging or change any host-wide logging settings.

Logging category and event schema

Category: Reimaginate.ProcessingLockService.Contention. Both events use Information:

Event ID Name Purpose
6100 LockWaitProgress Threshold crossed while still waiting
6101 LockWaitCompleted One final summary for a wait at least as long as the slow threshold

The structured fields are:

Field Type Meaning
WaitId String Generated ID for this wait; shared by its events
TenantHash String Tenant fingerprint; never the raw tenant ID
WaitingOwnerId String or null Safe owner correlation ID, described below
ElapsedMilliseconds Number Monotonic duration at event creation
WaitTimeoutMilliseconds Number Effective per-call wait timeout, including service default
RequestedLockCount Integer Distinct requested keys
BlockedLockCount Integer or null Total blockers from the last contention observation; null means unavailable
ObservedAtUtc UTC timestamp or null Time of that observation; may precede the final event
WaitPhase String local_semaphore, repository_acquisition, or contention_polling
Outcome String or null Null for progress; final acquired, timed_out, cancelled, or failed
FailureShortCode String or null Existing returned failure-code name, unchanged; null on success
BlockingLocksJson JSON string Array of at most MaxSampledLocks observations, never complete lock objects

Each blocker sample contains KeyHash, HoldingOwnerId, StoredTimestampUtc, AgeMilliseconds and EstimatedExpiryUtc. Times are UTC; age is milliseconds at ObservedAtUtc. Missing/default timestamps or unavailable expiry are null. The stored timestamp resets on renewal in both built-in repositories: it is not necessarily the original acquisition time. Expiry is estimated as stored timestamp plus duration, not an independently read Redis TTL; clock differences can affect these estimates.

The final event retains the last observed blockers, even if acquisition subsequently succeeds. Use ObservedAtUtc to judge freshness. Null blocker counts mean unknown, not zero. The built-in repositories provide full conflict counts and sampled holder metadata in their existing acquisition results. Custom providers may supply less metadata. No inventory scans, extra repository reads, acquisition attempts or renewals are performed for diagnostics.

The outcome is observational and does not replace public result semantics. In particular, contention expiry can still return InUse, and cancellation still returns TimeOut. An acquisition that succeeds after cancellation or after the wait deadline retains success and logs acquired, matching existing behaviour. The wait timeout is not a hard deadline on semaphore or repository calls; this feature does not introduce one.

Safe correlation

Keep existing lock owner/lockedBy values and ownership semantics. By default, all owners are emitted as hmac: plus an owner fingerprint; absent owners remain null. If QPAC confirms its GUID owner values are safe request/job correlation IDs, it may explicitly set IncludeGuidOwnerIds: true. Only then are owners formatted as GUIDs (32 hexadecimal digits or standard 36-character hyphenated form) logged verbatim for direct joins with job/API logs. A GUID alone does not prove an owner is safe: it could identify a customer. Other owner strings always remain fingerprinted. An owner is caller-supplied and need not identify a unique request. If ownership does not already identify requests, the library cannot reconstruct the holding request's correlation ID; WaitId still groups wait events.

Fingerprints are uppercase hexadecimal HMAC-SHA256 using the configured secret over the UTF-8 System.Text.Json serialization of [kind, tenantId, value]. Kinds are key, owner or tenant; tenant fingerprints use an empty string for value. QPAC can compute the same owner fingerprint when correlating existing non-GUID owner IDs. Tenant scoping prevents identical keys or owner values from being joined across customers. Explicitly opted-in GUID correlation IDs remain verbatim by design.

No raw lock keys, tenant IDs, lock tokens, credentials, payloads, failure-detail strings or exception objects/messages are emitted. Configure host enrichers separately if they add request scopes or other sensitive fields to logs.

These events provide investigation evidence. Lock durations, retry policy and lock clearing must be considered separately once that evidence is available.

Product Compatible and additional computed target framework versions.
.NET net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages (7)

Showing the top 5 NuGet packages that depend on Reimaginate.ProcessingLockService:

Package Downloads
Reimaginate.DataHub

Core DataHub runtime for request handling, data operations, validation, telemetry, and persistence integration.

Reimaginate.DataHub.D365Agent

Microsoft Dynamics 365 Customer Engagement integration agent for Reimaginate DataHub synchronization, merge processing, and Dataverse access.

Reimaginate.DataHub.Agent

Agent helpers for integrating DataHub client calls, shared models, validation, and processing lock workflows.

Reimaginate.ProcessingLockService.Redis

Package Description

Reimaginate.DataHub.Agent.Dataverse

Microsoft Dataverse integration agent for Reimaginate DataHub synchronization, merge processing, and Dataverse access.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
3.1.0 0 10/7/2026
2.1.0 2,749 7/22/2026
2.1.0-rc.1 2,993 5/19/2026
2.0.4 1,085 5/26/2025
2.0.3 588 5/18/2025
2.0.2 266 5/18/2025
2.0.1 267 5/18/2025
2.0.0 264 5/18/2025
1.3.0 125 7/22/2026
1.3.0-rc.1 89 5/19/2026
1.1.2 2,308 6/4/2024
1.1.1 1,874 3/19/2024
1.1.0 713 3/12/2024
1.0.0-preview.10 594 11/8/2023
1.0.0-preview.9 241 9/25/2023
1.0.0-preview.8 267 5/30/2023
1.0.0-preview.7 239 5/30/2023
1.0.0-preview.6 236 5/26/2023
1.0.0-preview.5 213 5/26/2023
1.0.0-preview.4 223 5/26/2023
Loading failed