{"record":{"id":"65859f8b0917f866","repo":"HangfireIO/Hangfire","slug":"can-not-release-a-distributed-lock-it-was-not-acq","errorCode":null,"errorMessage":"Can not release a distributed lock: it was not acquired.","messagePattern":"Can not release a distributed lock: it was not acquired\\.","errorType":"exception","errorClass":"InvalidOperationException","httpStatus":null,"severity":"error","filePath":"src/Hangfire.Core/DisableConcurrentExecutionAttribute.cs","lineNumber":59,"sourceCode":"\n        [CanBeNull]\n        public string Resource { get; }\n        public int TimeoutSec { get; }\n\n        public void OnPerforming(PerformingContext context)\n        {\n            var resource = GetResource(context.BackgroundJob.Job);\n            var timeout = TimeSpan.FromSeconds(TimeoutSec);\n\n            var distributedLock = context.Connection.AcquireDistributedLock(resource, timeout);\n            context.Items[\"DistributedLock\"] = distributedLock;\n        }\n\n        public void OnPerformed(PerformedContext context)\n        {\n            if (!context.Items.TryGetValue(\"DistributedLock\", out var value))\n            {\n                throw new InvalidOperationException(\"Can not release a distributed lock: it was not acquired.\");\n            }\n\n            var distributedLock = (IDisposable)value;\n            distributedLock.Dispose();\n        }\n\n        private string GetResource(Job job)\n        {\n            if (!String.IsNullOrWhiteSpace(Resource))\n            {\n                try\n                {\n                    return String.Format(CultureInfo.InvariantCulture, Resource, job.Args.ToArray()).ToLowerInvariant();\n                }\n                catch (Exception ex)\n                {\n                    throw new FormatException($\"Unable to obtain resource identifier: {ex.Message}\");\n                }","sourceCodeStart":41,"sourceCodeEnd":77,"githubUrl":"https://github.com/HangfireIO/Hangfire/blob/c236dd0f930f831ec151e436e138ddc429a02a72/src/Hangfire.Core/DisableConcurrentExecutionAttribute.cs#L41-L77","documentation":"DisableConcurrentExecutionAttribute.OnPerformed (DisableConcurrentExecutionAttribute.cs:59) attempts to release the distributed lock acquired in OnPerforming by reading it from context.Items[\"DistributedLock\"]. If the key is absent the lock was never acquired (OnPerforming threw before storing it, or a custom filter removed it), so releasing is impossible and an InvalidOperationException is thrown to signal state corruption.","triggerScenarios":"The OnPerformed handler runs but the \"DistributedLock\" item is missing from context.Items. This happens if OnPerforming threw an exception before or during AcquireDistributedLock (so the item was never set), or if another filter in the pipeline cleared context.Items.","commonSituations":"The distributed lock acquisition itself threw (storage connectivity issue, timeout) but OnPerformed still fires due to partial pipeline execution; a misbehaving custom filter that manipulates context.Items; unit testing the filter without a real OnPerforming call.","solutions":["Ensure OnPerforming completed successfully (lock acquired) before OnPerformed runs — investigate any storage errors during AcquireDistributedLock.","Do not clear or modify context.Items in custom IServerFilter implementations that run alongside DisableConcurrentExecutionAttribute.","If writing tests, simulate the full OnPerforming -> OnPerformed lifecycle rather than calling OnPerformed in isolation."],"exampleFix":"// before: test calls OnPerformed directly without OnPerforming\nfilter.OnPerformed(performedContext);\n\n// after: call OnPerforming first so the lock item is set\nfilter.OnPerforming(performingContext);\n// ... job runs ...\nfilter.OnPerformed(performedContext);","handlingStrategy":"validation","validationCode":"// In custom filters that interleave with DisableConcurrentExecution,\n// never remove context.Items entries you did not add.\n// Before OnPerformed, verify the lock item exists:\nif (!context.Items.ContainsKey(\"DistributedLock\"))\n    throw new InvalidOperationException(\n        \"DistributedLock missing — OnPerforming may have failed.\");","typeGuard":null,"tryCatchPattern":"// OnPerformed is called by the pipeline; if it throws,\n// catch in an outer IServerExceptionFilter:\npublic void OnServerException(ServerExceptionContext ctx)\n{\n    if (ctx.Exception is InvalidOperationException ex\n        && ex.Message.Contains(\"not acquired\"))\n    {\n        // log and suppress — lock acquisition failed upstream\n        ctx.ExceptionHandled = true;\n    }\n}","preventionTips":["Ensure OnPerforming succeeds (lock acquired) before OnPerformed runs — investigate storage errors.","Never clear or modify context.Items in custom IServerFilter implementations.","In unit tests, always simulate the full OnPerforming -> OnPerformed lifecycle."],"tags":["concurrency","distributed-lock","state-corruption","filter-lifecycle"],"backgroundTag":null,"analyzedSha":"c236dd0f930f831ec151e436e138ddc429a02a72","analyzedAt":"2026-08-13T20:27:11.027Z","schemaVersion":2},"datasetVersion":"2026-08-14T00:17:13.853Z"}