├── .gitattributes ├── src ├── Playground │ ├── appsettings.json │ ├── Playground.csproj │ └── Program.cs ├── RedLock │ ├── IRedisLockManager.cs │ ├── RedLockOptions.cs │ ├── RedLockServiceCollectionExtensions.cs │ ├── RedLockOptionsProvider.cs │ ├── RedLock.csproj │ └── RedisLockManager.cs ├── Directory.Build.props └── RedLock.sln ├── .editorconfig ├── release.config.js ├── ci └── azure-pipelines.yml ├── README.md ├── .gitignore ├── CHANGELOG.md └── LICENSE /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto -------------------------------------------------------------------------------- /src/Playground/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "RedLock": { 3 | "ConnectionString": "127.0.0.1:6379" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /src/RedLock/IRedisLockManager.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | 4 | namespace Elders.RedLock 5 | { 6 | public interface IRedisLockManager 7 | { 8 | Task IsLockedAsync(string resource); 9 | 10 | Task LockAsync(string resource, TimeSpan ttl); 11 | 12 | Task ExtendLockAsync(string resource, TimeSpan ttl); 13 | 14 | Task UnlockAsync(string resource); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | ; Check http://editorconfig.org/ for more informations 2 | ; Top-most EditorConfig file 3 | ; Use with VisualStudio https://visualstudiogallery.msdn.microsoft.com/c8bccfe2-650c-4b42-bc5c-845e21f96328 4 | root = true 5 | 6 | ; 2-column space indentation 7 | [*] 8 | indent_style = space 9 | indent_size = 4 10 | trim_trailing_whitespace = true 11 | insert_final_newline = true 12 | 13 | ; Not change VS generated files 14 | [*.{sln,csroj}] 15 | trim_trailing_whitespace = false 16 | insert_final_newline = false 17 | 18 | [*.cs] 19 | indent_size = 4 20 | 21 | [*.csproj] 22 | indent_size = 2 23 | 24 | [*.nuspec] 25 | indent_size = 2 -------------------------------------------------------------------------------- /src/RedLock/RedLockOptions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.ComponentModel.DataAnnotations; 3 | 4 | namespace Elders.RedLock 5 | { 6 | public sealed class RedLockOptions 7 | { 8 | [Required(AllowEmptyStrings = false, ErrorMessage = $"{nameof(RedLockOptions)}.{nameof(ConnectionString)} is required.")] 9 | public string ConnectionString { get; set; } 10 | 11 | public ushort LockRetryCount { get; set; } = 1; 12 | 13 | public TimeSpan LockRetryDelay { get; set; } = TimeSpan.FromMilliseconds(10); 14 | 15 | /// 16 | /// https://redis.io/docs/manual/patterns/distributed-locks/#safety-arguments 17 | /// 18 | public double ClockDriveFactor { get; set; } = 0.01; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Playground/Playground.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net8.0 6 | enable 7 | 8 | 9 | 10 | 11 | PreserveNewest 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Elders OSS 5 | Elders OSS 6 | 7 | true 8 | true 9 | true 10 | snupkg 11 | $(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb 12 | 13 | 14 | 15 | true 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/RedLock/RedLockServiceCollectionExtensions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.DependencyInjection; 2 | using Microsoft.Extensions.Options; 3 | 4 | namespace Elders.RedLock 5 | { 6 | public static class RedLockServiceCollectionExtensions 7 | { 8 | public static IServiceCollection AddRedLock(this IServiceCollection services) 9 | { 10 | services.AddSingleton(); 11 | 12 | services.AddOptions(); 13 | services.AddSingleton, RedLockOptionsProvider>(); 14 | 15 | return services; 16 | } 17 | 18 | public static IServiceCollection AddRedLock(this IServiceCollection services) 19 | where TOptionsProvider : class, IConfigureOptions 20 | { 21 | services.AddSingleton(); 22 | 23 | services.AddOptions(); 24 | services.AddSingleton, TOptionsProvider>(); 25 | 26 | return services; 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /release.config.js: -------------------------------------------------------------------------------- 1 | module.exports={ 2 | plugins: [ 3 | ["@semantic-release/commit-analyzer", { 4 | releaseRules: [ 5 | {"type": "major" , "release": "major"}, 6 | {"type": "release", "release": "major"}, 7 | ], 8 | parserOpts: { 9 | "noteKeywords": ["BREAKING CHANGE", "BREAKING CHANGES", "BREAKING"] 10 | } 11 | }], 12 | 13 | ["@semantic-release/exec",{ 14 | prepareCmd: ` 15 | set -e 16 | VER=\${nextRelease.version} 17 | ##vso[build.updatebuildnumber]\${nextRelease.version} 18 | dotnet pack "src/$PROJECT_DIR/"*.csproj -o "$STAGING_PATH" -p:Configuration=Release -p:PackageVersion=$VER --verbosity Detailed 19 | `, 20 | successCmd: ` 21 | set -e 22 | echo "##vso[task.setvariable variable=newVer;]yes" 23 | `, 24 | }], 25 | 26 | "@semantic-release/release-notes-generator", 27 | "@semantic-release/changelog", 28 | "@semantic-release/git" 29 | ], 30 | 31 | branches: [ 32 | 'master', 33 | {name: 'beta', channel: 'beta', prerelease: true}, 34 | {name: 'preview', channel: 'beta', prerelease: true} 35 | ], 36 | } 37 | -------------------------------------------------------------------------------- /src/RedLock/RedLockOptionsProvider.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.ComponentModel.DataAnnotations; 4 | using System.Linq; 5 | using Microsoft.Extensions.Configuration; 6 | using Microsoft.Extensions.Options; 7 | 8 | namespace Elders.RedLock 9 | { 10 | internal sealed class RedLockOptionsProvider : IConfigureOptions 11 | { 12 | private readonly IConfiguration configuration; 13 | 14 | public RedLockOptionsProvider(IConfiguration configuration) 15 | { 16 | this.configuration = configuration; 17 | } 18 | 19 | public void Configure(RedLockOptions options) 20 | { 21 | configuration.GetSection("RedLock").Bind(options); 22 | 23 | var validationResults = new List(); 24 | var context = new ValidationContext(options); 25 | var valid = Validator.TryValidateObject(options, context, validationResults, true); 26 | if (valid) 27 | return; 28 | 29 | var msg = string.Join("\n", validationResults.Select(r => r.ErrorMessage)); 30 | throw new Exception($"Invalid configuration!':\n{msg}"); 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/RedLock/RedLock.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | Library 4 | net8.0 5 | 6 | 7 | 8 | 9 | RedLock 10 | 11 | RedLock 12 | RedLock 13 | RedLock 14 | distributed lock redis redlock cronus 15 | 16 | Copyright © Elders OSS 2013-2024 17 | Apache-2.0 18 | true 19 | 20 | https://github.com/Elders/RedLock/blob/migrate-to-dotnet6/CHANGELOG.md 21 | https://github.com/Elders/RedLock.git 22 | https://github.com/Elders/RedLock 23 | git 24 | 25 | Elders.RedLock 26 | Elders.RedLock 27 | en-US 28 | true 29 | 9.0.0 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /ci/azure-pipelines.yml: -------------------------------------------------------------------------------- 1 | --- 2 | variables: 3 | PROJECT_DIR: RedLock 4 | 5 | trigger: 6 | branches: 7 | include: [master,beta,preview] 8 | paths: 9 | exclude: [CHANGELOG.md] 10 | 11 | pool: 12 | vmImage: ubuntu-latest 13 | 14 | jobs: 15 | - job: build_pack_publish 16 | 17 | steps: 18 | - checkout: self 19 | clean: true 20 | persistCredentials: true 21 | 22 | - task: UseDotNet@2 23 | inputs: 24 | packageType: 'sdk' 25 | version: '8.x' 26 | includePreviewVersions: true 27 | 28 | - task: DotNetCoreCLI@2 29 | name: test 30 | inputs: 31 | command: test 32 | projects: '**/*Tests.csproj' 33 | 34 | - task: DotNetCoreCLI@2 35 | name: build 36 | inputs: 37 | command: build 38 | projects: 'src/$(PROJECT_DIR)/*.csproj' 39 | 40 | - task: Bash@3 41 | name: release 42 | displayName: semantic release + pack 43 | env: 44 | STAGING_PATH: $(Build.StagingDirectory) 45 | inputs: 46 | targetType: 'inline' 47 | script: | 48 | time curl -L https://github.com/Elders/blob/releases/download/SemRel-01/node_modules.tar.gz | tar mx -I pigz 49 | time npx semantic-release --no-ci 50 | # few commands for debugging purposes 51 | ls -l $STAGING_PATH/*.nupkg 52 | echo dotnet msbuild `dotnet msbuild --version` 53 | echo dotnet nuget `dotnet nuget --version` 54 | echo dotnet `dotnet --version` 55 | 56 | - task: NuGetCommand@2 57 | name: publish 58 | enabled: true 59 | condition: and(eq(variables['newVer'], 'yes'), succeeded()) 60 | inputs: 61 | command: 'push' 62 | packagesToPush: '$(Build.StagingDirectory)/*.nupkg' 63 | nuGetFeedType: 'external' 64 | publishFeedCredentials: 'CI-AzurePipelines' 65 | -------------------------------------------------------------------------------- /src/RedLock.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.1.32210.238 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RedLock", "RedLock\RedLock.csproj", "{5753C640-A66E-4C7D-A8EF-F5FCA5B1F3E6}" 7 | EndProject 8 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{CA597954-4993-405E-BA6C-EC88FFCD200B}" 9 | ProjectSection(SolutionItems) = preProject 10 | ..\README.md = ..\README.md 11 | EndProjectSection 12 | EndProject 13 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Playground", "Playground\Playground.csproj", "{B5748723-4D28-4463-8424-C1504A9FB8D7}" 14 | EndProject 15 | Global 16 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 17 | Debug|Any CPU = Debug|Any CPU 18 | Release|Any CPU = Release|Any CPU 19 | EndGlobalSection 20 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 21 | {5753C640-A66E-4C7D-A8EF-F5FCA5B1F3E6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 22 | {5753C640-A66E-4C7D-A8EF-F5FCA5B1F3E6}.Debug|Any CPU.Build.0 = Debug|Any CPU 23 | {5753C640-A66E-4C7D-A8EF-F5FCA5B1F3E6}.Release|Any CPU.ActiveCfg = Release|Any CPU 24 | {5753C640-A66E-4C7D-A8EF-F5FCA5B1F3E6}.Release|Any CPU.Build.0 = Release|Any CPU 25 | {B5748723-4D28-4463-8424-C1504A9FB8D7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 26 | {B5748723-4D28-4463-8424-C1504A9FB8D7}.Debug|Any CPU.Build.0 = Debug|Any CPU 27 | {B5748723-4D28-4463-8424-C1504A9FB8D7}.Release|Any CPU.ActiveCfg = Release|Any CPU 28 | {B5748723-4D28-4463-8424-C1504A9FB8D7}.Release|Any CPU.Build.0 = Release|Any CPU 29 | EndGlobalSection 30 | GlobalSection(SolutionProperties) = preSolution 31 | HideSolutionNode = FALSE 32 | EndGlobalSection 33 | GlobalSection(ExtensibilityGlobals) = postSolution 34 | SolutionGuid = {BDCDF8F3-E5BD-4A15-BFCD-3F74FD2FAFEA} 35 | EndGlobalSection 36 | EndGlobal 37 | -------------------------------------------------------------------------------- /src/Playground/Program.cs: -------------------------------------------------------------------------------- 1 | using Elders.RedLock; 2 | using Microsoft.Extensions.Configuration; 3 | using Microsoft.Extensions.DependencyInjection; 4 | 5 | var configuration = new ConfigurationBuilder() 6 | .AddJsonFile("appsettings.json") 7 | .Build(); 8 | 9 | var services = new ServiceCollection(); 10 | services.AddRedLock(); 11 | services.AddSingleton(configuration); 12 | 13 | var provider = services.BuildServiceProvider(); 14 | var redlock = provider.GetRequiredService(); 15 | 16 | var resource = "resource_key"; 17 | if (await redlock.LockAsync(resource, TimeSpan.FromSeconds(20))) 18 | { 19 | try 20 | { 21 | // do stuff 22 | Console.WriteLine("locked"); 23 | if (await redlock.IsLockedAsync(resource)) 24 | { 25 | // do more stuff 26 | Console.WriteLine("still locked"); 27 | await Task.Delay(TimeSpan.FromSeconds(5)); 28 | if (await redlock.ExtendLockAsync(resource, TimeSpan.FromSeconds(20))) 29 | { 30 | // do even more stuff 31 | Console.WriteLine("lock extended"); 32 | await Task.Delay(TimeSpan.FromSeconds(5)); 33 | if (await redlock.IsLockedAsync(resource)) 34 | { 35 | Console.WriteLine("still locked"); 36 | } 37 | } 38 | else 39 | { 40 | // failed to extend resource lock 41 | // fallback 42 | Console.WriteLine("failed to extend"); 43 | } 44 | } 45 | else 46 | { 47 | // do something else 48 | Console.WriteLine("not locked anymore"); 49 | } 50 | } 51 | finally 52 | { 53 | await redlock.UnlockAsync(resource); 54 | Console.WriteLine("unlocked"); 55 | } 56 | } 57 | else 58 | { 59 | // failed to lock resource 60 | // fallback 61 | Console.WriteLine("lock failed"); 62 | } 63 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # RedLock 2 | 3 | ## A C# implementation of a distributed lock algorithm with Redis based on [this documentation](https://redis.io/docs/manual/patterns/distributed-locks/) 4 | 5 | ### Setup 6 | 7 | 1. Register RedLock to your container 8 | ```csharp 9 | services.AddRedLock(); 10 | ``` 11 | 12 | 2. Configure 13 | ```json 14 | { 15 | "RedLock": { 16 | "ConnectionString": "{YOUR_REDIS_CONNECTION_STRING}", 17 | "LockRetryCount": 2, 18 | "LockRetryDelay": "00:00:00.500", 19 | "ClockDriveFactor": 0.02 20 | } 21 | } 22 | ``` 23 | | Setting | Comment | Default | 24 | | --- | --- | :---: | 25 | | ConnectionString | Your Redis connection string | (required) | 26 | | LockRetryCount | Total amount of retries to aquire lock | 1 | 27 | | LockRetryDelay | Time to wait between retries | 10 ms | 28 | | ClockDriveFactor | Factor to determine the clock drift. `clock_drift = (lock_ttl_milliseconds * ClockDriveFactor) + 2`. Adding 2 milliseconds to the drift to account for Redis expires precision (1 ms) plus the configured allowable drift factor. Read [this](https://redis.io/docs/manual/patterns/distributed-locks/#safety-arguments) for details. | 0.01 | 29 | 30 | ### Usage 31 | 32 | ```csharp 33 | var redlock = serviceProvider.GetRequiredService(); 34 | 35 | var resource = "resource_key"; 36 | if (await redlock.LockAsync(resource, TimeSpan.FromSeconds(2))) 37 | { 38 | try 39 | { 40 | // do stuff 41 | if (await redlock.IsLockedAsync(resource)) 42 | { 43 | // do more stuff 44 | if (await redlock.ExtendLockAsync(resource, TimeSpan.FromSeconds(2))) 45 | { 46 | // do even more stuff 47 | } 48 | else 49 | { 50 | // failed to extend resource lock 51 | // fallback 52 | } 53 | } 54 | else 55 | { 56 | // do something else 57 | } 58 | } 59 | finally 60 | { 61 | await redlock.UnlockAsync(resource); 62 | } 63 | } 64 | else 65 | { 66 | // failed to lock resource 67 | // fallback 68 | } 69 | ``` 70 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ##################### 2 | ## CURRENT PROJECT ## 3 | ##################### 4 | 5 | *.orig 6 | 7 | ################ 8 | ## Elders.Nyx ## 9 | ################ 10 | release.cmd 11 | */**/AssemblyInfo.cs 12 | 13 | ############################################################## 14 | ## Ignore Visual Studio temporary files, build results, and ## 15 | ## files generated by popular Visual Studio add-ons. ## 16 | ############################################################## 17 | 18 | # Ignore most popular binaries in Windows 19 | .exe 20 | .dll 21 | 22 | # Roslyn stuff 23 | *.sln.ide 24 | *.ide/ 25 | 26 | # User-specific files 27 | *.suo 28 | *.user 29 | *.userosscache 30 | *.sln.docstates 31 | 32 | # User-specific files (MonoDevelop/Xamarin Studio) 33 | *.userprefs 34 | 35 | # Build results 36 | [Dd]ebug/ 37 | [Dd]ebugPublic/ 38 | [Rr]elease/ 39 | [Rr]eleases/ 40 | x64/ 41 | x86/ 42 | build/ 43 | bld/ 44 | [Bb]in/ 45 | [Oo]bj/ 46 | 47 | # Visual Studo 2015 cache/options directory 48 | .vs/ 49 | 50 | # MSTest test Results 51 | [Tt]est[Rr]esult*/ 52 | [Bb]uild[Ll]og.* 53 | 54 | #NUNIT 55 | *.VisualState.xml 56 | TestResult.xml 57 | 58 | # Build Results of an ATL Project 59 | [Dd]ebugPS/ 60 | [Rr]eleasePS/ 61 | dlldata.c 62 | 63 | *_i.c 64 | *_p.c 65 | *_i.h 66 | *.ilk 67 | *.meta 68 | *.obj 69 | *.pch 70 | *.pdb 71 | *.pgc 72 | *.pgd 73 | *.rsp 74 | *.sbr 75 | *.tlb 76 | *.tli 77 | *.tlh 78 | *.tmp 79 | *.tmp_proj 80 | *.log 81 | *.vspscc 82 | *.vssscc 83 | .builds 84 | *.pidb 85 | *.svclog 86 | *.scc 87 | 88 | # Chutzpah Test files 89 | _Chutzpah* 90 | 91 | # Visual C++ cache files 92 | ipch/ 93 | *.aps 94 | *.ncb 95 | *.opensdf 96 | *.sdf 97 | *.cachefile 98 | 99 | # Visual Studio profiler 100 | *.psess 101 | *.vsp 102 | *.vspx 103 | 104 | # TFS 2012 Local Workspace 105 | $tf/ 106 | 107 | # Guidance Automation Toolkit 108 | *.gpState 109 | 110 | # ReSharper is a .NET coding add-in 111 | _ReSharper*/ 112 | *.[Rr]e[Ss]harper 113 | *.DotSettings.user 114 | 115 | # JustCode is a .NET coding addin-in 116 | .JustCode 117 | 118 | # TeamCity is a build add-in 119 | _TeamCity* 120 | 121 | # DotCover is a Code Coverage Tool 122 | *.dotCover 123 | 124 | # NCrunch 125 | _NCrunch_* 126 | .*crunch*.local.xml 127 | 128 | # MightyMoose 129 | *.mm.* 130 | AutoTest.Net/ 131 | 132 | # Web workbench (sass) 133 | .sass-cache/ 134 | 135 | # Installshield output folder 136 | [Ee]xpress/ 137 | 138 | # DocProject is a documentation generator add-in 139 | DocProject/buildhelp/ 140 | DocProject/Help/*.HxT 141 | DocProject/Help/*.HxC 142 | DocProject/Help/*.hhc 143 | DocProject/Help/*.hhk 144 | DocProject/Help/*.hhp 145 | DocProject/Help/Html2 146 | DocProject/Help/html 147 | 148 | # Click-Once directory 149 | publish/ 150 | 151 | # Publish Web Output 152 | *.[Pp]ublish.xml 153 | *.azurePubxml 154 | # TODO: Comment the next line if you want to checkin your web deploy settings 155 | # but database connection strings (with potential passwords) will be unencrypted 156 | *.pubxml 157 | *.publishproj 158 | 159 | # NuGet Packages 160 | *.nupkg 161 | # The packages folder can be ignored because of Package Restore 162 | **/packages/* 163 | # except build/, which is used as an MSBuild target. 164 | !**/packages/build/ 165 | # Uncomment if necessary however generally it will be regenerated when needed 166 | #!**/packages/repositories.config 167 | 168 | # Windows Azure Build Output 169 | csx/ 170 | *.build.csdef 171 | 172 | # Windows Store app package directory 173 | AppPackages/ 174 | 175 | # Others 176 | *.[Cc]ache 177 | ClientBin/ 178 | [Ss]tyle[Cc]op.* 179 | ~$* 180 | *~ 181 | *.dbmdl 182 | *.dbproj.schemaview 183 | *.pfx 184 | *.publishsettings 185 | node_modules/ 186 | bower_components/ 187 | 188 | # RIA/Silverlight projects 189 | Generated_Code/ 190 | 191 | # Backup & report files from converting an old project file 192 | # to a newer Visual Studio version. Backup files are not needed, 193 | # because we have git ;-) 194 | _UpgradeReport_Files/ 195 | Backup*/ 196 | UpgradeLog*.XML 197 | UpgradeLog*.htm 198 | 199 | # SQL Server files 200 | *.mdf 201 | *.ldf 202 | 203 | # Business Intelligence projects 204 | *.rdl.data 205 | *.bim.layout 206 | *.bim_*.settings 207 | 208 | # Microsoft Fakes 209 | FakesAssemblies/ 210 | 211 | # Node.js Tools for Visual Studio 212 | .ntvs_analysis.dat 213 | 214 | # Visual Studio 6 build log 215 | *.plg 216 | 217 | # Visual Studio 6 workspace options file 218 | *.opt 219 | 220 | ############# 221 | ## WINDOWS ## 222 | ############# 223 | # Windows image file caches 224 | Thumbs.db 225 | ehthumbs.db 226 | 227 | # Folder config file 228 | Desktop.ini 229 | 230 | # Recycle Bin used on file shares 231 | $RECYCLE.BIN/ 232 | 233 | # Windows Installer files 234 | *.cab 235 | *.msi 236 | *.msm 237 | *.msp 238 | !*.msi/ 239 | 240 | # Windows shortcuts 241 | *.lnk 242 | 243 | # Directories which start with '.' 244 | **/.*/ 245 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [9.0.2](https://github.com/Elders/RedLock/compare/v9.0.1...v9.0.2) (2024-09-19) 2 | 3 | 4 | ### Bug Fixes 5 | 6 | * prevent NullReferenceException when using the constructor accepting only options ([56ad8c3](https://github.com/Elders/RedLock/commit/56ad8c3db131074179fe35fca737d45b624bc219)) 7 | 8 | ## [9.0.1](https://github.com/Elders/RedLock/compare/v9.0.0...v9.0.1) (2024-03-27) 9 | 10 | 11 | ### Bug Fixes 12 | 13 | * Updates packages ([7bc5f33](https://github.com/Elders/RedLock/commit/7bc5f33f52ecb5fd6c9afc77a7f09056a7bdca95)) 14 | 15 | # [9.0.0](https://github.com/Elders/RedLock/compare/v8.3.0...v9.0.0) (2023-11-17) 16 | 17 | 18 | ### Bug Fixes 19 | 20 | * Configures the CI to use net8 preview ([0be952b](https://github.com/Elders/RedLock/commit/0be952b80db5e6a64515b1c760eff8d8bc189f17)) 21 | 22 | # [8.3.0](https://github.com/Elders/RedLock/compare/v8.2.1...v8.3.0) (2023-10-23) 23 | 24 | 25 | ### Features 26 | 27 | * Adds ExtendLockAsync ([23db9ab](https://github.com/Elders/RedLock/commit/23db9ab50d60b42fcf789497ff217fda7be701d6)) 28 | * extending lock duration ([851ffc6](https://github.com/Elders/RedLock/commit/851ffc60cb2c0a2b3ff2968aa0c702bc79e9c7a6)) 29 | 30 | # [8.3.0-preview.1](https://github.com/Elders/RedLock/compare/v8.2.1...v8.3.0-preview.1) (2023-02-15) 31 | 32 | 33 | ### Features 34 | 35 | * extending lock duration ([851ffc6](https://github.com/Elders/RedLock/commit/851ffc60cb2c0a2b3ff2968aa0c702bc79e9c7a6)) 36 | 37 | ## [8.2.1](https://github.com/Elders/RedLock/compare/v8.2.0...v8.2.1) (2023-01-11) 38 | 39 | 40 | ### Bug Fixes 41 | 42 | * Improves logging ([23b917c](https://github.com/Elders/RedLock/commit/23b917c7a8897eebe60b4af0bb6ee1a7f10618d8)) 43 | 44 | # [8.2.0](https://github.com/Elders/RedLock/compare/v8.1.0...v8.2.0) (2023-01-09) 45 | 46 | 47 | ### Features 48 | 49 | * Allows overriding the default config options provider ([525edc9](https://github.com/Elders/RedLock/commit/525edc9a9f4e55d5b1ecfe0c860d704f7152792d)) 50 | 51 | # [8.1.0](https://github.com/Elders/RedLock/compare/v8.0.0...v8.1.0) (2023-01-06) 52 | 53 | 54 | ### Bug Fixes 55 | 56 | * bind Redlock options from the configurations ([99aefa1](https://github.com/Elders/RedLock/commit/99aefa15dc839e71dfc02fe182ad476a8599b914)) 57 | * validate options ([b7693ac](https://github.com/Elders/RedLock/commit/b7693ac48cb85c306ed37cdc095be52e1c9e01e0)) 58 | 59 | 60 | ### Features 61 | 62 | * making the logger optional ([ae69a92](https://github.com/Elders/RedLock/commit/ae69a924d328120f82055668d09fc9c3966cb75f)) 63 | 64 | # [8.1.0-preview.2](https://github.com/Elders/RedLock/compare/v8.1.0-preview.1...v8.1.0-preview.2) (2023-01-06) 65 | 66 | 67 | ### Bug Fixes 68 | 69 | * validate options ([b7693ac](https://github.com/Elders/RedLock/commit/b7693ac48cb85c306ed37cdc095be52e1c9e01e0)) 70 | 71 | # [8.1.0-preview.1](https://github.com/Elders/RedLock/compare/v8.0.0...v8.1.0-preview.1) (2023-01-06) 72 | 73 | 74 | ### Bug Fixes 75 | 76 | * bind Redlock options from the configurations ([99aefa1](https://github.com/Elders/RedLock/commit/99aefa15dc839e71dfc02fe182ad476a8599b914)) 77 | 78 | 79 | ### Features 80 | 81 | * making the logger optional ([ae69a92](https://github.com/Elders/RedLock/commit/ae69a924d328120f82055668d09fc9c3966cb75f)) 82 | 83 | # [8.0.0](https://github.com/Elders/RedLock/compare/v7.0.1...v8.0.0) (2023-01-06) 84 | 85 | 86 | ### Bug Fixes 87 | 88 | * Fixes the retry to work properly ([7c00d3d](https://github.com/Elders/RedLock/commit/7c00d3ddff5928ed4f6e7797905914acca827b96)) 89 | 90 | # [8.0.0-preview.2](https://github.com/Elders/RedLock/compare/v8.0.0-preview.1...v8.0.0-preview.2) (2023-01-06) 91 | 92 | 93 | ### Bug Fixes 94 | 95 | * Fixes the retry to work properly ([7c00d3d](https://github.com/Elders/RedLock/commit/7c00d3ddff5928ed4f6e7797905914acca827b96)) 96 | 97 | # [8.0.0-preview.1](https://github.com/Elders/RedLock/compare/v7.0.1...v8.0.0-preview.1) (2023-01-05) 98 | 99 | ## [7.0.1](https://github.com/Elders/RedLock/compare/v7.0.0...v7.0.1) (2022-08-16) 100 | 101 | 102 | ### Bug Fixes 103 | 104 | * pipeline update ([d01b3e1](https://github.com/Elders/RedLock/commit/d01b3e10b86b14a8fb833acd7df7bd95982c4904)) 105 | 106 | # [7.0.0](https://github.com/Elders/RedLock/compare/v6.1.0...v7.0.0) (2022-05-16) 107 | 108 | # [6.1.0](https://github.com/Elders/RedLock/compare/v6.0.0...v6.1.0) (2022-03-28) 109 | 110 | 111 | ### Features 112 | 113 | * Changes Logger ([27ecc6f](https://github.com/Elders/RedLock/commit/27ecc6fd070dc38971c70cde82da74a0e3b0e4eb)) 114 | 115 | # [6.0.0](https://github.com/Elders/RedLock/compare/v5.0.0...v6.0.0) (2022-03-28) 116 | 117 | ### 5.0.0 - 09.04.2020 118 | * Using Options pattern 119 | 120 | ### 4.0.1 - 17.06.2019 121 | * Update StackExchange.Redis to 2.0.601 122 | * Update LibLog to 5.0.6 123 | * Update copy-right attribute 124 | 125 | #### 4.0.0 - 12.10.2018 126 | * Updates StackExchange.Redis package major version 127 | * Updates LibLog package major version 128 | * Fixes the project file such as nuget info, assembly name, root namespace and adds SourceLink 129 | 130 | #### 3.0.1 - 13.08.2018 131 | * Adds ConfigureAwait to async calls 132 | 133 | #### 3.0.0 - 25.07.2018 134 | * Removes Newtonsoft.Json 135 | * Removes LockResult 136 | * Removes Mutex 137 | 138 | #### 2.0.1 - 20.02.2018 139 | * Downgrades Newtonsoft.Json to 10.0.3 140 | 141 | #### 2.0.0 - 19.02.2018 142 | * Adds netstandard2.0 support 143 | 144 | #### 1.0.1 - 27.01.2016 145 | * Changed RedisLockManager constructor to work with Redis Connection String 146 | 147 | 148 | #### 1.0.0 - 27.01.2016 149 | * Changed RedisLockManager constructor to work with StackExchange.Redis.ConnectionOptions instead with IPEndpoints 150 | 151 | #### 0.1.0 - 07.01.2016 152 | * Use a 64-based string as a Redis key if an object is supplied as a resource key. 153 | * Update RedisLockManager to use Redis cluster. 154 | * RedisLockManager.UnlockInstance does not throw if the end point is unreachable. 155 | * Added dependency to LibLog. 156 | * Exceptions from LockInstance(), UnlockInstance() and IsLocked() are being logged. 157 | -------------------------------------------------------------------------------- /src/RedLock/RedisLockManager.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Threading.Tasks; 4 | using Microsoft.Extensions.Logging; 5 | using Microsoft.Extensions.Logging.Abstractions; 6 | using Microsoft.Extensions.Options; 7 | using StackExchange.Redis; 8 | 9 | namespace Elders.RedLock 10 | { 11 | public class RedisLockManager : IRedisLockManager, IDisposable 12 | { 13 | private readonly ILogger logger; 14 | 15 | private RedLockOptions options; 16 | 17 | private bool isDisposed = false; 18 | 19 | private ConnectionMultiplexer connectionDoNotUse; 20 | 21 | public RedisLockManager(IOptionsMonitor options) : this(options, NullLogger.Instance) { } 22 | 23 | public RedisLockManager(IOptionsMonitor options, ILogger logger) 24 | { 25 | this.options = options.CurrentValue; 26 | options.OnChange(x => this.options = x); 27 | this.logger = logger; 28 | } 29 | 30 | public Task LockAsync(string resource, TimeSpan ttl) 31 | { 32 | using (logger.BeginScope(new Dictionary { ["redlock_resource"] = resource })) 33 | { 34 | return RetryAsync(async () => await AcquireLockAsync(resource, ttl), options.LockRetryCount, options.LockRetryDelay); 35 | } 36 | } 37 | 38 | public Task UnlockAsync(string resource) 39 | { 40 | using (logger.BeginScope(new Dictionary { ["redlock_resource"] = resource })) 41 | { 42 | return UnlockInstanceAsync(resource); 43 | } 44 | } 45 | 46 | public async Task IsLockedAsync(string resource) 47 | { 48 | return await ExecuteAsync(async connection => 49 | await connection.GetDatabase().KeyExistsAsync(resource, CommandFlags.DemandMaster).ConfigureAwait(false)).ConfigureAwait(false); 50 | } 51 | 52 | public async Task ExtendLockAsync(string resource, TimeSpan ttl) 53 | { 54 | using (logger.BeginScope(new Dictionary { ["redlock_resource"] = resource })) 55 | { 56 | return await ExecuteAsync(async connection => 57 | await connection.GetDatabase().KeyExpireAsync(resource, ttl, ExpireWhen.Always, CommandFlags.DemandMaster).ConfigureAwait(false)).ConfigureAwait(false); 58 | } 59 | } 60 | 61 | public void Dispose() 62 | { 63 | Dispose(true); 64 | } 65 | 66 | protected virtual void Dispose(bool disposing) 67 | { 68 | if (isDisposed) return; 69 | 70 | if (disposing) 71 | { 72 | connectionDoNotUse?.Dispose(); 73 | connectionDoNotUse = null; 74 | isDisposed = true; 75 | } 76 | } 77 | 78 | private async Task AcquireLockAsync(string resource, TimeSpan ttl) 79 | { 80 | var startTime = DateTime.Now; 81 | 82 | if (await LockInstanceAsync(resource, ttl).ConfigureAwait(false) == false) 83 | { 84 | return false; 85 | } 86 | 87 | // Add 2 milliseconds to the drift to account for Redis expires precision, which is 1 ms, plus the configured allowable drift factor. 88 | // Read https://redis.io/docs/manual/patterns/distributed-locks/#safety-arguments 89 | var drift = Convert.ToInt32((ttl.TotalMilliseconds * options.ClockDriveFactor) + 2); 90 | var validityTime = ttl - (DateTime.Now - startTime) - new TimeSpan(0, 0, 0, 0, drift); 91 | 92 | if (validityTime.TotalMilliseconds > 0) 93 | return true; 94 | 95 | await UnlockInstanceAsync(resource).ConfigureAwait(false); 96 | 97 | logger.LogWarning("Unable to lock the resource. Reason1: The lock request to Redis took more than expected and the resource has been unlocked immediately. Reason2: The specified TTL for the resource '{Resource}' was too short ({Ttl}ms). Try using longer TTL value.", resource, ttl.TotalMilliseconds); 98 | 99 | return false; 100 | } 101 | 102 | private async Task LockInstanceAsync(string resource, TimeSpan ttl) 103 | { 104 | return await ExecuteAsync(async connection => 105 | { 106 | DateTimeOffset now = DateTimeOffset.UtcNow; 107 | var absExpiration = now.Add(ttl).ToFileTime(); 108 | 109 | var prevAbsExpiration = await connection.GetDatabase().StringSetAndGetAsync(resource, absExpiration, ttl, When.Always, CommandFlags.DemandMaster).ConfigureAwait(false); 110 | if (prevAbsExpiration.HasValue) 111 | { 112 | var nextAbsExpiration = DateTimeOffset.FromFileTime((long)prevAbsExpiration); 113 | var newTTl = nextAbsExpiration - now; 114 | if (newTTl.TotalMilliseconds > 0) 115 | { 116 | await connection.GetDatabase().KeyExpireAsync(resource, newTTl, CommandFlags.FireAndForget).ConfigureAwait(false); 117 | } 118 | } 119 | 120 | return prevAbsExpiration.HasValue == false; 121 | }); 122 | } 123 | 124 | private async Task UnlockInstanceAsync(string resource) 125 | { 126 | await ExecuteAsync(async connection => 127 | await connection.GetDatabase().KeyDeleteAsync(resource, CommandFlags.DemandMaster | CommandFlags.FireAndForget).ConfigureAwait(false)).ConfigureAwait(false); 128 | } 129 | 130 | private async Task RetryAsync(Func> action, ushort retryCount, TimeSpan retryDelay) 131 | { 132 | var currentRetry = 0; 133 | var result = false; 134 | 135 | while (currentRetry++ <= retryCount) 136 | { 137 | try 138 | { 139 | result = await action().ConfigureAwait(false); 140 | if (result) 141 | return result; 142 | } 143 | catch (Exception ex) 144 | { 145 | logger.LogError(ex, "Redlock operation has failed."); 146 | } 147 | 148 | await Task.Delay(retryDelay).ConfigureAwait(false); 149 | } 150 | 151 | return result; 152 | } 153 | 154 | private async Task ExecuteAsync(Func> theLogic) 155 | { 156 | if (connectionDoNotUse is null || (connectionDoNotUse.IsConnected == false && connectionDoNotUse.IsConnecting == false)) 157 | { 158 | try 159 | { 160 | var configurationOptions = ConfigurationOptions.Parse(options.ConnectionString); 161 | connectionDoNotUse = await ConnectionMultiplexer.ConnectAsync(configurationOptions); 162 | } 163 | catch (Exception ex) 164 | { 165 | logger.LogError(ex, "Unable to establish connection with Redis: {ConnectionString}", options.ConnectionString); 166 | throw; 167 | } 168 | } 169 | 170 | try 171 | { 172 | return await theLogic(connectionDoNotUse).ConfigureAwait(false); 173 | } 174 | catch (Exception ex) 175 | { 176 | logger.LogError(ex, "Unable to execute Redis query."); 177 | 178 | return default; 179 | } 180 | } 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, and 10 | distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by the copyright 13 | owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all other entities 16 | that control, are controlled by, or are under common control with that entity. 17 | For the purposes of this definition, "control" means (i) the power, direct or 18 | indirect, to cause the direction or management of such entity, whether by 19 | contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the 20 | outstanding shares, or (iii) beneficial ownership of such entity. 21 | 22 | "You" (or "Your") shall mean an individual or Legal Entity exercising 23 | permissions granted by this License. 24 | 25 | "Source" form shall mean the preferred form for making modifications, including 26 | but not limited to software source code, documentation source, and configuration 27 | files. 28 | 29 | "Object" form shall mean any form resulting from mechanical transformation or 30 | translation of a Source form, including but not limited to compiled object code, 31 | generated documentation, and conversions to other media types. 32 | 33 | "Work" shall mean the work of authorship, whether in Source or Object form, made 34 | available under the License, as indicated by a copyright notice that is included 35 | in or attached to the work (an example is provided in the Appendix below). 36 | 37 | "Derivative Works" shall mean any work, whether in Source or Object form, that 38 | is based on (or derived from) the Work and for which the editorial revisions, 39 | annotations, elaborations, or other modifications represent, as a whole, an 40 | original work of authorship. For the purposes of this License, Derivative Works 41 | shall not include works that remain separable from, or merely link (or bind by 42 | name) to the interfaces of, the Work and Derivative Works thereof. 43 | 44 | "Contribution" shall mean any work of authorship, including the original version 45 | of the Work and any modifications or additions to that Work or Derivative Works 46 | thereof, that is intentionally submitted to Licensor for inclusion in the Work 47 | by the copyright owner or by an individual or Legal Entity authorized to submit 48 | on behalf of the copyright owner. For the purposes of this definition, 49 | "submitted" means any form of electronic, verbal, or written communication sent 50 | to the Licensor or its representatives, including but not limited to 51 | communication on electronic mailing lists, source code control systems, and 52 | issue tracking systems that are managed by, or on behalf of, the Licensor for 53 | the purpose of discussing and improving the Work, but excluding communication 54 | that is conspicuously marked or otherwise designated in writing by the copyright 55 | owner as "Not a Contribution." 56 | 57 | "Contributor" shall mean Licensor and any individual or Legal Entity on behalf 58 | of whom a Contribution has been received by Licensor and subsequently 59 | incorporated within the Work. 60 | 61 | 2. Grant of Copyright License. 62 | 63 | Subject to the terms and conditions of this License, each Contributor hereby 64 | grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, 65 | irrevocable copyright license to reproduce, prepare Derivative Works of, 66 | publicly display, publicly perform, sublicense, and distribute the Work and such 67 | Derivative Works in Source or Object form. 68 | 69 | 3. Grant of Patent License. 70 | 71 | Subject to the terms and conditions of this License, each Contributor hereby 72 | grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, 73 | irrevocable (except as stated in this section) patent license to make, have 74 | made, use, offer to sell, sell, import, and otherwise transfer the Work, where 75 | such license applies only to those patent claims licensable by such Contributor 76 | that are necessarily infringed by their Contribution(s) alone or by combination 77 | of their Contribution(s) with the Work to which such Contribution(s) was 78 | submitted. If You institute patent litigation against any entity (including a 79 | cross-claim or counterclaim in a lawsuit) alleging that the Work or a 80 | Contribution incorporated within the Work constitutes direct or contributory 81 | patent infringement, then any patent licenses granted to You under this License 82 | for that Work shall terminate as of the date such litigation is filed. 83 | 84 | 4. Redistribution. 85 | 86 | You may reproduce and distribute copies of the Work or Derivative Works thereof 87 | in any medium, with or without modifications, and in Source or Object form, 88 | provided that You meet the following conditions: 89 | 90 | You must give any other recipients of the Work or Derivative Works a copy of 91 | this License; and 92 | You must cause any modified files to carry prominent notices stating that You 93 | changed the files; and 94 | You must retain, in the Source form of any Derivative Works that You distribute, 95 | all copyright, patent, trademark, and attribution notices from the Source form 96 | of the Work, excluding those notices that do not pertain to any part of the 97 | Derivative Works; and 98 | If the Work includes a "NOTICE" text file as part of its distribution, then any 99 | Derivative Works that You distribute must include a readable copy of the 100 | attribution notices contained within such NOTICE file, excluding those notices 101 | that do not pertain to any part of the Derivative Works, in at least one of the 102 | following places: within a NOTICE text file distributed as part of the 103 | Derivative Works; within the Source form or documentation, if provided along 104 | with the Derivative Works; or, within a display generated by the Derivative 105 | Works, if and wherever such third-party notices normally appear. The contents of 106 | the NOTICE file are for informational purposes only and do not modify the 107 | License. You may add Your own attribution notices within Derivative Works that 108 | You distribute, alongside or as an addendum to the NOTICE text from the Work, 109 | provided that such additional attribution notices cannot be construed as 110 | modifying the License. 111 | You may add Your own copyright statement to Your modifications and may provide 112 | additional or different license terms and conditions for use, reproduction, or 113 | distribution of Your modifications, or for any such Derivative Works as a whole, 114 | provided Your use, reproduction, and distribution of the Work otherwise complies 115 | with the conditions stated in this License. 116 | 117 | 5. Submission of Contributions. 118 | 119 | Unless You explicitly state otherwise, any Contribution intentionally submitted 120 | for inclusion in the Work by You to the Licensor shall be under the terms and 121 | conditions of this License, without any additional terms or conditions. 122 | Notwithstanding the above, nothing herein shall supersede or modify the terms of 123 | any separate license agreement you may have executed with Licensor regarding 124 | such Contributions. 125 | 126 | 6. Trademarks. 127 | 128 | This License does not grant permission to use the trade names, trademarks, 129 | service marks, or product names of the Licensor, except as required for 130 | reasonable and customary use in describing the origin of the Work and 131 | reproducing the content of the NOTICE file. 132 | 133 | 7. Disclaimer of Warranty. 134 | 135 | Unless required by applicable law or agreed to in writing, Licensor provides the 136 | Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, 137 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, 138 | including, without limitation, any warranties or conditions of TITLE, 139 | NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are 140 | solely responsible for determining the appropriateness of using or 141 | redistributing the Work and assume any risks associated with Your exercise of 142 | permissions under this License. 143 | 144 | 8. Limitation of Liability. 145 | 146 | In no event and under no legal theory, whether in tort (including negligence), 147 | contract, or otherwise, unless required by applicable law (such as deliberate 148 | and grossly negligent acts) or agreed to in writing, shall any Contributor be 149 | liable to You for damages, including any direct, indirect, special, incidental, 150 | or consequential damages of any character arising as a result of this License or 151 | out of the use or inability to use the Work (including but not limited to 152 | damages for loss of goodwill, work stoppage, computer failure or malfunction, or 153 | any and all other commercial damages or losses), even if such Contributor has 154 | been advised of the possibility of such damages. 155 | 156 | 9. Accepting Warranty or Additional Liability. 157 | 158 | While redistributing the Work or Derivative Works thereof, You may choose to 159 | offer, and charge a fee for, acceptance of support, warranty, indemnity, or 160 | other liability obligations and/or rights consistent with this License. However, 161 | in accepting such obligations, You may act only on Your own behalf and on Your 162 | sole responsibility, not on behalf of any other Contributor, and only if You 163 | agree to indemnify, defend, and hold each Contributor harmless for any liability 164 | incurred by, or claims asserted against, such Contributor by reason of your 165 | accepting any such warranty or additional liability. 166 | 167 | END OF TERMS AND CONDITIONS 168 | 169 | APPENDIX: How to apply the Apache License to your work 170 | 171 | To apply the Apache License to your work, attach the following boilerplate 172 | notice, with the fields enclosed by brackets "[]" replaced with your own 173 | identifying information. (Don't include the brackets!) The text should be 174 | enclosed in the appropriate comment syntax for the file format. We also 175 | recommend that a file or class name and description of purpose be included on 176 | the same "printed page" as the copyright notice for easier identification within 177 | third-party archives. 178 | 179 | Copyright [yyyy] [name of copyright owner] 180 | 181 | Licensed under the Apache License, Version 2.0 (the "License"); 182 | you may not use this file except in compliance with the License. 183 | You may obtain a copy of the License at 184 | 185 | http://www.apache.org/licenses/LICENSE-2.0 186 | 187 | Unless required by applicable law or agreed to in writing, software 188 | distributed under the License is distributed on an "AS IS" BASIS, 189 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 190 | See the License for the specific language governing permissions and 191 | limitations under the License. 192 | --------------------------------------------------------------------------------