From 112272851baabfe49ecfb217e513621e05e137a2 Mon Sep 17 00:00:00 2001 From: Joes Date: Fri, 27 Mar 2026 16:06:31 +0800 Subject: [PATCH 1/4] perf(MongoDB): reduce allocations and improve robustness in lock hot paths MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Cache IMongoCollection reference in constructor instead of calling GetCollection on every TryAcquireAsync invocation - Promote immutable BsonDocument sub-expressions (expiredOrMissing, newFencingToken) to static readonly fields; cache newExpiresAt as an instance field (depends on expiry) to avoid rebuilding ~10 BsonDocument objects per busy-wait iteration - Move TTL index initialization to the start of TryAcquireAsync so it is triggered on every acquisition attempt, not only on success - Cache ownerFilter and renewUpdate in InnerHandle constructor; lease renewal (~every 10 s) no longer allocates new filter/update objects - Wrap ReleaseLockAsync in try-catch so network failures during Dispose do not surface unexpected exceptions to callers (TTL index provides eventual cleanup) - Add internal constructor accepting pre-parsed MongoDistributedLockOptions so MongoDistributedSynchronizationProvider can parse options once and reuse the result across all CreateLock calls - Fix stale comment "foo" → "expiresAt" in CheckIfIndexExists - Upgraded Microsoft.SourceLink.GitHub, Microsoft.Build.Tasks.Git, and Microsoft.SourceLink.Common to version 10.0.201 in DistributedLock.ZooKeeper, DistributedLock, and DistributedLockTaker. - Updated System.IO.Hashing to version 10.0.5 and added its dependencies. - Increased MongoDB.Bson and MongoDB.Driver versions to 3.7.1. - Updated System.Diagnostics.DiagnosticSource to version 10.0.5. - Adjusted DistributedLock.Oracle and DistributedLock.SqlServer versions to 1.0.5 and 1.0.7 respectively. - Made various transitive dependency updates to ensure compatibility and stability. --- src/Directory.Packages.props | 6 +- src/DistributedLock.Azure/packages.lock.json | 112 ++- src/DistributedLock.Core/packages.lock.json | 173 ++-- .../packages.lock.json | 116 ++- .../MongoDistributedLock.cs | 154 ++-- ...MongoDistributedSynchronizationProvider.cs | 4 +- .../MongoIndexInitializer.cs | 16 +- .../packages.lock.json | 149 ++-- src/DistributedLock.MySql/packages.lock.json | 114 +-- src/DistributedLock.Oracle/packages.lock.json | 78 +- .../packages.lock.json | 147 ++-- src/DistributedLock.Redis/packages.lock.json | 122 +-- .../packages.lock.json | 92 ++- src/DistributedLock.Tests/packages.lock.json | 773 +----------------- .../packages.lock.json | 121 ++- .../packages.lock.json | 116 ++- src/DistributedLock/packages.lock.json | 160 ++-- src/DistributedLockTaker/packages.lock.json | 55 +- 18 files changed, 986 insertions(+), 1522 deletions(-) diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props index b65321ae..9158afbd 100644 --- a/src/Directory.Packages.props +++ b/src/Directory.Packages.props @@ -4,10 +4,10 @@ true - + - + @@ -22,7 +22,7 @@ - + diff --git a/src/DistributedLock.Azure/packages.lock.json b/src/DistributedLock.Azure/packages.lock.json index d1945781..a4c67c2f 100644 --- a/src/DistributedLock.Azure/packages.lock.json +++ b/src/DistributedLock.Azure/packages.lock.json @@ -29,13 +29,9 @@ }, "Microsoft.SourceLink.GitHub": { "type": "Direct", - "requested": "[8.0.0, )", - "resolved": "8.0.0", - "contentHash": "G5q7OqtwIyGTkeIOAc3u2ZuV/kicQaec5EaRnc0pIeSnh9LUjj+PYQrJYBURvDt7twGl2PKA7nSN0kz1Zw5bnQ==", - "dependencies": { - "Microsoft.Build.Tasks.Git": "8.0.0", - "Microsoft.SourceLink.Common": "8.0.0" - } + "requested": "[10.0.201, )", + "resolved": "10.0.201", + "contentHash": "qxYAmO4ktzd9L+HMdnqWucxpu7bI9undPyACXOMqPyhaiMtbpbYL/n0ACyWIJlbyEJrXFwxiOaBOSasLtDvsCg==" }, "Azure.Core": { "type": "Transitive", @@ -62,21 +58,11 @@ "System.IO.Hashing": "6.0.0" } }, - "Microsoft.Build.Tasks.Git": { - "type": "Transitive", - "resolved": "8.0.0", - "contentHash": "bZKfSIKJRXLTuSzLudMFte/8CempWjVamNUR5eHJizsy+iuOuO/k2gnh7W0dHJmYY0tBf+gUErfluCv5mySAOQ==" - }, "Microsoft.NETFramework.ReferenceAssemblies.net462": { "type": "Transitive", "resolved": "1.0.3", "contentHash": "IzAV30z22ESCeQfxP29oVf4qEo8fBGXLXSU6oacv/9Iqe6PzgHDKCaWfwMBak7bSJQM0F5boXWoZS+kChztRIQ==" }, - "Microsoft.SourceLink.Common": { - "type": "Transitive", - "resolved": "8.0.0", - "contentHash": "dk9JPxTCIevS75HyEQ0E4OVAFhB2N+V9ShCXf8Q6FkUQZDkgLI12y679Nym1YqsiSysuQskT7Z+6nUf3yab6Vw==" - }, "System.Buffers": { "type": "Transitive", "resolved": "4.5.1", @@ -243,12 +229,13 @@ }, "Microsoft.SourceLink.GitHub": { "type": "Direct", - "requested": "[8.0.0, )", - "resolved": "8.0.0", - "contentHash": "G5q7OqtwIyGTkeIOAc3u2ZuV/kicQaec5EaRnc0pIeSnh9LUjj+PYQrJYBURvDt7twGl2PKA7nSN0kz1Zw5bnQ==", + "requested": "[10.0.201, )", + "resolved": "10.0.201", + "contentHash": "qxYAmO4ktzd9L+HMdnqWucxpu7bI9undPyACXOMqPyhaiMtbpbYL/n0ACyWIJlbyEJrXFwxiOaBOSasLtDvsCg==", "dependencies": { - "Microsoft.Build.Tasks.Git": "8.0.0", - "Microsoft.SourceLink.Common": "8.0.0" + "Microsoft.Build.Tasks.Git": "10.0.201", + "Microsoft.SourceLink.Common": "10.0.201", + "System.IO.Hashing": "10.0.5" } }, "NETStandard.Library": { @@ -285,8 +272,11 @@ }, "Microsoft.Build.Tasks.Git": { "type": "Transitive", - "resolved": "8.0.0", - "contentHash": "bZKfSIKJRXLTuSzLudMFte/8CempWjVamNUR5eHJizsy+iuOuO/k2gnh7W0dHJmYY0tBf+gUErfluCv5mySAOQ==" + "resolved": "10.0.201", + "contentHash": "DMYBnrFZvLnBKn14VavEuuIr31CY6YY2i2L9P8DorS/Qp6ifRR8ZPLdJCFLFfjikNq8DykbYyLd/RP6lSqHcWw==", + "dependencies": { + "System.IO.Hashing": "10.0.5" + } }, "Microsoft.NETCore.Platforms": { "type": "Transitive", @@ -295,13 +285,13 @@ }, "Microsoft.SourceLink.Common": { "type": "Transitive", - "resolved": "8.0.0", - "contentHash": "dk9JPxTCIevS75HyEQ0E4OVAFhB2N+V9ShCXf8Q6FkUQZDkgLI12y679Nym1YqsiSysuQskT7Z+6nUf3yab6Vw==" + "resolved": "10.0.201", + "contentHash": "QbBYhkjgL6rCnBfDbzsAJLlsad13TlBHqYCFDIw56OO2g6ix+9RsmY8uxiQGdWwFKbZXaXyAA6jDCzFYVGCZDw==" }, "System.Buffers": { "type": "Transitive", - "resolved": "4.5.1", - "contentHash": "Rw7ijyl1qqRS0YQD/WycNst8hUUMgrMH4FCn1nNm27M4VxchZ1js3fVjQaANHO5f3sN4isvP4a+Met9Y4YomAg==" + "resolved": "4.6.1", + "contentHash": "N8GXpmiLMtljq7gwvyS+1QvKT/W2J8sNAvx+HVg4NGmsG/H+2k/y9QI23auLJRterrzCiDH+IWAw4V/GPwsMlw==" }, "System.Diagnostics.DiagnosticSource": { "type": "Transitive", @@ -314,21 +304,21 @@ }, "System.IO.Hashing": { "type": "Transitive", - "resolved": "6.0.0", - "contentHash": "Rfm2jYCaUeGysFEZjDe7j1R4x6Z6BzumS/vUT5a1AA/AWJuGX71PoGB0RmpyX3VmrGqVnAwtfMn39OHR8Y/5+g==", + "resolved": "10.0.5", + "contentHash": "8IBJWcCT9+e4Bmevm4T7+fQEiAh133KGiz4oiVTgJckd3Q76OFdR1falgn9lpz7+C4HJvogCDJeAa2QmvbeVtg==", "dependencies": { - "System.Buffers": "4.5.1", - "System.Memory": "4.5.4" + "System.Buffers": "4.6.1", + "System.Memory": "4.6.3" } }, "System.Memory": { "type": "Transitive", - "resolved": "4.5.4", - "contentHash": "1MbJTHS1lZ4bS4FmsJjnuGJOu88ZzTT2rLvrhW7Ygic+pC0NWA+3hgAen0HRdsocuQXCkUTdFn9yHJJhsijDXw==", + "resolved": "4.6.3", + "contentHash": "qdcDOgnFZY40+Q9876JUHnlHu7bosOHX8XISRoH94fwk6hgaeQGSgfZd8srWRZNt5bV9ZW2TljcegDNxsf+96A==", "dependencies": { - "System.Buffers": "4.5.1", - "System.Numerics.Vectors": "4.4.0", - "System.Runtime.CompilerServices.Unsafe": "4.5.3" + "System.Buffers": "4.6.1", + "System.Numerics.Vectors": "4.6.1", + "System.Runtime.CompilerServices.Unsafe": "6.1.2" } }, "System.Memory.Data": { @@ -342,13 +332,13 @@ }, "System.Numerics.Vectors": { "type": "Transitive", - "resolved": "4.5.0", - "contentHash": "QQTlPTl06J/iiDbJCiepZ4H//BVraReU4O4EoRw1U02H5TLUIT7xn3GnDp9AXPSlJUDyFs4uWjWafNX6WrAojQ==" + "resolved": "4.6.1", + "contentHash": "sQxefTnhagrhoq2ReR0D/6K0zJcr9Hrd6kikeXsA1I8kOCboTavcUC4r7TSfpKFeE163uMuxZcyfO1mGO3EN8Q==" }, "System.Runtime.CompilerServices.Unsafe": { "type": "Transitive", - "resolved": "6.0.0", - "contentHash": "/iUeP3tq1S0XdNNoMz5C9twLSrM/TH+qElHkXWaPvuNOt+99G75NrV0OS2EqHx5wMN7popYjpc8oTjC1y16DLg==" + "resolved": "6.1.2", + "contentHash": "2hBr6zdbIBTDE3EhK7NSVNdX58uTK6iHW/P/Axmm9sl1xoGSLqDvMtpecn226TNwHByFokYwJmt/aQQNlO5CRw==" }, "System.Text.Encodings.Web": { "type": "Transitive", @@ -416,12 +406,13 @@ }, "Microsoft.SourceLink.GitHub": { "type": "Direct", - "requested": "[8.0.0, )", - "resolved": "8.0.0", - "contentHash": "G5q7OqtwIyGTkeIOAc3u2ZuV/kicQaec5EaRnc0pIeSnh9LUjj+PYQrJYBURvDt7twGl2PKA7nSN0kz1Zw5bnQ==", + "requested": "[10.0.201, )", + "resolved": "10.0.201", + "contentHash": "qxYAmO4ktzd9L+HMdnqWucxpu7bI9undPyACXOMqPyhaiMtbpbYL/n0ACyWIJlbyEJrXFwxiOaBOSasLtDvsCg==", "dependencies": { - "Microsoft.Build.Tasks.Git": "8.0.0", - "Microsoft.SourceLink.Common": "8.0.0" + "Microsoft.Build.Tasks.Git": "10.0.201", + "Microsoft.SourceLink.Common": "10.0.201", + "System.IO.Hashing": "10.0.5" } }, "Azure.Core": { @@ -457,26 +448,29 @@ }, "Microsoft.Build.Tasks.Git": { "type": "Transitive", - "resolved": "8.0.0", - "contentHash": "bZKfSIKJRXLTuSzLudMFte/8CempWjVamNUR5eHJizsy+iuOuO/k2gnh7W0dHJmYY0tBf+gUErfluCv5mySAOQ==" + "resolved": "10.0.201", + "contentHash": "DMYBnrFZvLnBKn14VavEuuIr31CY6YY2i2L9P8DorS/Qp6ifRR8ZPLdJCFLFfjikNq8DykbYyLd/RP6lSqHcWw==", + "dependencies": { + "System.IO.Hashing": "10.0.5" + } }, "Microsoft.SourceLink.Common": { "type": "Transitive", - "resolved": "8.0.0", - "contentHash": "dk9JPxTCIevS75HyEQ0E4OVAFhB2N+V9ShCXf8Q6FkUQZDkgLI12y679Nym1YqsiSysuQskT7Z+6nUf3yab6Vw==" + "resolved": "10.0.201", + "contentHash": "QbBYhkjgL6rCnBfDbzsAJLlsad13TlBHqYCFDIw56OO2g6ix+9RsmY8uxiQGdWwFKbZXaXyAA6jDCzFYVGCZDw==" }, "System.Buffers": { "type": "Transitive", - "resolved": "4.5.1", - "contentHash": "Rw7ijyl1qqRS0YQD/WycNst8hUUMgrMH4FCn1nNm27M4VxchZ1js3fVjQaANHO5f3sN4isvP4a+Met9Y4YomAg==" + "resolved": "4.6.1", + "contentHash": "N8GXpmiLMtljq7gwvyS+1QvKT/W2J8sNAvx+HVg4NGmsG/H+2k/y9QI23auLJRterrzCiDH+IWAw4V/GPwsMlw==" }, "System.IO.Hashing": { "type": "Transitive", - "resolved": "6.0.0", - "contentHash": "Rfm2jYCaUeGysFEZjDe7j1R4x6Z6BzumS/vUT5a1AA/AWJuGX71PoGB0RmpyX3VmrGqVnAwtfMn39OHR8Y/5+g==", + "resolved": "10.0.5", + "contentHash": "8IBJWcCT9+e4Bmevm4T7+fQEiAh133KGiz4oiVTgJckd3Q76OFdR1falgn9lpz7+C4HJvogCDJeAa2QmvbeVtg==", "dependencies": { - "System.Buffers": "4.5.1", - "System.Memory": "4.5.4" + "System.Buffers": "4.6.1", + "System.Memory": "4.6.3" } }, "System.Memory": { @@ -535,9 +529,9 @@ }, "System.Diagnostics.DiagnosticSource": { "type": "CentralTransitive", - "requested": "[10.0.1, )", - "resolved": "10.0.1", - "contentHash": "wVYO4/71Pk177uQ3TG8ZQFS3Pnmr98cF9pYxnpuIb/bMnbEWsdZZoLU/euv29mfSi2/Iuypj0TRUchPk7aqBGg==", + "requested": "[10.0.5, )", + "resolved": "10.0.5", + "contentHash": "CCbzHQ26L3jskdwHh+4bxxW84lUMIrAAmeSlpO69AlrQV0DKbj1/I+feLaLSuZeqXPr9UlSy0OcgZoXOk2a6/g==", "dependencies": { "System.Memory": "4.6.3", "System.Runtime.CompilerServices.Unsafe": "6.1.2" diff --git a/src/DistributedLock.Core/packages.lock.json b/src/DistributedLock.Core/packages.lock.json index 2f96fece..aead9bfa 100644 --- a/src/DistributedLock.Core/packages.lock.json +++ b/src/DistributedLock.Core/packages.lock.json @@ -11,12 +11,6 @@ "System.Threading.Tasks.Extensions": "4.5.4" } }, - "Microsoft.CodeAnalysis.PublicApiAnalyzers": { - "type": "Direct", - "requested": "[3.3.4, )", - "resolved": "3.3.4", - "contentHash": "kNLTfXtXUWDHVt5iaPkkiPuyHYlMgLI6SOFT4w88bfeI2vqSeGgHunFkdvlaCM8RDfcY0t2+jnesQtidRJJ/DA==" - }, "Microsoft.NETFramework.ReferenceAssemblies": { "type": "Direct", "requested": "[1.0.3, )", @@ -28,13 +22,9 @@ }, "Microsoft.SourceLink.GitHub": { "type": "Direct", - "requested": "[8.0.0, )", - "resolved": "8.0.0", - "contentHash": "G5q7OqtwIyGTkeIOAc3u2ZuV/kicQaec5EaRnc0pIeSnh9LUjj+PYQrJYBURvDt7twGl2PKA7nSN0kz1Zw5bnQ==", - "dependencies": { - "Microsoft.Build.Tasks.Git": "8.0.0", - "Microsoft.SourceLink.Common": "8.0.0" - } + "requested": "[10.0.201, )", + "resolved": "10.0.201", + "contentHash": "qxYAmO4ktzd9L+HMdnqWucxpu7bI9undPyACXOMqPyhaiMtbpbYL/n0ACyWIJlbyEJrXFwxiOaBOSasLtDvsCg==" }, "System.ValueTuple": { "type": "Direct", @@ -42,21 +32,11 @@ "resolved": "4.5.0", "contentHash": "okurQJO6NRE/apDIP23ajJ0hpiNmJ+f0BwOlB/cSqTLQlw5upkf+5+96+iG2Jw40G1fCVCyPz/FhIABUjMR+RQ==" }, - "Microsoft.Build.Tasks.Git": { - "type": "Transitive", - "resolved": "8.0.0", - "contentHash": "bZKfSIKJRXLTuSzLudMFte/8CempWjVamNUR5eHJizsy+iuOuO/k2gnh7W0dHJmYY0tBf+gUErfluCv5mySAOQ==" - }, "Microsoft.NETFramework.ReferenceAssemblies.net462": { "type": "Transitive", "resolved": "1.0.3", "contentHash": "IzAV30z22ESCeQfxP29oVf4qEo8fBGXLXSU6oacv/9Iqe6PzgHDKCaWfwMBak7bSJQM0F5boXWoZS+kChztRIQ==" }, - "Microsoft.SourceLink.Common": { - "type": "Transitive", - "resolved": "8.0.0", - "contentHash": "dk9JPxTCIevS75HyEQ0E4OVAFhB2N+V9ShCXf8Q6FkUQZDkgLI12y679Nym1YqsiSysuQskT7Z+6nUf3yab6Vw==" - }, "System.Runtime.CompilerServices.Unsafe": { "type": "Transitive", "resolved": "4.5.3", @@ -81,20 +61,15 @@ "System.Threading.Tasks.Extensions": "4.5.4" } }, - "Microsoft.CodeAnalysis.PublicApiAnalyzers": { - "type": "Direct", - "requested": "[3.3.4, )", - "resolved": "3.3.4", - "contentHash": "kNLTfXtXUWDHVt5iaPkkiPuyHYlMgLI6SOFT4w88bfeI2vqSeGgHunFkdvlaCM8RDfcY0t2+jnesQtidRJJ/DA==" - }, "Microsoft.SourceLink.GitHub": { "type": "Direct", - "requested": "[8.0.0, )", - "resolved": "8.0.0", - "contentHash": "G5q7OqtwIyGTkeIOAc3u2ZuV/kicQaec5EaRnc0pIeSnh9LUjj+PYQrJYBURvDt7twGl2PKA7nSN0kz1Zw5bnQ==", + "requested": "[10.0.201, )", + "resolved": "10.0.201", + "contentHash": "qxYAmO4ktzd9L+HMdnqWucxpu7bI9undPyACXOMqPyhaiMtbpbYL/n0ACyWIJlbyEJrXFwxiOaBOSasLtDvsCg==", "dependencies": { - "Microsoft.Build.Tasks.Git": "8.0.0", - "Microsoft.SourceLink.Common": "8.0.0" + "Microsoft.Build.Tasks.Git": "10.0.201", + "Microsoft.SourceLink.Common": "10.0.201", + "System.IO.Hashing": "10.0.5" } }, "NETStandard.Library": { @@ -108,8 +83,11 @@ }, "Microsoft.Build.Tasks.Git": { "type": "Transitive", - "resolved": "8.0.0", - "contentHash": "bZKfSIKJRXLTuSzLudMFte/8CempWjVamNUR5eHJizsy+iuOuO/k2gnh7W0dHJmYY0tBf+gUErfluCv5mySAOQ==" + "resolved": "10.0.201", + "contentHash": "DMYBnrFZvLnBKn14VavEuuIr31CY6YY2i2L9P8DorS/Qp6ifRR8ZPLdJCFLFfjikNq8DykbYyLd/RP6lSqHcWw==", + "dependencies": { + "System.IO.Hashing": "10.0.5" + } }, "Microsoft.NETCore.Platforms": { "type": "Transitive", @@ -118,13 +96,42 @@ }, "Microsoft.SourceLink.Common": { "type": "Transitive", - "resolved": "8.0.0", - "contentHash": "dk9JPxTCIevS75HyEQ0E4OVAFhB2N+V9ShCXf8Q6FkUQZDkgLI12y679Nym1YqsiSysuQskT7Z+6nUf3yab6Vw==" + "resolved": "10.0.201", + "contentHash": "QbBYhkjgL6rCnBfDbzsAJLlsad13TlBHqYCFDIw56OO2g6ix+9RsmY8uxiQGdWwFKbZXaXyAA6jDCzFYVGCZDw==" + }, + "System.Buffers": { + "type": "Transitive", + "resolved": "4.6.1", + "contentHash": "N8GXpmiLMtljq7gwvyS+1QvKT/W2J8sNAvx+HVg4NGmsG/H+2k/y9QI23auLJRterrzCiDH+IWAw4V/GPwsMlw==" + }, + "System.IO.Hashing": { + "type": "Transitive", + "resolved": "10.0.5", + "contentHash": "8IBJWcCT9+e4Bmevm4T7+fQEiAh133KGiz4oiVTgJckd3Q76OFdR1falgn9lpz7+C4HJvogCDJeAa2QmvbeVtg==", + "dependencies": { + "System.Buffers": "4.6.1", + "System.Memory": "4.6.3" + } + }, + "System.Memory": { + "type": "Transitive", + "resolved": "4.6.3", + "contentHash": "qdcDOgnFZY40+Q9876JUHnlHu7bosOHX8XISRoH94fwk6hgaeQGSgfZd8srWRZNt5bV9ZW2TljcegDNxsf+96A==", + "dependencies": { + "System.Buffers": "4.6.1", + "System.Numerics.Vectors": "4.6.1", + "System.Runtime.CompilerServices.Unsafe": "6.1.2" + } + }, + "System.Numerics.Vectors": { + "type": "Transitive", + "resolved": "4.6.1", + "contentHash": "sQxefTnhagrhoq2ReR0D/6K0zJcr9Hrd6kikeXsA1I8kOCboTavcUC4r7TSfpKFeE163uMuxZcyfO1mGO3EN8Q==" }, "System.Runtime.CompilerServices.Unsafe": { "type": "Transitive", - "resolved": "4.5.3", - "contentHash": "3TIsJhD1EiiT0w2CcDMN/iSSwnNnsrnbzeVHSKkaEgV85txMprmuO+Yq2AdSbeVGcg28pdNDTPK87tJhX7VFHw==" + "resolved": "6.1.2", + "contentHash": "2hBr6zdbIBTDE3EhK7NSVNdX58uTK6iHW/P/Axmm9sl1xoGSLqDvMtpecn226TNwHByFokYwJmt/aQQNlO5CRw==" }, "System.Threading.Tasks.Extensions": { "type": "Transitive", @@ -136,65 +143,85 @@ } }, ".NETStandard,Version=v2.1": { - "Microsoft.CodeAnalysis.PublicApiAnalyzers": { - "type": "Direct", - "requested": "[3.3.4, )", - "resolved": "3.3.4", - "contentHash": "kNLTfXtXUWDHVt5iaPkkiPuyHYlMgLI6SOFT4w88bfeI2vqSeGgHunFkdvlaCM8RDfcY0t2+jnesQtidRJJ/DA==" - }, "Microsoft.SourceLink.GitHub": { "type": "Direct", - "requested": "[8.0.0, )", - "resolved": "8.0.0", - "contentHash": "G5q7OqtwIyGTkeIOAc3u2ZuV/kicQaec5EaRnc0pIeSnh9LUjj+PYQrJYBURvDt7twGl2PKA7nSN0kz1Zw5bnQ==", + "requested": "[10.0.201, )", + "resolved": "10.0.201", + "contentHash": "qxYAmO4ktzd9L+HMdnqWucxpu7bI9undPyACXOMqPyhaiMtbpbYL/n0ACyWIJlbyEJrXFwxiOaBOSasLtDvsCg==", "dependencies": { - "Microsoft.Build.Tasks.Git": "8.0.0", - "Microsoft.SourceLink.Common": "8.0.0" + "Microsoft.Build.Tasks.Git": "10.0.201", + "Microsoft.SourceLink.Common": "10.0.201", + "System.IO.Hashing": "10.0.5" } }, "Microsoft.Build.Tasks.Git": { "type": "Transitive", - "resolved": "8.0.0", - "contentHash": "bZKfSIKJRXLTuSzLudMFte/8CempWjVamNUR5eHJizsy+iuOuO/k2gnh7W0dHJmYY0tBf+gUErfluCv5mySAOQ==" + "resolved": "10.0.201", + "contentHash": "DMYBnrFZvLnBKn14VavEuuIr31CY6YY2i2L9P8DorS/Qp6ifRR8ZPLdJCFLFfjikNq8DykbYyLd/RP6lSqHcWw==", + "dependencies": { + "System.IO.Hashing": "10.0.5" + } }, "Microsoft.SourceLink.Common": { "type": "Transitive", - "resolved": "8.0.0", - "contentHash": "dk9JPxTCIevS75HyEQ0E4OVAFhB2N+V9ShCXf8Q6FkUQZDkgLI12y679Nym1YqsiSysuQskT7Z+6nUf3yab6Vw==" + "resolved": "10.0.201", + "contentHash": "QbBYhkjgL6rCnBfDbzsAJLlsad13TlBHqYCFDIw56OO2g6ix+9RsmY8uxiQGdWwFKbZXaXyAA6jDCzFYVGCZDw==" + }, + "System.Buffers": { + "type": "Transitive", + "resolved": "4.6.1", + "contentHash": "N8GXpmiLMtljq7gwvyS+1QvKT/W2J8sNAvx+HVg4NGmsG/H+2k/y9QI23auLJRterrzCiDH+IWAw4V/GPwsMlw==" + }, + "System.IO.Hashing": { + "type": "Transitive", + "resolved": "10.0.5", + "contentHash": "8IBJWcCT9+e4Bmevm4T7+fQEiAh133KGiz4oiVTgJckd3Q76OFdR1falgn9lpz7+C4HJvogCDJeAa2QmvbeVtg==", + "dependencies": { + "System.Buffers": "4.6.1", + "System.Memory": "4.6.3" + } + }, + "System.Memory": { + "type": "Transitive", + "resolved": "4.6.3", + "contentHash": "qdcDOgnFZY40+Q9876JUHnlHu7bosOHX8XISRoH94fwk6hgaeQGSgfZd8srWRZNt5bV9ZW2TljcegDNxsf+96A==" } }, "net8.0": { - "Microsoft.CodeAnalysis.PublicApiAnalyzers": { - "type": "Direct", - "requested": "[3.3.4, )", - "resolved": "3.3.4", - "contentHash": "kNLTfXtXUWDHVt5iaPkkiPuyHYlMgLI6SOFT4w88bfeI2vqSeGgHunFkdvlaCM8RDfcY0t2+jnesQtidRJJ/DA==" - }, "Microsoft.NET.ILLink.Tasks": { "type": "Direct", - "requested": "[8.0.4, )", - "resolved": "8.0.4", - "contentHash": "PZb5nfQ+U19nhnmnR9T1jw+LTmozhuG2eeuzuW5A7DqxD/UXW2ucjmNJqnqOuh8rdPzM3MQXoF8AfFCedJdCUw==" + "requested": "[8.0.24, )", + "resolved": "8.0.24", + "contentHash": "1gnadp//+DoGJvV4AFdzPqYPxkypaWYjYMCr7KAacV0iadsHz1nU+rrkoxBCna4FCmeKH49CisEwa7g94/MbEg==" }, "Microsoft.SourceLink.GitHub": { "type": "Direct", - "requested": "[8.0.0, )", - "resolved": "8.0.0", - "contentHash": "G5q7OqtwIyGTkeIOAc3u2ZuV/kicQaec5EaRnc0pIeSnh9LUjj+PYQrJYBURvDt7twGl2PKA7nSN0kz1Zw5bnQ==", + "requested": "[10.0.201, )", + "resolved": "10.0.201", + "contentHash": "qxYAmO4ktzd9L+HMdnqWucxpu7bI9undPyACXOMqPyhaiMtbpbYL/n0ACyWIJlbyEJrXFwxiOaBOSasLtDvsCg==", "dependencies": { - "Microsoft.Build.Tasks.Git": "8.0.0", - "Microsoft.SourceLink.Common": "8.0.0" + "Microsoft.Build.Tasks.Git": "10.0.201", + "Microsoft.SourceLink.Common": "10.0.201", + "System.IO.Hashing": "10.0.5" } }, "Microsoft.Build.Tasks.Git": { "type": "Transitive", - "resolved": "8.0.0", - "contentHash": "bZKfSIKJRXLTuSzLudMFte/8CempWjVamNUR5eHJizsy+iuOuO/k2gnh7W0dHJmYY0tBf+gUErfluCv5mySAOQ==" + "resolved": "10.0.201", + "contentHash": "DMYBnrFZvLnBKn14VavEuuIr31CY6YY2i2L9P8DorS/Qp6ifRR8ZPLdJCFLFfjikNq8DykbYyLd/RP6lSqHcWw==", + "dependencies": { + "System.IO.Hashing": "10.0.5" + } }, "Microsoft.SourceLink.Common": { "type": "Transitive", - "resolved": "8.0.0", - "contentHash": "dk9JPxTCIevS75HyEQ0E4OVAFhB2N+V9ShCXf8Q6FkUQZDkgLI12y679Nym1YqsiSysuQskT7Z+6nUf3yab6Vw==" + "resolved": "10.0.201", + "contentHash": "QbBYhkjgL6rCnBfDbzsAJLlsad13TlBHqYCFDIw56OO2g6ix+9RsmY8uxiQGdWwFKbZXaXyAA6jDCzFYVGCZDw==" + }, + "System.IO.Hashing": { + "type": "Transitive", + "resolved": "10.0.5", + "contentHash": "8IBJWcCT9+e4Bmevm4T7+fQEiAh133KGiz4oiVTgJckd3Q76OFdR1falgn9lpz7+C4HJvogCDJeAa2QmvbeVtg==" } } } diff --git a/src/DistributedLock.FileSystem/packages.lock.json b/src/DistributedLock.FileSystem/packages.lock.json index adc5221f..bdf49de5 100644 --- a/src/DistributedLock.FileSystem/packages.lock.json +++ b/src/DistributedLock.FileSystem/packages.lock.json @@ -19,13 +19,9 @@ }, "Microsoft.SourceLink.GitHub": { "type": "Direct", - "requested": "[8.0.0, )", - "resolved": "8.0.0", - "contentHash": "G5q7OqtwIyGTkeIOAc3u2ZuV/kicQaec5EaRnc0pIeSnh9LUjj+PYQrJYBURvDt7twGl2PKA7nSN0kz1Zw5bnQ==", - "dependencies": { - "Microsoft.Build.Tasks.Git": "8.0.0", - "Microsoft.SourceLink.Common": "8.0.0" - } + "requested": "[10.0.201, )", + "resolved": "10.0.201", + "contentHash": "qxYAmO4ktzd9L+HMdnqWucxpu7bI9undPyACXOMqPyhaiMtbpbYL/n0ACyWIJlbyEJrXFwxiOaBOSasLtDvsCg==" }, "Nullable": { "type": "Direct", @@ -33,21 +29,11 @@ "resolved": "1.3.1", "contentHash": "Mk4ZVDfAORTjvckQprCSehi1XgOAAlk5ez06Va/acRYEloN9t6d6zpzJRn5MEq7+RnagyFIq9r+kbWzLGd+6QA==" }, - "Microsoft.Build.Tasks.Git": { - "type": "Transitive", - "resolved": "8.0.0", - "contentHash": "bZKfSIKJRXLTuSzLudMFte/8CempWjVamNUR5eHJizsy+iuOuO/k2gnh7W0dHJmYY0tBf+gUErfluCv5mySAOQ==" - }, "Microsoft.NETFramework.ReferenceAssemblies.net462": { "type": "Transitive", "resolved": "1.0.3", "contentHash": "IzAV30z22ESCeQfxP29oVf4qEo8fBGXLXSU6oacv/9Iqe6PzgHDKCaWfwMBak7bSJQM0F5boXWoZS+kChztRIQ==" }, - "Microsoft.SourceLink.Common": { - "type": "Transitive", - "resolved": "8.0.0", - "contentHash": "dk9JPxTCIevS75HyEQ0E4OVAFhB2N+V9ShCXf8Q6FkUQZDkgLI12y679Nym1YqsiSysuQskT7Z+6nUf3yab6Vw==" - }, "System.Runtime.CompilerServices.Unsafe": { "type": "Transitive", "resolved": "4.5.3", @@ -93,12 +79,13 @@ }, "Microsoft.SourceLink.GitHub": { "type": "Direct", - "requested": "[8.0.0, )", - "resolved": "8.0.0", - "contentHash": "G5q7OqtwIyGTkeIOAc3u2ZuV/kicQaec5EaRnc0pIeSnh9LUjj+PYQrJYBURvDt7twGl2PKA7nSN0kz1Zw5bnQ==", + "requested": "[10.0.201, )", + "resolved": "10.0.201", + "contentHash": "qxYAmO4ktzd9L+HMdnqWucxpu7bI9undPyACXOMqPyhaiMtbpbYL/n0ACyWIJlbyEJrXFwxiOaBOSasLtDvsCg==", "dependencies": { - "Microsoft.Build.Tasks.Git": "8.0.0", - "Microsoft.SourceLink.Common": "8.0.0" + "Microsoft.Build.Tasks.Git": "10.0.201", + "Microsoft.SourceLink.Common": "10.0.201", + "System.IO.Hashing": "10.0.5" } }, "NETStandard.Library": { @@ -118,8 +105,11 @@ }, "Microsoft.Build.Tasks.Git": { "type": "Transitive", - "resolved": "8.0.0", - "contentHash": "bZKfSIKJRXLTuSzLudMFte/8CempWjVamNUR5eHJizsy+iuOuO/k2gnh7W0dHJmYY0tBf+gUErfluCv5mySAOQ==" + "resolved": "10.0.201", + "contentHash": "DMYBnrFZvLnBKn14VavEuuIr31CY6YY2i2L9P8DorS/Qp6ifRR8ZPLdJCFLFfjikNq8DykbYyLd/RP6lSqHcWw==", + "dependencies": { + "System.IO.Hashing": "10.0.5" + } }, "Microsoft.NETCore.Platforms": { "type": "Transitive", @@ -128,13 +118,42 @@ }, "Microsoft.SourceLink.Common": { "type": "Transitive", - "resolved": "8.0.0", - "contentHash": "dk9JPxTCIevS75HyEQ0E4OVAFhB2N+V9ShCXf8Q6FkUQZDkgLI12y679Nym1YqsiSysuQskT7Z+6nUf3yab6Vw==" + "resolved": "10.0.201", + "contentHash": "QbBYhkjgL6rCnBfDbzsAJLlsad13TlBHqYCFDIw56OO2g6ix+9RsmY8uxiQGdWwFKbZXaXyAA6jDCzFYVGCZDw==" + }, + "System.Buffers": { + "type": "Transitive", + "resolved": "4.6.1", + "contentHash": "N8GXpmiLMtljq7gwvyS+1QvKT/W2J8sNAvx+HVg4NGmsG/H+2k/y9QI23auLJRterrzCiDH+IWAw4V/GPwsMlw==" + }, + "System.IO.Hashing": { + "type": "Transitive", + "resolved": "10.0.5", + "contentHash": "8IBJWcCT9+e4Bmevm4T7+fQEiAh133KGiz4oiVTgJckd3Q76OFdR1falgn9lpz7+C4HJvogCDJeAa2QmvbeVtg==", + "dependencies": { + "System.Buffers": "4.6.1", + "System.Memory": "4.6.3" + } + }, + "System.Memory": { + "type": "Transitive", + "resolved": "4.6.3", + "contentHash": "qdcDOgnFZY40+Q9876JUHnlHu7bosOHX8XISRoH94fwk6hgaeQGSgfZd8srWRZNt5bV9ZW2TljcegDNxsf+96A==", + "dependencies": { + "System.Buffers": "4.6.1", + "System.Numerics.Vectors": "4.6.1", + "System.Runtime.CompilerServices.Unsafe": "6.1.2" + } + }, + "System.Numerics.Vectors": { + "type": "Transitive", + "resolved": "4.6.1", + "contentHash": "sQxefTnhagrhoq2ReR0D/6K0zJcr9Hrd6kikeXsA1I8kOCboTavcUC4r7TSfpKFeE163uMuxZcyfO1mGO3EN8Q==" }, "System.Runtime.CompilerServices.Unsafe": { "type": "Transitive", - "resolved": "4.5.3", - "contentHash": "3TIsJhD1EiiT0w2CcDMN/iSSwnNnsrnbzeVHSKkaEgV85txMprmuO+Yq2AdSbeVGcg28pdNDTPK87tJhX7VFHw==" + "resolved": "6.1.2", + "contentHash": "2hBr6zdbIBTDE3EhK7NSVNdX58uTK6iHW/P/Axmm9sl1xoGSLqDvMtpecn226TNwHByFokYwJmt/aQQNlO5CRw==" }, "System.Threading.Tasks.Extensions": { "type": "Transitive", @@ -169,23 +188,46 @@ }, "Microsoft.SourceLink.GitHub": { "type": "Direct", - "requested": "[8.0.0, )", - "resolved": "8.0.0", - "contentHash": "G5q7OqtwIyGTkeIOAc3u2ZuV/kicQaec5EaRnc0pIeSnh9LUjj+PYQrJYBURvDt7twGl2PKA7nSN0kz1Zw5bnQ==", + "requested": "[10.0.201, )", + "resolved": "10.0.201", + "contentHash": "qxYAmO4ktzd9L+HMdnqWucxpu7bI9undPyACXOMqPyhaiMtbpbYL/n0ACyWIJlbyEJrXFwxiOaBOSasLtDvsCg==", "dependencies": { - "Microsoft.Build.Tasks.Git": "8.0.0", - "Microsoft.SourceLink.Common": "8.0.0" + "Microsoft.Build.Tasks.Git": "10.0.201", + "Microsoft.SourceLink.Common": "10.0.201", + "System.IO.Hashing": "10.0.5" } }, "Microsoft.Build.Tasks.Git": { "type": "Transitive", - "resolved": "8.0.0", - "contentHash": "bZKfSIKJRXLTuSzLudMFte/8CempWjVamNUR5eHJizsy+iuOuO/k2gnh7W0dHJmYY0tBf+gUErfluCv5mySAOQ==" + "resolved": "10.0.201", + "contentHash": "DMYBnrFZvLnBKn14VavEuuIr31CY6YY2i2L9P8DorS/Qp6ifRR8ZPLdJCFLFfjikNq8DykbYyLd/RP6lSqHcWw==", + "dependencies": { + "System.IO.Hashing": "10.0.5" + } }, "Microsoft.SourceLink.Common": { "type": "Transitive", - "resolved": "8.0.0", - "contentHash": "dk9JPxTCIevS75HyEQ0E4OVAFhB2N+V9ShCXf8Q6FkUQZDkgLI12y679Nym1YqsiSysuQskT7Z+6nUf3yab6Vw==" + "resolved": "10.0.201", + "contentHash": "QbBYhkjgL6rCnBfDbzsAJLlsad13TlBHqYCFDIw56OO2g6ix+9RsmY8uxiQGdWwFKbZXaXyAA6jDCzFYVGCZDw==" + }, + "System.Buffers": { + "type": "Transitive", + "resolved": "4.6.1", + "contentHash": "N8GXpmiLMtljq7gwvyS+1QvKT/W2J8sNAvx+HVg4NGmsG/H+2k/y9QI23auLJRterrzCiDH+IWAw4V/GPwsMlw==" + }, + "System.IO.Hashing": { + "type": "Transitive", + "resolved": "10.0.5", + "contentHash": "8IBJWcCT9+e4Bmevm4T7+fQEiAh133KGiz4oiVTgJckd3Q76OFdR1falgn9lpz7+C4HJvogCDJeAa2QmvbeVtg==", + "dependencies": { + "System.Buffers": "4.6.1", + "System.Memory": "4.6.3" + } + }, + "System.Memory": { + "type": "Transitive", + "resolved": "4.6.3", + "contentHash": "qdcDOgnFZY40+Q9876JUHnlHu7bosOHX8XISRoH94fwk6hgaeQGSgfZd8srWRZNt5bV9ZW2TljcegDNxsf+96A==" }, "distributedlock.core": { "type": "Project" diff --git a/src/DistributedLock.MongoDB/MongoDistributedLock.cs b/src/DistributedLock.MongoDB/MongoDistributedLock.cs index 536766cd..f485e697 100644 --- a/src/DistributedLock.MongoDB/MongoDistributedLock.cs +++ b/src/DistributedLock.MongoDB/MongoDistributedLock.cs @@ -1,7 +1,6 @@ using Medallion.Threading.Internal; using MongoDB.Bson; using MongoDB.Driver; -using System.Collections.Concurrent; using System.Diagnostics; namespace Medallion.Threading.MongoDB; @@ -22,8 +21,29 @@ public sealed partial class MongoDistributedLock : IInternalDistributedLock _collection; + + // Cached immutable BsonDocument sub-expressions to reduce GC pressure on hot paths + private static readonly BsonDocument ExpiredOrMissingExpr = new( + "$lte", + new BsonArray + { + new BsonDocument("$ifNull", new BsonArray { "$expiresAt", new BsonDateTime(EpochUtc) }), + "$$NOW" + } + ); + + private static readonly BsonDocument NewFencingTokenExpr = new( + "$add", + new BsonArray + { + new BsonDocument("$ifNull", new BsonArray { "$fencingToken", 0L }), + 1L + } + ); + + private readonly BsonDocument _newExpiresAtExpr; /// /// The MongoDB key used to implement the lock @@ -47,14 +67,27 @@ public MongoDistributedLock(string key, IMongoDatabase database, Action. /// public MongoDistributedLock(string key, IMongoDatabase database, string collectionName, Action? options = null) + : this(key, database, collectionName, MongoDistributedSynchronizationOptionsBuilder.GetOptions(options)) { } + + internal MongoDistributedLock(string key, IMongoDatabase database, string collectionName, MongoDistributedLockOptions options) { - this._database = database ?? throw new ArgumentNullException(nameof(database)); + var database1 = database ?? throw new ArgumentNullException(nameof(database)); this._collectionName = collectionName ?? throw new ArgumentNullException(nameof(collectionName)); // From what I can tell, modern (and all supported) MongoDB versions have no limits on index keys or // _id lengths other than the 16MB document limit. This is so high that providing "safe name" functionality as a fallback doesn't // see worth it. this.Key = key ?? throw new ArgumentNullException(nameof(key)); - this._options = MongoDistributedSynchronizationOptionsBuilder.GetOptions(options); + this._options = options; + this._collection = database1.GetCollection(this._collectionName); + this._newExpiresAtExpr = new BsonDocument( + "$dateAdd", + new BsonDocument + { + { "startDate", "$$NOW" }, + { "unit", "millisecond" }, + { "amount", this._options.Expiry.InMilliseconds } + } + ); } ValueTask IInternalDistributedLock.InternalTryAcquireAsync(TimeoutValue timeout, CancellationToken cancellationToken) => @@ -71,16 +104,18 @@ public MongoDistributedLock(string key, IMongoDatabase database, string collecti activity?.SetTag("lock.key", this.Key); activity?.SetTag("lock.collection", this._collectionName); - var collection = this._database.GetCollection(this._collectionName); + // Ensure TTL index exists (fire-and-forget, idempotent). Triggered on every attempt + // so that cleanup is set up even when all acquisition attempts fail. + _ = IndexInitializer.InitializeTtlIndex(this._collection); // Use a unique token per acquisition attempt (like Redis' value token) var lockId = Guid.NewGuid().ToString("N"); - + // We avoid exception-driven contention (DuplicateKey) by using a single upsert on {_id == Key} // and an update pipeline that only overwrites fields when the existing lock is expired. // This is conceptually similar to Redis: SET key value NX PX . var filter = Builders.Filter.Eq(d => d.Id, this.Key); - var update = CreateAcquireUpdate(lockId, this._options.Expiry); + var update = this.CreateAcquireUpdate(lockId); var options = new FindOneAndUpdateOptions { IsUpsert = true, @@ -88,64 +123,33 @@ public MongoDistributedLock(string key, IMongoDatabase database, string collecti }; var result = SyncViaAsync.IsSynchronous - ? collection.FindOneAndUpdate(filter, update, options, cancellationToken) - : await collection.FindOneAndUpdateAsync(filter, update, options, cancellationToken).ConfigureAwait(false); + ? this._collection.FindOneAndUpdate(filter, update, options, cancellationToken) + : await this._collection.FindOneAndUpdateAsync(filter, update, options, cancellationToken).ConfigureAwait(false); // Verify we actually got the lock if (result?.LockId == lockId) { - _ = IndexInitializer.InitializeTtlIndex(collection); activity?.SetTag("lock.acquired", true); activity?.SetTag("lock.fencing_token", result.FencingToken); - return new(new(this, lockId, collection), result.FencingToken); + return new(new(this, lockId, this._collection), result.FencingToken); } activity?.SetTag("lock.acquired", false); return null; } - private static UpdateDefinition CreateAcquireUpdate(string lockId, TimeoutValue expiry) + private UpdateDefinition CreateAcquireUpdate(string lockId) { - Invariant.Require(!expiry.IsInfinite); - - // expired := ifNull(expiresAt, epoch) <= $$NOW - var expiredOrMissing = new BsonDocument( - "$lte", - new BsonArray - { - new BsonDocument("$ifNull", new BsonArray { "$expiresAt", new BsonDateTime(EpochUtc) }), - "$$NOW" - } - ); - - var newExpiresAt = new BsonDocument( - "$dateAdd", - new BsonDocument - { - { "startDate", "$$NOW" }, - { "unit", "millisecond" }, - { "amount", expiry.InMilliseconds } - } - ); - - // Increment fencing token only when acquiring a new lock - var newFencingToken = new BsonDocument( - "$add", - new BsonArray - { - new BsonDocument("$ifNull", new BsonArray { "$fencingToken", 0L }), - 1L - } - ); - + // ExpiredOrMissingExpr, _newExpiresAtExpr, and NewFencingTokenExpr are pre-cached + // immutable BsonDocuments to reduce allocations on the busy-wait hot path. var setStage = new BsonDocument( "$set", new BsonDocument { // Only overwrite lock fields when the previous lock is expired/missing - { nameof(lockId), new BsonDocument("$cond", new BsonArray { expiredOrMissing, lockId, "$lockId" }) }, - { "expiresAt", new BsonDocument("$cond", new BsonArray { expiredOrMissing, newExpiresAt, "$expiresAt" }) }, - { "acquiredAt", new BsonDocument("$cond", new BsonArray { expiredOrMissing, "$$NOW", "$acquiredAt" }) }, - { "fencingToken", new BsonDocument("$cond", new BsonArray { expiredOrMissing, newFencingToken, "$fencingToken" }) } + { nameof(lockId), new BsonDocument("$cond", new BsonArray { ExpiredOrMissingExpr, lockId, "$lockId" }) }, + { "expiresAt", new BsonDocument("$cond", new BsonArray { ExpiredOrMissingExpr, this._newExpiresAtExpr, "$expiresAt" }) }, + { "acquiredAt", new BsonDocument("$cond", new BsonArray { ExpiredOrMissingExpr, "$$NOW", "$acquiredAt" }) }, + { "fencingToken", new BsonDocument("$cond", new BsonArray { ExpiredOrMissingExpr, NewFencingTokenExpr, "$fencingToken" }) } } ); @@ -159,10 +163,13 @@ private static UpdateDefinition CreateAcquireUpdate(string lo internal sealed class InnerHandle : IAsyncDisposable, LeaseMonitor.ILeaseHandle { private readonly MongoDistributedLock _lock; - private readonly string _lockId; private readonly IMongoCollection _collection; private readonly LeaseMonitor _monitor; - + + // Cached filter and update definitions to avoid repeated allocations during renewal cycles + private readonly FilterDefinition _ownerFilter; + private readonly PipelineUpdateDefinition _renewUpdate; + public CancellationToken HandleLostToken => this._monitor.HandleLostToken; TimeoutValue LeaseMonitor.ILeaseHandle.LeaseDuration => this._lock._options.Expiry; @@ -171,8 +178,17 @@ internal sealed class InnerHandle : IAsyncDisposable, LeaseMonitor.ILeaseHandle public InnerHandle(MongoDistributedLock @lock, string lockId, IMongoCollection collection) { this._lock = @lock; - this._lockId = lockId; this._collection = collection; + + // Cache the filter that identifies this specific lock ownership + this._ownerFilter = Builders.Filter.Eq(d => d.Id, @lock.Key) + & Builders.Filter.Eq(d => d.LockId, lockId); + + // Cache the renewal update pipeline (expiry is fixed for the lock's lifetime) + this._renewUpdate = new PipelineUpdateDefinition( + new[] { new BsonDocument("$set", new BsonDocument("expiresAt", @lock._newExpiresAtExpr)) } + ); + // important to set this last, since the monitor constructor will read other fields of this this._monitor = new(this); } @@ -185,36 +201,28 @@ public async ValueTask DisposeAsync() private async ValueTask ReleaseLockAsync() { - var filter = Builders.Filter.Eq(d => d.Id, this._lock.Key) & Builders.Filter.Eq(d => d.LockId, this._lockId); - if (SyncViaAsync.IsSynchronous) + try { - this._collection.DeleteOne(filter); + if (SyncViaAsync.IsSynchronous) + { + // ReSharper disable once MethodHasAsyncOverload + this._collection.DeleteOne(this._ownerFilter); + } + else + { + await this._collection.DeleteOneAsync(this._ownerFilter, this.HandleLostToken).ConfigureAwait(false); + } } - else + catch (Exception) { - await this._collection.DeleteOneAsync(filter).ConfigureAwait(false); + // Release failure is non-fatal: the TTL index will eventually clean up expired documents. + // Swallowing exceptions here prevents surprising callers during Dispose. } } async Task LeaseMonitor.ILeaseHandle.RenewOrValidateLeaseAsync(CancellationToken cancellationToken) { - var filter = Builders.Filter.Eq(d => d.Id, this._lock.Key) & Builders.Filter.Eq(d => d.LockId, this._lockId); - - // Use server time ($$NOW) for expiry to avoid client clock skew. - var newExpiresAt = new BsonDocument( - "$dateAdd", - new BsonDocument - { - { "startDate", "$$NOW" }, - { "unit", "millisecond" }, - { "amount", this._lock._options.Expiry.InMilliseconds } - } - ); - var update = new PipelineUpdateDefinition( - new[] { new BsonDocument("$set", new BsonDocument("expiresAt", newExpiresAt)) } - ); - - var result = await this._collection.UpdateOneAsync(filter, update, cancellationToken: cancellationToken).ConfigureAwait(false); + var result = await this._collection.UpdateOneAsync(this._ownerFilter, this._renewUpdate, cancellationToken: cancellationToken).ConfigureAwait(false); return result.MatchedCount > 0 ? LeaseMonitor.LeaseState.Renewed : LeaseMonitor.LeaseState.Lost; } } diff --git a/src/DistributedLock.MongoDB/MongoDistributedSynchronizationProvider.cs b/src/DistributedLock.MongoDB/MongoDistributedSynchronizationProvider.cs index 05a00035..32a8ec67 100644 --- a/src/DistributedLock.MongoDB/MongoDistributedSynchronizationProvider.cs +++ b/src/DistributedLock.MongoDB/MongoDistributedSynchronizationProvider.cs @@ -9,7 +9,7 @@ public sealed class MongoDistributedSynchronizationProvider : IDistributedLockPr { private readonly string _collectionName; private readonly IMongoDatabase _database; - private readonly Action? _options; + private readonly MongoDistributedLockOptions _options; /// /// Constructs a that connects to the provided @@ -26,7 +26,7 @@ public MongoDistributedSynchronizationProvider(IMongoDatabase database, string c { this._database = database ?? throw new ArgumentNullException(nameof(database)); this._collectionName = collectionName ?? throw new ArgumentNullException(nameof(collectionName)); - this._options = options; + this._options = MongoDistributedSynchronizationOptionsBuilder.GetOptions(options); } /// diff --git a/src/DistributedLock.MongoDB/MongoIndexInitializer.cs b/src/DistributedLock.MongoDB/MongoIndexInitializer.cs index 8a799fd8..021c2bba 100644 --- a/src/DistributedLock.MongoDB/MongoIndexInitializer.cs +++ b/src/DistributedLock.MongoDB/MongoIndexInitializer.cs @@ -1,8 +1,9 @@ -using MongoDB.Driver; +using MongoDB.Driver; using System.Collections.Concurrent; namespace Medallion.Threading.MongoDB; +// ReSharper disable once ClassWithVirtualMembersNeverInherited.Global internal class MongoIndexInitializer { private const string IndexName = "expiresAt_ttl"; @@ -40,7 +41,7 @@ public Task InitializeTtlIndex(IMongoCollection collection) private async Task CreateIndexIfNotExistsWrapperAsync(IMongoCollection collection) { - if (await this.CreateIndexIfNotExistsAsync(collection).ConfigureAwait(false) is { } result) + if (await CreateIndexIfNotExistsAsync(collection).ConfigureAwait(false) is { } result) { return result; } @@ -54,7 +55,7 @@ public Task InitializeTtlIndex(IMongoCollection collection) // exposed for mocking internal virtual Task DelayBeforeRetry() => Task.Delay(TimeSpan.FromMinutes(1)); - private async Task CreateIndexIfNotExistsAsync(IMongoCollection collection) + private static async Task CreateIndexIfNotExistsAsync(IMongoCollection collection) { using var activity = MongoDistributedLock.ActivitySource.StartActivity(nameof(MongoIndexInitializer) + ".CreateIndexIfNotExists"); activity?.AddTag("collection", collection.CollectionNamespace.FullName); @@ -122,12 +123,13 @@ internal static async Task CheckIfIndexExists(IMongoCollection Date: Fri, 27 Mar 2026 16:21:43 +0800 Subject: [PATCH 2/4] fix(MongoDB): change collection field to Lazy> for improved initialization --- .../MongoDistributedLock.cs | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/src/DistributedLock.MongoDB/MongoDistributedLock.cs b/src/DistributedLock.MongoDB/MongoDistributedLock.cs index f485e697..ec44a0b8 100644 --- a/src/DistributedLock.MongoDB/MongoDistributedLock.cs +++ b/src/DistributedLock.MongoDB/MongoDistributedLock.cs @@ -22,7 +22,7 @@ public sealed partial class MongoDistributedLock : IInternalDistributedLock _collection; + private readonly Lazy> _collection; // Cached immutable BsonDocument sub-expressions to reduce GC pressure on hot paths private static readonly BsonDocument ExpiredOrMissingExpr = new( @@ -78,7 +78,7 @@ internal MongoDistributedLock(string key, IMongoDatabase database, string collec // see worth it. this.Key = key ?? throw new ArgumentNullException(nameof(key)); this._options = options; - this._collection = database1.GetCollection(this._collectionName); + this._collection = new(() => database1.GetCollection(this._collectionName)); this._newExpiresAtExpr = new BsonDocument( "$dateAdd", new BsonDocument @@ -104,13 +104,11 @@ internal MongoDistributedLock(string key, IMongoDatabase database, string collec activity?.SetTag("lock.key", this.Key); activity?.SetTag("lock.collection", this._collectionName); - // Ensure TTL index exists (fire-and-forget, idempotent). Triggered on every attempt - // so that cleanup is set up even when all acquisition attempts fail. - _ = IndexInitializer.InitializeTtlIndex(this._collection); - // Use a unique token per acquisition attempt (like Redis' value token) var lockId = Guid.NewGuid().ToString("N"); + var collection = this._collection.Value; + // We avoid exception-driven contention (DuplicateKey) by using a single upsert on {_id == Key} // and an update pipeline that only overwrites fields when the existing lock is expired. // This is conceptually similar to Redis: SET key value NX PX . @@ -123,15 +121,18 @@ internal MongoDistributedLock(string key, IMongoDatabase database, string collec }; var result = SyncViaAsync.IsSynchronous - ? this._collection.FindOneAndUpdate(filter, update, options, cancellationToken) - : await this._collection.FindOneAndUpdateAsync(filter, update, options, cancellationToken).ConfigureAwait(false); + ? collection.FindOneAndUpdate(filter, update, options, cancellationToken) + : await collection.FindOneAndUpdateAsync(filter, update, options, cancellationToken).ConfigureAwait(false); // Verify we actually got the lock if (result?.LockId == lockId) { + // Fire-and-forget TTL index creation only on successful acquire to avoid + // unnecessary DB calls when the lock is contended. + _ = IndexInitializer.InitializeTtlIndex(collection); activity?.SetTag("lock.acquired", true); activity?.SetTag("lock.fencing_token", result.FencingToken); - return new(new(this, lockId, this._collection), result.FencingToken); + return new(new(this, lockId, collection), result.FencingToken); } activity?.SetTag("lock.acquired", false); return null; From 4c3da54ed22bc85fbf266f2f3a863644ad2efd0a Mon Sep 17 00:00:00 2001 From: Joes Date: Fri, 27 Mar 2026 16:27:02 +0800 Subject: [PATCH 3/4] refactor(MongoDB): improve code clarity and exception handling in MongoDistributedLock --- src/DistributedLock.MongoDB/MongoDistributedLock.cs | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/DistributedLock.MongoDB/MongoDistributedLock.cs b/src/DistributedLock.MongoDB/MongoDistributedLock.cs index ec44a0b8..8be431fb 100644 --- a/src/DistributedLock.MongoDB/MongoDistributedLock.cs +++ b/src/DistributedLock.MongoDB/MongoDistributedLock.cs @@ -24,7 +24,8 @@ public sealed partial class MongoDistributedLock : IInternalDistributedLock> _collection; - // Cached immutable BsonDocument sub-expressions to reduce GC pressure on hot paths + // Shared read-only BsonDocument sub-expressions cached to reduce GC pressure on hot paths. + // BsonDocument is mutable; these instances must never be modified after initialization. private static readonly BsonDocument ExpiredOrMissingExpr = new( "$lte", new BsonArray @@ -71,14 +72,14 @@ public MongoDistributedLock(string key, IMongoDatabase database, string collecti internal MongoDistributedLock(string key, IMongoDatabase database, string collectionName, MongoDistributedLockOptions options) { - var database1 = database ?? throw new ArgumentNullException(nameof(database)); + var validatedDatabase = database ?? throw new ArgumentNullException(nameof(database)); this._collectionName = collectionName ?? throw new ArgumentNullException(nameof(collectionName)); // From what I can tell, modern (and all supported) MongoDB versions have no limits on index keys or // _id lengths other than the 16MB document limit. This is so high that providing "safe name" functionality as a fallback doesn't // see worth it. this.Key = key ?? throw new ArgumentNullException(nameof(key)); this._options = options; - this._collection = new(() => database1.GetCollection(this._collectionName)); + this._collection = new(() => validatedDatabase.GetCollection(this._collectionName)); this._newExpiresAtExpr = new BsonDocument( "$dateAdd", new BsonDocument @@ -214,10 +215,11 @@ private async ValueTask ReleaseLockAsync() await this._collection.DeleteOneAsync(this._ownerFilter, this.HandleLostToken).ConfigureAwait(false); } } - catch (Exception) + catch (Exception ex) when (ex is MongoException or TimeoutException or OperationCanceledException) { // Release failure is non-fatal: the TTL index will eventually clean up expired documents. - // Swallowing exceptions here prevents surprising callers during Dispose. + // Swallowing only expected network/write/cancellation failures prevents surprising callers + // during Dispose without hiding programming errors (e.g. ArgumentException). } } From b6e8275b42b8bd451f553823c66a79f2cdaf45ad Mon Sep 17 00:00:00 2001 From: Joes Date: Fri, 27 Mar 2026 16:33:29 +0800 Subject: [PATCH 4/4] fix(MongoDB): avoid using disposed CancellationTokenSource in DeleteOneAsync --- src/DistributedLock.MongoDB/MongoDistributedLock.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/DistributedLock.MongoDB/MongoDistributedLock.cs b/src/DistributedLock.MongoDB/MongoDistributedLock.cs index 8be431fb..272802d8 100644 --- a/src/DistributedLock.MongoDB/MongoDistributedLock.cs +++ b/src/DistributedLock.MongoDB/MongoDistributedLock.cs @@ -212,7 +212,9 @@ private async ValueTask ReleaseLockAsync() } else { - await this._collection.DeleteOneAsync(this._ownerFilter, this.HandleLostToken).ConfigureAwait(false); + // Do not use HandleLostToken here: the monitor (and its CancellationTokenSource) is + // already disposed before ReleaseLockAsync is called from DisposeAsync. + await this._collection.DeleteOneAsync(this._ownerFilter, CancellationToken.None).ConfigureAwait(false); } } catch (Exception ex) when (ex is MongoException or TimeoutException or OperationCanceledException)