├── src ├── packages │ ├── NUnit.2.6.3 │ │ ├── license.txt │ │ ├── NUnit.2.6.3.nupkg │ │ └── lib │ │ │ └── nunit.framework.dll │ ├── PostSharp.4.0.39 │ │ ├── PostSharp.4.0.39.nupkg │ │ ├── tools │ │ │ ├── PostSharp-Tools.exe │ │ │ ├── PostSharp.MSBuild.v4.0.39.Release.dll │ │ │ ├── PostSharp.properties │ │ │ ├── uninstall.ps1 │ │ │ ├── install.ps1 │ │ │ ├── PostSharp.tasks │ │ │ └── PostSharp.targets │ │ └── lib │ │ │ ├── net35-client │ │ │ └── PostSharp.dll │ │ │ └── portable-net4+sl5+netcore45+wpa81+wp8 │ │ │ └── PostSharp.dll │ ├── ServiceStack.Text.4.0.33 │ │ ├── lib │ │ │ ├── sl5 │ │ │ │ └── ServiceStack.Text.dll │ │ │ ├── net40 │ │ │ │ └── ServiceStack.Text.dll │ │ │ └── portable-net45+win8+monotouch+monoandroid │ │ │ │ └── ServiceStack.Text.dll │ │ └── ServiceStack.Text.4.0.33.nupkg │ ├── ServiceStack.Redis.4.0.33 │ │ ├── ServiceStack.Redis.4.0.33.nupkg │ │ └── lib │ │ │ └── net40 │ │ │ └── ServiceStack.Redis.dll │ ├── ServiceStack.Common.4.0.33 │ │ ├── ServiceStack.Common.4.0.33.nupkg │ │ └── lib │ │ │ └── net40 │ │ │ ├── ServiceStack.Common.dll │ │ │ └── ServiceStack.Common.xml │ ├── ServiceStack.Interfaces.4.0.33 │ │ ├── ServiceStack.Interfaces.4.0.33.nupkg │ │ └── lib │ │ │ └── portable-wp80+sl5+net40+win8+monotouch+monoandroid │ │ │ └── ServiceStack.Interfaces.dll │ └── repositories.config ├── DistributedLockingPerMethod │ ├── App.config │ ├── MethodTrackerDto.cs │ ├── packages.config │ ├── WorkerInstance.cs │ ├── Properties │ │ └── AssemblyInfo.cs │ ├── Program.cs │ ├── MethodMutexWithFrequency.cs │ ├── DistributedLockingPerMethod.csproj │ └── MethodMutex.cs ├── ConsoleApp1 │ ├── Class1.cs │ ├── Properties │ │ └── AssemblyInfo.cs │ └── ConsoleApp1.csproj ├── RedisWithTaggingAndLocking │ ├── packages.config │ ├── Scripts │ │ ├── AddWithTags.lua │ │ └── CleanupTags.lua │ ├── Properties │ │ └── AssemblyInfo.cs │ ├── RedisClientLockingExtensions.cs │ ├── LockResult.cs │ ├── LuaResources.Designer.cs │ ├── RedisWithTaggingAndLocking.csproj │ ├── CacheException.cs │ ├── RedisClientTaggingExtensions.cs │ ├── LuaResources.resx │ └── StackifyRedisLocker.cs ├── RedisWithTaggingAndLockingTests │ ├── packages.config │ ├── Properties │ │ └── AssemblyInfo.cs │ ├── RedisWithTaggingAndLockingTests.csproj │ └── RedisTestsAndExamples.cs ├── RedisWithTaggingAndLocking.NoPostSharp.sln ├── RedisWithTaggingAndLocking.sln └── .gitignore ├── bin └── DistributedLockingPerMethod │ ├── PostSharp.dll │ ├── ServiceStack.Common.dll │ ├── ServiceStack.Redis.dll │ ├── ServiceStack.Text.dll │ ├── ServiceStack.Interfaces.dll │ ├── RedisWithTaggingAndLocking.dll │ ├── RedisWithTaggingAndLocking.pdb │ ├── DistributedLockingPerMethod.exe │ ├── DistributedLockingPerMethod.pdb │ ├── DistributedLockingPerMethod.pssym │ ├── DistributedLockingPerMethod.exe.config │ └── ServiceStack.Common.xml ├── .gitignore ├── README.md └── LICENSE /src/packages/NUnit.2.6.3/license.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stackify/redis-tagging-locking-sample/HEAD/src/packages/NUnit.2.6.3/license.txt -------------------------------------------------------------------------------- /src/packages/NUnit.2.6.3/NUnit.2.6.3.nupkg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stackify/redis-tagging-locking-sample/HEAD/src/packages/NUnit.2.6.3/NUnit.2.6.3.nupkg -------------------------------------------------------------------------------- /bin/DistributedLockingPerMethod/PostSharp.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stackify/redis-tagging-locking-sample/HEAD/bin/DistributedLockingPerMethod/PostSharp.dll -------------------------------------------------------------------------------- /src/packages/NUnit.2.6.3/lib/nunit.framework.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stackify/redis-tagging-locking-sample/HEAD/src/packages/NUnit.2.6.3/lib/nunit.framework.dll -------------------------------------------------------------------------------- /src/packages/PostSharp.4.0.39/PostSharp.4.0.39.nupkg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stackify/redis-tagging-locking-sample/HEAD/src/packages/PostSharp.4.0.39/PostSharp.4.0.39.nupkg -------------------------------------------------------------------------------- /bin/DistributedLockingPerMethod/ServiceStack.Common.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stackify/redis-tagging-locking-sample/HEAD/bin/DistributedLockingPerMethod/ServiceStack.Common.dll -------------------------------------------------------------------------------- /bin/DistributedLockingPerMethod/ServiceStack.Redis.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stackify/redis-tagging-locking-sample/HEAD/bin/DistributedLockingPerMethod/ServiceStack.Redis.dll -------------------------------------------------------------------------------- /bin/DistributedLockingPerMethod/ServiceStack.Text.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stackify/redis-tagging-locking-sample/HEAD/bin/DistributedLockingPerMethod/ServiceStack.Text.dll -------------------------------------------------------------------------------- /src/packages/PostSharp.4.0.39/tools/PostSharp-Tools.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stackify/redis-tagging-locking-sample/HEAD/src/packages/PostSharp.4.0.39/tools/PostSharp-Tools.exe -------------------------------------------------------------------------------- /bin/DistributedLockingPerMethod/ServiceStack.Interfaces.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stackify/redis-tagging-locking-sample/HEAD/bin/DistributedLockingPerMethod/ServiceStack.Interfaces.dll -------------------------------------------------------------------------------- /src/packages/PostSharp.4.0.39/lib/net35-client/PostSharp.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stackify/redis-tagging-locking-sample/HEAD/src/packages/PostSharp.4.0.39/lib/net35-client/PostSharp.dll -------------------------------------------------------------------------------- /bin/DistributedLockingPerMethod/RedisWithTaggingAndLocking.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stackify/redis-tagging-locking-sample/HEAD/bin/DistributedLockingPerMethod/RedisWithTaggingAndLocking.dll -------------------------------------------------------------------------------- /bin/DistributedLockingPerMethod/RedisWithTaggingAndLocking.pdb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stackify/redis-tagging-locking-sample/HEAD/bin/DistributedLockingPerMethod/RedisWithTaggingAndLocking.pdb -------------------------------------------------------------------------------- /bin/DistributedLockingPerMethod/DistributedLockingPerMethod.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stackify/redis-tagging-locking-sample/HEAD/bin/DistributedLockingPerMethod/DistributedLockingPerMethod.exe -------------------------------------------------------------------------------- /bin/DistributedLockingPerMethod/DistributedLockingPerMethod.pdb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stackify/redis-tagging-locking-sample/HEAD/bin/DistributedLockingPerMethod/DistributedLockingPerMethod.pdb -------------------------------------------------------------------------------- /src/packages/ServiceStack.Text.4.0.33/lib/sl5/ServiceStack.Text.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stackify/redis-tagging-locking-sample/HEAD/src/packages/ServiceStack.Text.4.0.33/lib/sl5/ServiceStack.Text.dll -------------------------------------------------------------------------------- /src/packages/ServiceStack.Redis.4.0.33/ServiceStack.Redis.4.0.33.nupkg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stackify/redis-tagging-locking-sample/HEAD/src/packages/ServiceStack.Redis.4.0.33/ServiceStack.Redis.4.0.33.nupkg -------------------------------------------------------------------------------- /src/packages/ServiceStack.Text.4.0.33/ServiceStack.Text.4.0.33.nupkg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stackify/redis-tagging-locking-sample/HEAD/src/packages/ServiceStack.Text.4.0.33/ServiceStack.Text.4.0.33.nupkg -------------------------------------------------------------------------------- /src/packages/ServiceStack.Text.4.0.33/lib/net40/ServiceStack.Text.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stackify/redis-tagging-locking-sample/HEAD/src/packages/ServiceStack.Text.4.0.33/lib/net40/ServiceStack.Text.dll -------------------------------------------------------------------------------- /src/DistributedLockingPerMethod/App.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/packages/ServiceStack.Common.4.0.33/ServiceStack.Common.4.0.33.nupkg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stackify/redis-tagging-locking-sample/HEAD/src/packages/ServiceStack.Common.4.0.33/ServiceStack.Common.4.0.33.nupkg -------------------------------------------------------------------------------- /src/packages/ServiceStack.Redis.4.0.33/lib/net40/ServiceStack.Redis.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stackify/redis-tagging-locking-sample/HEAD/src/packages/ServiceStack.Redis.4.0.33/lib/net40/ServiceStack.Redis.dll -------------------------------------------------------------------------------- /src/packages/PostSharp.4.0.39/tools/PostSharp.MSBuild.v4.0.39.Release.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stackify/redis-tagging-locking-sample/HEAD/src/packages/PostSharp.4.0.39/tools/PostSharp.MSBuild.v4.0.39.Release.dll -------------------------------------------------------------------------------- /src/packages/ServiceStack.Common.4.0.33/lib/net40/ServiceStack.Common.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stackify/redis-tagging-locking-sample/HEAD/src/packages/ServiceStack.Common.4.0.33/lib/net40/ServiceStack.Common.dll -------------------------------------------------------------------------------- /src/packages/ServiceStack.Interfaces.4.0.33/ServiceStack.Interfaces.4.0.33.nupkg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stackify/redis-tagging-locking-sample/HEAD/src/packages/ServiceStack.Interfaces.4.0.33/ServiceStack.Interfaces.4.0.33.nupkg -------------------------------------------------------------------------------- /src/packages/PostSharp.4.0.39/lib/portable-net4+sl5+netcore45+wpa81+wp8/PostSharp.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stackify/redis-tagging-locking-sample/HEAD/src/packages/PostSharp.4.0.39/lib/portable-net4+sl5+netcore45+wpa81+wp8/PostSharp.dll -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | src/RedisWithTaggingAndLocking/obj/ 2 | src/RedisWithTaggingAndLockingTests/bin/ 3 | src/RedisWithTaggingAndLockingTests/obj/ 4 | src/RedisWithTaggingAndLocking/bin/ 5 | src/DistributedLockingPerMethod/bin/ 6 | src/DistributedLockingPerMethod/obj/ 7 | -------------------------------------------------------------------------------- /src/ConsoleApp1/Class1.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace ConsoleApp1 8 | { 9 | public class Class1 10 | { 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/packages/ServiceStack.Text.4.0.33/lib/portable-net45+win8+monotouch+monoandroid/ServiceStack.Text.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stackify/redis-tagging-locking-sample/HEAD/src/packages/ServiceStack.Text.4.0.33/lib/portable-net45+win8+monotouch+monoandroid/ServiceStack.Text.dll -------------------------------------------------------------------------------- /src/packages/ServiceStack.Interfaces.4.0.33/lib/portable-wp80+sl5+net40+win8+monotouch+monoandroid/ServiceStack.Interfaces.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stackify/redis-tagging-locking-sample/HEAD/src/packages/ServiceStack.Interfaces.4.0.33/lib/portable-wp80+sl5+net40+win8+monotouch+monoandroid/ServiceStack.Interfaces.dll -------------------------------------------------------------------------------- /src/packages/repositories.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/packages/PostSharp.4.0.39/tools/PostSharp.properties: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 4.0.39 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/DistributedLockingPerMethod/MethodTrackerDto.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Runtime.Serialization; 5 | 6 | namespace DistributedLockingPerMethod 7 | { 8 | [DataContract] 9 | public class MethodTrackerDto 10 | { 11 | [DataMember] 12 | public DateTime? LastExecuted { get; set; } 13 | 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/RedisWithTaggingAndLocking/packages.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/DistributedLockingPerMethod/packages.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/RedisWithTaggingAndLockingTests/packages.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /bin/DistributedLockingPerMethod/DistributedLockingPerMethod.pssym: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/DistributedLockingPerMethod/WorkerInstance.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | 4 | namespace DistributedLockingPerMethod 5 | { 6 | public class WorkerInstance 7 | { 8 | [MethodMutexWithFrequency(10, 60)] 9 | public bool DoWork() 10 | { 11 | int workDurationSeconds = 5; // set this above the # of seconds passed to MethodMutex to explore what happens if the lock expires 12 | 13 | Console.WriteLine("{0:T} - I'm doing some work...", DateTime.Now); 14 | Thread.Sleep(workDurationSeconds * 1000); 15 | Console.WriteLine("{0:T} - Okay, I'm done!", DateTime.Now); 16 | 17 | return true; 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /bin/DistributedLockingPerMethod/DistributedLockingPerMethod.exe.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/RedisWithTaggingAndLocking/Scripts/AddWithTags.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | For a cache item and associating one or more tags. 3 | Given a cache key (KEYS[1]) and value (ARGV[1]), adds the 4 | key-value pair to the cache and associates it to the given 5 | list of tags (KEYS[2+]). If an expiration (ARGV[2]) is 6 | supplied, the key-value pair will expire accordingly, and 7 | any tags expire in ARGV[2] _or greater_ seconds. 8 | --]] 9 | local tagcount = 0 10 | local cacheKey = KEYS[1] 11 | local exp = ARGV[2] 12 | local setValue = ARGV[1] 13 | for i=2,#KEYS do 14 | local tagTtl = redis.call('ttl', KEYS[i]) 15 | tagcount = tagcount + redis.call('sadd', KEYS[i], cacheKey) 16 | redis.call('expire', KEYS[i], math.max(tagTtl, exp or 3600)) 17 | end 18 | 19 | if(setValue) then 20 | if(exp ~= nil) then 21 | redis.call('setex', cacheKey, exp, setValue) 22 | else 23 | redis.call('set', cacheKey, setValue) 24 | end 25 | end 26 | return tagcount -------------------------------------------------------------------------------- /src/RedisWithTaggingAndLocking/Scripts/CleanupTags.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Loops through the list of tags and cleans out any references to 3 | expired keys. If all keys are cleaned from a tag, removes the tag. 4 | --]] 5 | local tagList = {} 6 | local tagsRemoved = 0 7 | local keysCleaned = 0 8 | if(#tagList == 0) then 9 | tagList = redis.call('keys', '*:tag:*') 10 | end 11 | for _, tagName in pairs(tagList) do 12 | local tagType = redis.call('type', tagName) 13 | if(tagType['ok'] == 'set') then 14 | 15 | local tagKeys = redis.call('smembers', tagName) 16 | 17 | local tagActive = 0 18 | local deadKeys = {} 19 | for _, key in pairs(tagKeys) do 20 | local keyActive = redis.call('exists', key) 21 | if(keyActive == 0) then 22 | table.insert(deadKeys, key) 23 | end 24 | end 25 | 26 | if(#deadKeys > 0) then 27 | redis.call('srem', tagName, unpack(deadKeys)) 28 | end 29 | 30 | if(#deadKeys == #tagKeys) then 31 | redis.call('del', tagName) 32 | tagsRemoved = tagsRemoved + 1 33 | end 34 | keysCleaned = keysCleaned + #deadKeys 35 | end 36 | end 37 | return keysCleaned -------------------------------------------------------------------------------- /src/packages/PostSharp.4.0.39/tools/uninstall.ps1: -------------------------------------------------------------------------------- 1 | param($installPath, $toolsPath, $package, $project) 2 | 3 | $targetsFile = [System.IO.Path]::Combine($toolsPath, 'PostSharp.targets') 4 | 5 | # Need to load MSBuild assembly if it's not loaded yet. 6 | Add-Type -AssemblyName 'Microsoft.Build, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a' 7 | 8 | # Grab the loaded MSBuild project for the project 9 | $msbuild = [Microsoft.Build.Evaluation.ProjectCollection]::GlobalProjectCollection.GetLoadedProjects($project.FullName) | Select-Object -First 1 10 | 11 | $itemsToRemove = @() 12 | 13 | 14 | # Remove stuff from the project. 15 | $itemsToRemove += $msbuild.Xml.Properties | Where-Object {$_.Name.ToLowerInvariant() -eq "dontimportpostsharp" } 16 | $itemsToRemove += $msbuild.Xml.Imports | Where-Object { $_.Project.ToLowerInvariant().EndsWith("postsharp.targets") } 17 | $itemsToRemove += $msbuild.Xml.Targets | Where-Object {$_.Name.ToLowerInvariant() -eq "ensurepostsharpimported" } 18 | 19 | if ($itemsToRemove -and $itemsToRemove.length) 20 | { 21 | foreach ($itemToRemove in $itemsToRemove) 22 | { 23 | $itemToRemove.Parent.RemoveChild($itemToRemove) | out-null 24 | } 25 | 26 | $project.Save() 27 | $project.Object.Refresh() 28 | } -------------------------------------------------------------------------------- /src/ConsoleApp1/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | // General Information about an assembly is controlled through the following 6 | // set of attributes. Change these attribute values to modify the information 7 | // associated with an assembly. 8 | [assembly: AssemblyTitle("ConsoleApp1")] 9 | [assembly: AssemblyDescription("")] 10 | [assembly: AssemblyConfiguration("")] 11 | [assembly: AssemblyCompany("")] 12 | [assembly: AssemblyProduct("ConsoleApp1")] 13 | [assembly: AssemblyCopyright("Copyright © 2015")] 14 | [assembly: AssemblyTrademark("")] 15 | [assembly: AssemblyCulture("")] 16 | 17 | // Setting ComVisible to false makes the types in this assembly not visible 18 | // to COM components. If you need to access a type in this assembly from 19 | // COM, set the ComVisible attribute to true on that type. 20 | [assembly: ComVisible(false)] 21 | 22 | // The following GUID is for the ID of the typelib if this project is exposed to COM 23 | [assembly: Guid("1c21609f-02d8-4b77-803e-4821c6ba5f9a")] 24 | 25 | // Version information for an assembly consists of the following four values: 26 | // 27 | // Major Version 28 | // Minor Version 29 | // Build Number 30 | // Revision 31 | // 32 | // You can specify all the values or you can default the Build and Revision Numbers 33 | // by using the '*' as shown below: 34 | // [assembly: AssemblyVersion("1.0.*")] 35 | [assembly: AssemblyVersion("1.0.0.0")] 36 | [assembly: AssemblyFileVersion("1.0.0.0")] 37 | -------------------------------------------------------------------------------- /src/RedisWithTaggingAndLocking/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | // General Information about an assembly is controlled through the following 6 | // set of attributes. Change these attribute values to modify the information 7 | // associated with an assembly. 8 | [assembly: AssemblyTitle("RedisWithTaggingAndLocking")] 9 | [assembly: AssemblyDescription("")] 10 | [assembly: AssemblyConfiguration("")] 11 | [assembly: AssemblyCompany("")] 12 | [assembly: AssemblyProduct("RedisWithTaggingAndLocking")] 13 | [assembly: AssemblyCopyright("Copyright © 2014")] 14 | [assembly: AssemblyTrademark("")] 15 | [assembly: AssemblyCulture("")] 16 | 17 | // Setting ComVisible to false makes the types in this assembly not visible 18 | // to COM components. If you need to access a type in this assembly from 19 | // COM, set the ComVisible attribute to true on that type. 20 | [assembly: ComVisible(false)] 21 | 22 | // The following GUID is for the ID of the typelib if this project is exposed to COM 23 | [assembly: Guid("c7d77155-8a07-43e6-8433-5372fd31a49a")] 24 | 25 | // Version information for an assembly consists of the following four values: 26 | // 27 | // Major Version 28 | // Minor Version 29 | // Build Number 30 | // Revision 31 | // 32 | // You can specify all the values or you can default the Build and Revision Numbers 33 | // by using the '*' as shown below: 34 | // [assembly: AssemblyVersion("1.0.*")] 35 | [assembly: AssemblyVersion("1.0.0.0")] 36 | [assembly: AssemblyFileVersion("1.0.0.0")] 37 | -------------------------------------------------------------------------------- /src/DistributedLockingPerMethod/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | // General Information about an assembly is controlled through the following 6 | // set of attributes. Change these attribute values to modify the information 7 | // associated with an assembly. 8 | [assembly: AssemblyTitle("DistributedLockingPerMethod")] 9 | [assembly: AssemblyDescription("")] 10 | [assembly: AssemblyConfiguration("")] 11 | [assembly: AssemblyCompany("")] 12 | [assembly: AssemblyProduct("DistributedLockingPerMethod")] 13 | [assembly: AssemblyCopyright("Copyright © 2015")] 14 | [assembly: AssemblyTrademark("")] 15 | [assembly: AssemblyCulture("")] 16 | 17 | // Setting ComVisible to false makes the types in this assembly not visible 18 | // to COM components. If you need to access a type in this assembly from 19 | // COM, set the ComVisible attribute to true on that type. 20 | [assembly: ComVisible(false)] 21 | 22 | // The following GUID is for the ID of the typelib if this project is exposed to COM 23 | [assembly: Guid("41d0f17a-99ef-42cf-9ba7-33202b38a6a8")] 24 | 25 | // Version information for an assembly consists of the following four values: 26 | // 27 | // Major Version 28 | // Minor Version 29 | // Build Number 30 | // Revision 31 | // 32 | // You can specify all the values or you can default the Build and Revision Numbers 33 | // by using the '*' as shown below: 34 | // [assembly: AssemblyVersion("1.0.*")] 35 | [assembly: AssemblyVersion("1.0.0.0")] 36 | [assembly: AssemblyFileVersion("1.0.0.0")] 37 | -------------------------------------------------------------------------------- /src/RedisWithTaggingAndLocking/RedisClientLockingExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using ServiceStack.Redis; 3 | 4 | namespace RedisWithTaggingAndLocking 5 | { 6 | public static class RedisClientLockingExtensions 7 | { 8 | /// 9 | /// Attempts to acuire the distributed lock for a given key from Redis. 10 | /// 11 | /// 12 | /// A whose 13 | /// property will be true if the lock was granted. 14 | /// 15 | public static StackifyRedisLocker AcquireDlmLock(this IRedisClient client, string key) 16 | { 17 | return new StackifyRedisLocker(client, key); 18 | } 19 | 20 | /// 21 | /// Attempts to acuire the distributed lock for a given key from Redis, setting the lock 22 | /// duration to , and trying for a maximum of 23 | /// to acquire the lock. 24 | /// 25 | /// 26 | /// A whose 27 | /// property will be true if the lock was granted within the specified timeframe. 28 | /// 29 | public static StackifyRedisLocker AcquireDlmLock(this IRedisClient client, string key, TimeSpan lockMaxAge, TimeSpan lockAcquisitionTimeout) 30 | { 31 | return new StackifyRedisLocker(client, key, lockMaxAge, lockAcquisitionTimeout); 32 | } 33 | 34 | } 35 | } -------------------------------------------------------------------------------- /src/RedisWithTaggingAndLockingTests/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | // General Information about an assembly is controlled through the following 6 | // set of attributes. Change these attribute values to modify the information 7 | // associated with an assembly. 8 | [assembly: AssemblyTitle("RedisWithTaggingAndLockingTests")] 9 | [assembly: AssemblyDescription("")] 10 | [assembly: AssemblyConfiguration("")] 11 | [assembly: AssemblyCompany("")] 12 | [assembly: AssemblyProduct("RedisWithTaggingAndLockingTests")] 13 | [assembly: AssemblyCopyright("Copyright © 2014")] 14 | [assembly: AssemblyTrademark("")] 15 | [assembly: AssemblyCulture("")] 16 | 17 | // Setting ComVisible to false makes the types in this assembly not visible 18 | // to COM components. If you need to access a type in this assembly from 19 | // COM, set the ComVisible attribute to true on that type. 20 | [assembly: ComVisible(false)] 21 | 22 | // The following GUID is for the ID of the typelib if this project is exposed to COM 23 | [assembly: Guid("671a1fc1-163b-4d16-9707-7d11eb192408")] 24 | 25 | // Version information for an assembly consists of the following four values: 26 | // 27 | // Major Version 28 | // Minor Version 29 | // Build Number 30 | // Revision 31 | // 32 | // You can specify all the values or you can default the Build and Revision Numbers 33 | // by using the '*' as shown below: 34 | // [assembly: AssemblyVersion("1.0.*")] 35 | [assembly: AssemblyVersion("1.0.0.0")] 36 | [assembly: AssemblyFileVersion("1.0.0.0")] 37 | -------------------------------------------------------------------------------- /src/RedisWithTaggingAndLocking.NoPostSharp.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 2013 4 | VisualStudioVersion = 12.0.31101.0 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RedisWithTaggingAndLocking", "RedisWithTaggingAndLocking\RedisWithTaggingAndLocking.csproj", "{3CE4B102-0169-4F89-955F-F1F84E714BDC}" 7 | EndProject 8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RedisWithTaggingAndLockingTests", "RedisWithTaggingAndLockingTests\RedisWithTaggingAndLockingTests.csproj", "{F2A83D26-698D-4014-881E-2E06386EC014}" 9 | EndProject 10 | Global 11 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 12 | Debug|Any CPU = Debug|Any CPU 13 | Release|Any CPU = Release|Any CPU 14 | EndGlobalSection 15 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 16 | {3CE4B102-0169-4F89-955F-F1F84E714BDC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 17 | {3CE4B102-0169-4F89-955F-F1F84E714BDC}.Debug|Any CPU.Build.0 = Debug|Any CPU 18 | {3CE4B102-0169-4F89-955F-F1F84E714BDC}.Release|Any CPU.ActiveCfg = Release|Any CPU 19 | {3CE4B102-0169-4F89-955F-F1F84E714BDC}.Release|Any CPU.Build.0 = Release|Any CPU 20 | {F2A83D26-698D-4014-881E-2E06386EC014}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 21 | {F2A83D26-698D-4014-881E-2E06386EC014}.Debug|Any CPU.Build.0 = Debug|Any CPU 22 | {F2A83D26-698D-4014-881E-2E06386EC014}.Release|Any CPU.ActiveCfg = Release|Any CPU 23 | {F2A83D26-698D-4014-881E-2E06386EC014}.Release|Any CPU.Build.0 = Release|Any CPU 24 | EndGlobalSection 25 | GlobalSection(SolutionProperties) = preSolution 26 | HideSolutionNode = FALSE 27 | EndGlobalSection 28 | EndGlobal 29 | -------------------------------------------------------------------------------- /src/RedisWithTaggingAndLocking/LockResult.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Runtime.Serialization; 5 | 6 | namespace RedisWithTaggingAndLocking 7 | { 8 | public struct LockResult 9 | { 10 | private readonly bool _acquired; 11 | private readonly TResult _result; 12 | private readonly string _key; 13 | private readonly LockHandleWrapper _handle; 14 | 15 | public LockResult(string key) 16 | { 17 | _key = key; 18 | _acquired = false; 19 | _handle = null; 20 | _result = default(TResult); 21 | } 22 | 23 | public LockResult(string key, bool acquired, LockHandleWrapper handle, TResult result) 24 | { 25 | _key = key; 26 | _acquired = acquired; 27 | _handle = handle; 28 | _result = result; 29 | } 30 | 31 | public static LockResult Fail(string key) 32 | { 33 | return new LockResult(key); 34 | } 35 | 36 | public string Key { get { return _key; } } 37 | 38 | public bool Acquired { get { return _acquired; } } 39 | 40 | public TResult Result { get { return _result; } } 41 | 42 | public LockHandleWrapper Handle { get { return _handle; } } 43 | } 44 | 45 | [DataContract] 46 | public class LockHandleWrapper 47 | { 48 | public LockHandleWrapper(string key, StackifyRedisLocker handle) 49 | { 50 | Key = key; 51 | Handle = handle; 52 | } 53 | 54 | internal T GetHandle() where T : class 55 | { 56 | return Handle as T; 57 | } 58 | 59 | 60 | [DataMember] 61 | public StackifyRedisLocker Handle { get; private set; } 62 | 63 | [DataMember] 64 | public string Key { get; private set; } 65 | } 66 | 67 | 68 | } 69 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # redis-tagging-locking-sample 2 | 3 | 4 | Sample code to use Redis as a tagging cache and Distributed Lock Manager. 5 | 6 | ## Usage 7 | 8 | Run multiple instances of the *DistributedLockingPerMethod* console app to see a simulation of Redis as a distributed lock manager. 9 | Examine the tests in *RedisWithTaggingAndLockingTests* and run them using your favorite test runner for examples of how to consume the Tagging and Locking extensions. 10 | 11 | ## Notes 12 | 13 | This project is intended to give a general idea of how tagging and DLM can be accomplished with Redis and .NET. Many specific functions (Removing cache entries by tag) have not been included in this sample, but could be easily accomplished with modified versions of the included C# code and Lua scripts. 14 | If you do not wish to install the PostSharp extension in Visual Studio, you can find a pre-compiled version of DistributedLockingPerMethod.exe in the top level "bin" folder. 15 | Be sure to update the connection info for your Redis server! 16 | 17 | ## Dependencies 18 | 19 | ServiceStack.Redis 4.0.33, NUnit 2.6.3, PostSharp 4.0.39, PostSharp Visual Studio Extension* 20 | 21 | ## License 22 | 23 | Copyright 2014 Stackify, LLC. 24 | 25 | Licensed under the Apache License, Version 2.0 (the "License"); 26 | you may not use this file except in compliance with the License. 27 | You may obtain a copy of the License at 28 | 29 | http://www.apache.org/licenses/LICENSE-2.0 30 | 31 | Unless required by applicable law or agreed to in writing, software 32 | distributed under the License is distributed on an "AS IS" BASIS, 33 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 34 | See the License for the specific language governing permissions and 35 | limitations under the License. 36 | 37 | This code extends some portions of the ServiceStack.Redis project under the terms granted in the FOSS exceptions segment of [that project's license file](https://github.com/ServiceStack/ServiceStack.Redis/blob/v4.0.33/license.txt "ServiceStack.Redis License"). 38 | -------------------------------------------------------------------------------- /src/RedisWithTaggingAndLocking.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 2013 4 | VisualStudioVersion = 12.0.31101.0 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RedisWithTaggingAndLocking", "RedisWithTaggingAndLocking\RedisWithTaggingAndLocking.csproj", "{3CE4B102-0169-4F89-955F-F1F84E714BDC}" 7 | EndProject 8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RedisWithTaggingAndLockingTests", "RedisWithTaggingAndLockingTests\RedisWithTaggingAndLockingTests.csproj", "{F2A83D26-698D-4014-881E-2E06386EC014}" 9 | EndProject 10 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DistributedLockingPerMethod", "DistributedLockingPerMethod\DistributedLockingPerMethod.csproj", "{CD111B1D-9E9A-4A22-A6A8-DA3136D2C11A}" 11 | EndProject 12 | Global 13 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 14 | Debug|Any CPU = Debug|Any CPU 15 | Release|Any CPU = Release|Any CPU 16 | EndGlobalSection 17 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 18 | {3CE4B102-0169-4F89-955F-F1F84E714BDC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 19 | {3CE4B102-0169-4F89-955F-F1F84E714BDC}.Debug|Any CPU.Build.0 = Debug|Any CPU 20 | {3CE4B102-0169-4F89-955F-F1F84E714BDC}.Release|Any CPU.ActiveCfg = Release|Any CPU 21 | {3CE4B102-0169-4F89-955F-F1F84E714BDC}.Release|Any CPU.Build.0 = Release|Any CPU 22 | {F2A83D26-698D-4014-881E-2E06386EC014}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 23 | {F2A83D26-698D-4014-881E-2E06386EC014}.Debug|Any CPU.Build.0 = Debug|Any CPU 24 | {F2A83D26-698D-4014-881E-2E06386EC014}.Release|Any CPU.ActiveCfg = Release|Any CPU 25 | {F2A83D26-698D-4014-881E-2E06386EC014}.Release|Any CPU.Build.0 = Release|Any CPU 26 | {CD111B1D-9E9A-4A22-A6A8-DA3136D2C11A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 27 | {CD111B1D-9E9A-4A22-A6A8-DA3136D2C11A}.Debug|Any CPU.Build.0 = Debug|Any CPU 28 | {CD111B1D-9E9A-4A22-A6A8-DA3136D2C11A}.Release|Any CPU.ActiveCfg = Release|Any CPU 29 | {CD111B1D-9E9A-4A22-A6A8-DA3136D2C11A}.Release|Any CPU.Build.0 = Release|Any CPU 30 | EndGlobalSection 31 | GlobalSection(SolutionProperties) = preSolution 32 | HideSolutionNode = FALSE 33 | EndGlobalSection 34 | EndGlobal 35 | -------------------------------------------------------------------------------- /src/ConsoleApp1/ConsoleApp1.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Debug 6 | AnyCPU 7 | {78904EEE-4262-470E-9E1C-5049DE8A937D} 8 | Library 9 | Properties 10 | ConsoleApp1 11 | ConsoleApp1 12 | v4.5 13 | 512 14 | 15 | 16 | true 17 | full 18 | false 19 | bin\Debug\ 20 | DEBUG;TRACE 21 | prompt 22 | 4 23 | 24 | 25 | pdbonly 26 | true 27 | bin\Release\ 28 | TRACE 29 | prompt 30 | 4 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 53 | -------------------------------------------------------------------------------- /src/.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # User-specific files 5 | *.suo 6 | *.user 7 | *.sln.docstates 8 | 9 | # Build results 10 | 11 | [Dd]ebug/ 12 | [Rr]elease/ 13 | x64/ 14 | build/ 15 | [Bb]in/ 16 | [Oo]bj/ 17 | 18 | # Enable "build/" folder in the NuGet Packages folder since NuGet packages use it for MSBuild targets 19 | 20 | # MSTest test Results 21 | [Tt]est[Rr]esult*/ 22 | [Bb]uild[Ll]og.* 23 | 24 | *_i.c 25 | *_p.c 26 | *.ilk 27 | *.meta 28 | *.obj 29 | *.pch 30 | *.pdb 31 | *.pgc 32 | *.pgd 33 | *.rsp 34 | *.sbr 35 | *.tlb 36 | *.tli 37 | *.tlh 38 | *.tmp 39 | *.tmp_proj 40 | *.log 41 | *.vspscc 42 | *.vssscc 43 | .builds 44 | *.pidb 45 | *.log.* 46 | *.scc 47 | 48 | # Visual C++ cache files 49 | ipch/ 50 | *.aps 51 | *.ncb 52 | *.opensdf 53 | *.sdf 54 | *.cachefile 55 | 56 | # Visual Studio profiler 57 | *.psess 58 | *.vsp 59 | *.vspx 60 | 61 | # Guidance Automation Toolkit 62 | *.gpState 63 | 64 | # ReSharper is a .NET coding add-in 65 | _ReSharper*/ 66 | *.[Rr]e[Ss]harper 67 | 68 | # TeamCity is a build add-in 69 | _TeamCity* 70 | 71 | # DotCover is a Code Coverage Tool 72 | *.dotCover 73 | 74 | # NCrunch 75 | *.ncrunch* 76 | .*crunch*.local.xml 77 | 78 | # Installshield output folder 79 | [Ee]xpress/ 80 | 81 | # DocProject is a documentation generator add-in 82 | DocProject/buildhelp/ 83 | DocProject/Help/*.HxT 84 | DocProject/Help/*.HxC 85 | DocProject/Help/*.hhc 86 | DocProject/Help/*.hhk 87 | DocProject/Help/*.hhp 88 | DocProject/Help/Html2 89 | DocProject/Help/html 90 | 91 | # Click-Once directory 92 | publish/ 93 | 94 | # Publish Web Output 95 | *.Publish.xml 96 | 97 | # NuGet Packages Directory 98 | ## TODO: If you have NuGet Package Restore enabled, uncomment the next line 99 | #packages/ 100 | 101 | # Windows Azure Build Output 102 | csx 103 | *.build.csdef 104 | 105 | # Windows Store app package directory 106 | AppPackages/ 107 | 108 | # Others 109 | sql/ 110 | *.Cache 111 | ClientBin/ 112 | [Ss]tyle[Cc]op.* 113 | ~$* 114 | *~ 115 | *.dbmdl 116 | *.[Pp]ublish.xml 117 | *.pfx 118 | *.publishsettings 119 | 120 | # RIA/Silverlight projects 121 | Generated_Code/ 122 | 123 | # Backup & report files from converting an old project file to a newer 124 | # Visual Studio version. Backup files are not needed, because we have git ;-) 125 | _UpgradeReport_Files/ 126 | Backup*/ 127 | UpgradeLog*.XML 128 | UpgradeLog*.htm 129 | 130 | # SQL Server files 131 | App_Data/*.mdf 132 | App_Data/*.ldf 133 | 134 | 135 | #LightSwitch generated files 136 | GeneratedArtifacts/ 137 | _Pvt_Extensions/ 138 | ModelManifest.xml 139 | 140 | # ========================= 141 | # Windows detritus 142 | # ========================= 143 | 144 | # Windows image file caches 145 | Thumbs.db 146 | ehthumbs.db 147 | 148 | # Folder config file 149 | Desktop.ini 150 | 151 | # Recycle Bin used on file shares 152 | $RECYCLE.BIN/ 153 | 154 | # Mac desktop service store files 155 | .DS_Store 156 | 157 | *.orig 158 | .sass-cache/ 159 | 160 | 161 | # Allow files 162 | !*/packages/* 163 | !/ThirdParty/* 164 | -------------------------------------------------------------------------------- /src/RedisWithTaggingAndLocking/LuaResources.Designer.cs: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | // 3 | // This code was generated by a tool. 4 | // Runtime Version:4.0.30319.34014 5 | // 6 | // Changes to this file may cause incorrect behavior and will be lost if 7 | // the code is regenerated. 8 | // 9 | //------------------------------------------------------------------------------ 10 | 11 | namespace RedisWithTaggingAndLocking { 12 | using System; 13 | 14 | 15 | /// 16 | /// A strongly-typed resource class, for looking up localized strings, etc. 17 | /// 18 | // This class was auto-generated by the StronglyTypedResourceBuilder 19 | // class via a tool like ResGen or Visual Studio. 20 | // To add or remove a member, edit your .ResX file then rerun ResGen 21 | // with the /str option, or rebuild your VS project. 22 | [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] 23 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] 24 | [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] 25 | internal class LuaResources { 26 | 27 | private static global::System.Resources.ResourceManager resourceMan; 28 | 29 | private static global::System.Globalization.CultureInfo resourceCulture; 30 | 31 | [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] 32 | internal LuaResources() { 33 | } 34 | 35 | /// 36 | /// Returns the cached ResourceManager instance used by this class. 37 | /// 38 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] 39 | internal static global::System.Resources.ResourceManager ResourceManager { 40 | get { 41 | if (object.ReferenceEquals(resourceMan, null)) { 42 | global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("RedisWithTaggingAndLocking.LuaResources", typeof(LuaResources).Assembly); 43 | resourceMan = temp; 44 | } 45 | return resourceMan; 46 | } 47 | } 48 | 49 | /// 50 | /// Overrides the current thread's CurrentUICulture property for all 51 | /// resource lookups using this strongly typed resource class. 52 | /// 53 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] 54 | internal static global::System.Globalization.CultureInfo Culture { 55 | get { 56 | return resourceCulture; 57 | } 58 | set { 59 | resourceCulture = value; 60 | } 61 | } 62 | 63 | /// 64 | /// Looks up a localized resource of type System.Byte[]. 65 | /// 66 | internal static byte[] AddWithTags { 67 | get { 68 | object obj = ResourceManager.GetObject("AddWithTags", resourceCulture); 69 | return ((byte[])(obj)); 70 | } 71 | } 72 | 73 | /// 74 | /// Looks up a localized resource of type System.Byte[]. 75 | /// 76 | internal static byte[] CleanupTags { 77 | get { 78 | object obj = ResourceManager.GetObject("CleanupTags", resourceCulture); 79 | return ((byte[])(obj)); 80 | } 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/DistributedLockingPerMethod/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using System.Threading; 4 | 5 | namespace DistributedLockingPerMethod 6 | { 7 | public class Program 8 | { 9 | 10 | // This project demonstrates Redis as a Distributed Lock Manager combined with 11 | // Aspect Oriented Programming to create a 'MethodMutex' aspect. When a method 12 | // is decorated with this aspect, it effectively becomes single "threaded" 13 | // across all running instances, even if they are in different processes or on 14 | // different planets. Additionally, code calling the decorated method receives 15 | // immediate feedback and can determine whether to retry, continue, or take 16 | // any other course of action. 17 | // 18 | // To see it in action, build the project then find the executable in your 19 | // output/bin folder and launch multiple instances. Be sure to update the 20 | // app.config file with the connection info for your Redis server! 21 | 22 | 23 | public static void Main() 24 | { 25 | WorkerInstance worker2 = new WorkerInstance(); 26 | 27 | while (true) 28 | { 29 | Console.WriteLine(worker2.DoWork()); 30 | System.Threading.Thread.Sleep(1000); 31 | } 32 | 33 | 34 | Process thisInstance = Process.GetCurrentProcess(); 35 | Process[] allInstances = Process.GetProcessesByName(thisInstance.ProcessName); 36 | Thread.Sleep(200); // helps to randomize which instance gets the mutex first 37 | 38 | while (allInstances.Length == 1) 39 | { 40 | Console.Clear(); 41 | Console.WriteLine("Redis Distributed Locking Example\r\n"); 42 | Console.WriteLine("Launch a 2nd instance of this app to start..."); 43 | Thread.Sleep(400); 44 | allInstances = Process.GetProcessesByName(thisInstance.ProcessName); 45 | } 46 | Console.Clear(); 47 | Console.WriteLine("Redis Distributed Locking Example\r\n"); 48 | Console.WriteLine("Attempting to do work on PID {0}...\r\n", thisInstance.Id); 49 | 50 | WorkerInstance worker = new WorkerInstance(); 51 | bool gotWorkDone = worker.DoWork(); 52 | if (!gotWorkDone) 53 | { 54 | Console.WriteLine("{0:T} - Method was locked, waiting...", DateTime.Now); 55 | // whether to retry or continue depends on your specific scenario 56 | // for demo, we'll retry. In cases where workers might receive 57 | // duplicate payloads near-instantaneously and only one should 58 | // succeed, you might just choose to continue. 59 | 60 | Stopwatch time = Stopwatch.StartNew(); 61 | do 62 | { 63 | Thread.Sleep(500); 64 | gotWorkDone = worker.DoWork(); 65 | Console.WriteLine("gotWorkDone: " + gotWorkDone); 66 | } while (!gotWorkDone && time.Elapsed < TimeSpan.FromMinutes(1)); 67 | time.Stop(); 68 | 69 | if (!gotWorkDone) 70 | { 71 | Console.WriteLine("Timed out after 1 minute..."); 72 | } 73 | } 74 | 75 | Console.WriteLine("Press any key to quit, 'Q' to quit all..."); 76 | var ck = Console.ReadKey(); 77 | if (ck.KeyChar == 'Q') 78 | { 79 | allInstances = Process.GetProcessesByName(thisInstance.ProcessName); 80 | foreach (var app in allInstances) 81 | { 82 | app.CloseMainWindow(); 83 | } 84 | } 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/RedisWithTaggingAndLockingTests/RedisWithTaggingAndLockingTests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Debug 6 | AnyCPU 7 | {F2A83D26-698D-4014-881E-2E06386EC014} 8 | Library 9 | Properties 10 | RedisWithTaggingAndLockingTests 11 | RedisWithTaggingAndLockingTests 12 | v4.5.1 13 | 512 14 | 15 | 16 | 17 | true 18 | full 19 | false 20 | bin\Debug\ 21 | DEBUG;TRACE 22 | prompt 23 | 4 24 | false 25 | 26 | 27 | pdbonly 28 | true 29 | bin\Release\ 30 | TRACE 31 | prompt 32 | 4 33 | false 34 | 35 | 36 | 37 | ..\packages\NUnit.2.6.3\lib\nunit.framework.dll 38 | 39 | 40 | ..\packages\ServiceStack.Common.4.0.33\lib\net40\ServiceStack.Common.dll 41 | 42 | 43 | ..\packages\ServiceStack.Interfaces.4.0.33\lib\portable-wp80+sl5+net40+win8+monotouch+monoandroid\ServiceStack.Interfaces.dll 44 | 45 | 46 | ..\packages\ServiceStack.Redis.4.0.33\lib\net40\ServiceStack.Redis.dll 47 | 48 | 49 | ..\packages\ServiceStack.Text.4.0.33\lib\net40\ServiceStack.Text.dll 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | {3ce4b102-0169-4f89-955f-f1f84e714bdc} 68 | RedisWithTaggingAndLocking 69 | 70 | 71 | 72 | 79 | -------------------------------------------------------------------------------- /src/RedisWithTaggingAndLocking/RedisWithTaggingAndLocking.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Debug 6 | AnyCPU 7 | {3CE4B102-0169-4F89-955F-F1F84E714BDC} 8 | Library 9 | Properties 10 | RedisWithTaggingAndLocking 11 | RedisWithTaggingAndLocking 12 | v4.5.1 13 | 512 14 | 15 | 16 | 17 | true 18 | full 19 | false 20 | bin\Debug\ 21 | DEBUG;TRACE 22 | prompt 23 | 4 24 | false 25 | 26 | 27 | pdbonly 28 | true 29 | bin\Release\ 30 | TRACE 31 | prompt 32 | 4 33 | false 34 | 35 | 36 | 37 | ..\packages\ServiceStack.Common.4.0.33\lib\net40\ServiceStack.Common.dll 38 | 39 | 40 | ..\packages\ServiceStack.Interfaces.4.0.33\lib\portable-wp80+sl5+net40+win8+monotouch+monoandroid\ServiceStack.Interfaces.dll 41 | 42 | 43 | ..\packages\ServiceStack.Redis.4.0.33\lib\net40\ServiceStack.Redis.dll 44 | 45 | 46 | ..\packages\ServiceStack.Text.4.0.33\lib\net40\ServiceStack.Text.dll 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | True 61 | True 62 | LuaResources.resx 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | ResXFileCodeGenerator 77 | LuaResources.Designer.cs 78 | 79 | 80 | 81 | 88 | -------------------------------------------------------------------------------- /src/DistributedLockingPerMethod/MethodMutexWithFrequency.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Configuration; 4 | using System.Diagnostics; 5 | using System.Linq; 6 | using System.Runtime.Caching; 7 | using PostSharp.Aspects; 8 | using RedisWithTaggingAndLocking; 9 | using ServiceStack.Redis; 10 | 11 | namespace DistributedLockingPerMethod 12 | { 13 | [Serializable] 14 | public class MethodMutexWithFrequency : MethodMutex 15 | { 16 | private readonly TimeSpan _maxMethodFrequencyTime; 17 | 18 | public MethodMutexWithFrequency(int maxMethodLockTimeSeconds, int maxMethodFrequencySeconds) 19 | { 20 | _maxMethodLockTime = TimeSpan.FromSeconds(maxMethodLockTimeSeconds); 21 | _maxMethodFrequencyTime = TimeSpan.FromSeconds(maxMethodFrequencySeconds); 22 | } 23 | 24 | public override void OnInvoke(MethodInterceptionArgs args) 25 | { 26 | string key = DeriveCacheKey(args); 27 | _redisConnStr = ConfigurationManager.AppSettings["redisConnStr"]; 28 | 29 | MethodTrackerDto trackerFromDistributedCache = null; 30 | LockResult lockResult = new LockResult(); 31 | 32 | try 33 | { 34 | MethodTrackerDto trackerFromLocalCache = MemoryCache.Default.Get(key) as MethodTrackerDto; 35 | 36 | //local cache says we can't run this again for a while still due to frequency limits 37 | if (trackerFromLocalCache != null && trackerFromLocalCache.LastExecuted.HasValue && 38 | trackerFromLocalCache.LastExecuted.Value.Add(_maxMethodFrequencyTime) > DateTime.UtcNow) 39 | { 40 | ReturnWithoutRunning(args); 41 | return; 42 | } 43 | 44 | using (var client = new RedisClient(_redisConnStr)) 45 | { 46 | lockResult = TryGetLock(client, key, _maxMethodLockTime, TimeSpan.FromSeconds(2)); 47 | 48 | if (!lockResult.Acquired || _maxMethodFrequencyTime == TimeSpan.Zero) 49 | { 50 | ReturnWithoutRunning(args); 51 | } 52 | else 53 | { 54 | trackerFromDistributedCache = lockResult.Result; 55 | 56 | if (trackerFromDistributedCache == null || trackerFromDistributedCache.LastExecuted == null) 57 | { 58 | trackerFromDistributedCache = new MethodTrackerDto() {LastExecuted = DateTime.UtcNow}; 59 | 60 | RunMethod(args); 61 | } 62 | //is the last time plus the interval in the future? If so we can't run it and we can cache that knowledge locally 63 | else if (trackerFromDistributedCache.LastExecuted.Value.Add(_maxMethodLockTime) > DateTime.UtcNow) 64 | { 65 | //cache local since there is a max run. We should have found this locally 66 | 67 | ReturnWithoutRunning(args); 68 | } 69 | else 70 | { 71 | trackerFromDistributedCache = new MethodTrackerDto() {LastExecuted = DateTime.UtcNow}; 72 | 73 | RunMethod(args); 74 | } 75 | 76 | //update local cache in case the method is called again we don't have to hit redis to know we can't run it again yet 77 | MemoryCache.Default.Set(key, trackerFromDistributedCache, 78 | trackerFromDistributedCache.LastExecuted.Value.Add(_maxMethodFrequencyTime)); 79 | 80 | } 81 | 82 | if (trackerFromDistributedCache == null) 83 | { 84 | trackerFromDistributedCache = new MethodTrackerDto(); 85 | } 86 | trackerFromDistributedCache.LastExecuted = DateTime.UtcNow; 87 | 88 | //set back to the cache so we track the last time it ran 89 | using (var tx = client.CreateTransaction()) 90 | { 91 | tx.QueueCommand( 92 | c => c.Set(key, trackerFromDistributedCache, DateTime.UtcNow.Add(_maxMethodFrequencyTime))); 93 | } 94 | 95 | //clear the lock 96 | if (lockResult.Handle != null) 97 | { 98 | //put to the cache the last run time 99 | lockResult.Handle.Handle.Dispose(); 100 | } 101 | } 102 | } 103 | catch (Exception ex) 104 | { 105 | Debug.WriteLine(ex.ToString()); 106 | ReturnWithoutRunning(args); 107 | } 108 | } 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/packages/PostSharp.4.0.39/tools/install.ps1: -------------------------------------------------------------------------------- 1 | param($installPath, $toolsPath, $package, $project) 2 | 3 | 4 | function PathToUri([string] $path) 5 | { 6 | return new-object Uri('file://' + $path.Replace("%","%25").Replace("#","%23").Replace("$","%24").Replace("+","%2B").Replace(",","%2C").Replace("=","%3D").Replace("@","%40").Replace("~","%7E").Replace("^","%5E")) 7 | } 8 | 9 | function UriToPath([System.Uri] $uri) 10 | { 11 | return [System.Uri]::UnescapeDataString( $uri.ToString() ).Replace([System.IO.Path]::AltDirectorySeparatorChar, [System.IO.Path]::DirectorySeparatorChar) 12 | } 13 | 14 | $targetsFile = [System.IO.Path]::Combine($toolsPath, 'PostSharp.targets') 15 | 16 | # Need to load MSBuild assembly if it's not loaded yet. 17 | Add-Type -AssemblyName 'Microsoft.Build, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a' 18 | 19 | # Grab the loaded MSBuild project for the project 20 | $msbuild = [Microsoft.Build.Evaluation.ProjectCollection]::GlobalProjectCollection.GetLoadedProjects($project.FullName) | Select-Object -First 1 21 | 22 | # Make the path to the targets file relative. 23 | $projectUri = PathToUri $project.FullName 24 | $targetUri = PathToUri $targetsFile 25 | 26 | $relativePath = UriToPath $projectUri.MakeRelativeUri($targetUri) 27 | 28 | # Remove elements from previous installations or versions. 29 | $itemsToRemove = @() 30 | $itemsToRemove += $msbuild.Xml.Properties | Where-Object {$_.Name.ToLowerInvariant() -eq "dontimportpostsharp" } 31 | # $itemsToRemove += $msbuild.Xml.Properties | Where-Object {$_.Name.ToLowerInvariant().EndsWith("postsharpignoredpackages") } # Don't remove this so that it stays during upgrades. 32 | $itemsToRemove += $msbuild.Xml.Imports | Where-Object {$_.Project.ToLowerInvariant().EndsWith("postsharp.targets") } 33 | $itemsToRemove += $msbuild.Xml.Targets | Where-Object {$_.Name.ToLowerInvariant() -eq "ensurepostsharpimported" } 34 | 35 | 36 | if ($itemsToRemove -and $itemsToRemove.length) 37 | { 38 | foreach ($itemToRemove in $itemsToRemove) 39 | { 40 | $itemToRemove.Parent.RemoveChild($itemToRemove) | out-null 41 | } 42 | } 43 | 44 | # Remove references from PostSharp 1.* and 2.*. 45 | $referencesToRemove = @() 46 | $referencesToRemove += $project.Object.References | Where-Object {$_.Identity.ToLowerInvariant().StartsWith("postsharp.public") } 47 | $referencesToRemove += $project.Object.References | Where-Object {$_.Identity.ToLowerInvariant().StartsWith("postsharp.laos") } 48 | 49 | if ($referencesToRemove -and $referencesToRemove.length) 50 | { 51 | foreach ($referenceToRemove in $referencesToRemove) 52 | { 53 | $referenceToRemove.Remove() 54 | } 55 | } 56 | 57 | 58 | 59 | # Set property DontImportPostSharp to prevent locally-installed previous versions of PostSharp to interfere. 60 | $msbuild.Xml.AddProperty( "DontImportPostSharp", "True" ) | Out-Null 61 | 62 | # Add import to PostSharp.targets 63 | $import = $msbuild.Xml.AddImport($relativePath) 64 | $import.set_Condition( "Exists('$relativePath')" ) | Out-Null 65 | [string]::Format("Added import of '{0}'.", $relativePath ) 66 | 67 | # Add a target to fail the build when our targets are not imported 68 | $target = $msbuild.Xml.AddTarget("EnsurePostSharpImported") 69 | $target.BeforeTargets = "BeforeBuild" 70 | $target.Condition = "'`$(PostSharp30Imported)' == ''" 71 | 72 | # if the targets don't exist at the time the target runs, package restore didn't run 73 | $errorTask = $target.AddTask("Error") 74 | $errorTask.Condition = "!Exists('$relativePath')" 75 | $errorTask.SetParameter("Text", "This project references NuGet package(s) that are missing on this computer. Enable NuGet Package Restore to download them. For more information, see http://www.postsharp.net/links/nuget-restore."); 76 | 77 | # if the targets exist at the time the target runs, package restore ran but the build didn't import the targets. 78 | $errorTask = $target.AddTask("Error") 79 | $errorTask.Condition = "Exists('$relativePath')" 80 | $errorTask.SetParameter("Text", "The build restored NuGet packages. Build the project again to include these packages in the build. For more information, see http://www.postsharp.net/links/nuget-restore."); 81 | 82 | # For all the assembly references installed by this package - set CopyLocal = true 83 | $pakcageRefs = $package.AssemblyReferences | %{$_.Name} 84 | foreach ($reference in $project.Object.References) 85 | { 86 | if ($pakcageRefs -contains $reference.Name + ".dll") 87 | { 88 | # To persist the CopyLocal value we have to change it from true to false first 89 | $reference.CopyLocal = $false; 90 | $reference.CopyLocal = $true; 91 | } 92 | } 93 | 94 | $project.Save() 95 | $project.Object.Refresh() 96 | 97 | # Asynchronously run setup wizard if necessary. Since the setup wizard is compressed in PostSharp-Tools.exe, the easiest is to run it through MSBuild. 98 | $msbuildExe = [System.IO.Path]::Combine( [System.Runtime.InteropServices.RuntimeEnvironment]::GetRuntimeDirectory(), "msbuild.exe") 99 | "Starting $msbuildExe" 100 | Start-Process -FilePath $msbuildExe -ArgumentList @("""$toolsPath\PostSharp.targets""", "/t:PostSharp30InstallVsx /p:BuildingInsideVisualStudio=True") -WindowStyle Hidden 101 | -------------------------------------------------------------------------------- /src/RedisWithTaggingAndLocking/CacheException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace RedisWithTaggingAndLocking 8 | { 9 | #region General Cache Exceptions 10 | 11 | public abstract class CacheException : Exception 12 | { 13 | protected CacheException(string message, string key) 14 | : base(message) 15 | { 16 | CacheKey = key; 17 | } 18 | 19 | protected CacheException(string message, string key, Exception innerException) 20 | : base(message, innerException) 21 | { 22 | CacheKey = key; 23 | } 24 | 25 | public int ErrorCode { get { return ErrorFamily + ErrorNumber; } } 26 | 27 | /// 28 | /// An error family number should remain the same across a family of exception types. 29 | /// As a recommendation, root different error familys on different bits using bitshifts. 30 | /// 31 | public abstract int ErrorFamily { get; } 32 | /// 33 | /// A unique error number should be returned for each exception class. 34 | /// Should never be zero. 35 | /// 36 | protected abstract int ErrorNumber { get; } 37 | 38 | public string CacheKey { get; protected set; } 39 | } 40 | 41 | /// 42 | /// Represents a general cache exception for which no specific details are availble. 43 | /// 44 | public sealed class GeneralCacheException : CacheException 45 | { 46 | public GeneralCacheException(string message, string key) 47 | : base(message, key) 48 | { 49 | CacheKey = key; 50 | } 51 | 52 | public GeneralCacheException(string message, string key, Exception innerException) 53 | : base(message, key, innerException) 54 | { 55 | CacheKey = key; 56 | } 57 | 58 | // different familys of cache exceptions should have error codes rooted on different bits 59 | // checking can look like (if((ErrorCode >> 16) == 1) 60 | public override int ErrorFamily 61 | { 62 | get { return 1 << 16; } 63 | } 64 | 65 | protected override int ErrorNumber 66 | { 67 | get { return 1; } 68 | } 69 | } 70 | 71 | 72 | 73 | #endregion 74 | 75 | #region Lock Exceptions 76 | 77 | public class LockException : CacheException 78 | { 79 | public LockException(string message) : this(message, null) { } 80 | 81 | public LockException(string message, string cacheKey) : base(message, cacheKey) { } 82 | 83 | internal LockException(string message, string cacheKey, StackifyRedisLocker whichlock) : base(string.Format("{0} - {1}", whichlock.ToString(), message), cacheKey) { } 84 | 85 | internal LockException(string message, string cacheKey, Exception innerException) : base(message, cacheKey, innerException) { } 86 | 87 | public sealed override int ErrorFamily 88 | { 89 | get { return 1 << 17; } 90 | } 91 | 92 | protected override int ErrorNumber 93 | { 94 | get { return 1; } 95 | } 96 | } 97 | 98 | //public class LockNotAcquiredException : LockException 99 | //{ 100 | // public LockNotAcquiredException(string message) : base(message) { } 101 | 102 | // internal LockNotAcquiredException(string message, string cacheKey, string lockString, int timeout, TimeoutException innerException) 103 | // : base(string.Format("{0} - {1}", lockString, message), cacheKey, innerException) 104 | // { 105 | // LockTimeoutSeconds = timeout; 106 | // } 107 | 108 | // public int LockTimeoutSeconds { get; private set; } 109 | 110 | // protected override int ErrorNumber 111 | // { 112 | // get { return 2; } 113 | // } 114 | //} 115 | 116 | public class LockNotFoundException : LockException 117 | { 118 | public LockNotFoundException(string message) : base(message) { } 119 | 120 | internal LockNotFoundException(string message, string cacheKey, StackifyRedisLocker whichlock) : base(message, cacheKey, whichlock) { } 121 | 122 | protected override int ErrorNumber 123 | { 124 | get { return 3; } 125 | } 126 | } 127 | 128 | public class LockCorruptedException : LockException 129 | { 130 | public LockCorruptedException(string message) : base(message) { } 131 | internal LockCorruptedException(string message, string cacheKey, StackifyRedisLocker whichlock) : base(message, cacheKey, whichlock) { } 132 | 133 | protected override int ErrorNumber 134 | { 135 | get { return 4; } 136 | } 137 | } 138 | 139 | public class LockExpiredException : LockException 140 | { 141 | public LockExpiredException(string message) : base(message) { } 142 | internal LockExpiredException(string message, string cacheKey, StackifyRedisLocker whichlock) : base(message, cacheKey, whichlock) { } 143 | 144 | protected override int ErrorNumber 145 | { 146 | get { return 5; } 147 | } 148 | } 149 | 150 | #endregion 151 | } 152 | -------------------------------------------------------------------------------- /src/DistributedLockingPerMethod/DistributedLockingPerMethod.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Debug 6 | AnyCPU 7 | {CD111B1D-9E9A-4A22-A6A8-DA3136D2C11A} 8 | Exe 9 | Properties 10 | DistributedLockingPerMethod 11 | DistributedLockingPerMethod 12 | v4.5.1 13 | 512 14 | True 15 | 16 | 17 | 18 | true 19 | full 20 | false 21 | bin\Debug\ 22 | DEBUG;TRACE 23 | prompt 24 | 4 25 | 26 | 27 | pdbonly 28 | true 29 | bin\Release\ 30 | TRACE 31 | prompt 32 | 4 33 | 34 | 35 | 36 | 37 | 38 | 39 | False 40 | ..\packages\PostSharp.4.0.39\lib\net35-client\PostSharp.dll 41 | True 42 | 43 | 44 | ..\packages\ServiceStack.Common.4.0.33\lib\net40\ServiceStack.Common.dll 45 | 46 | 47 | ..\packages\ServiceStack.Interfaces.4.0.33\lib\portable-wp80+sl5+net40+win8+monotouch+monoandroid\ServiceStack.Interfaces.dll 48 | 49 | 50 | ..\packages\ServiceStack.Redis.4.0.33\lib\net40\ServiceStack.Redis.dll 51 | 52 | 53 | ..\packages\ServiceStack.Text.4.0.33\lib\net40\ServiceStack.Text.dll 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | Designer 77 | 78 | 79 | 80 | 81 | 82 | {3ce4b102-0169-4f89-955f-f1f84e714bdc} 83 | RedisWithTaggingAndLocking 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 99 | -------------------------------------------------------------------------------- /src/DistributedLockingPerMethod/MethodMutex.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Configuration; 4 | using System.Diagnostics; 5 | using System.Reflection; 6 | using System.Text; 7 | using System.Threading.Tasks; 8 | using PostSharp.Aspects; 9 | using RedisWithTaggingAndLocking; 10 | using ServiceStack.Redis; 11 | 12 | namespace DistributedLockingPerMethod 13 | { 14 | [Serializable] 15 | public class MethodMutex : MethodInterceptionAspect 16 | { 17 | // initialized at compile time then serialized. 18 | protected TimeSpan _maxMethodLockTime; 19 | private Type _returnType; 20 | 21 | // not intialized until runtime. 22 | protected string _redisConnStr; 23 | 24 | public MethodMutex(int maxMethodLockTime = 60) 25 | { 26 | _maxMethodLockTime = TimeSpan.FromSeconds(maxMethodLockTime); 27 | } 28 | 29 | // Method executed at build time. 30 | public override void CompileTimeInitialize(MethodBase method, AspectInfo aspectInfo) 31 | { 32 | if (method == null) throw new ArgumentNullException("method"); 33 | Debug.Assert(method.DeclaringType != null, "method.DeclaringType != null"); 34 | 35 | var info = method as MethodInfo; 36 | Debug.Assert(info != null); 37 | _returnType = info.ReturnType; 38 | 39 | if (info.ReturnType != typeof(bool) && info.ReturnType != typeof(Task)) 40 | { 41 | throw new ArgumentException("MethodMutex only works on methods that return a bool. Method " + method.Name + " return type " + info.ReturnType); 42 | } 43 | } 44 | 45 | // When a decorated method is called, this logic will be run instead 46 | public override void OnInvoke(MethodInterceptionArgs args) 47 | { 48 | string key = DeriveCacheKey(args); 49 | _redisConnStr = ConfigurationManager.AppSettings["redisConnStr"]; 50 | 51 | LockResult lockResult = new LockResult(); 52 | 53 | try 54 | { 55 | using (var client = new RedisClient(_redisConnStr)) 56 | { 57 | lockResult = TryGetLock(client, key, _maxMethodLockTime, TimeSpan.FromSeconds(2)); 58 | 59 | if (!lockResult.Acquired) 60 | { 61 | ReturnWithoutRunning(args); 62 | } 63 | else 64 | { 65 | RunMethod(args); 66 | } 67 | 68 | //clear the lock 69 | if (lockResult.Handle != null) 70 | { 71 | lockResult.Handle.Handle.Dispose(); 72 | } 73 | } 74 | } 75 | catch (Exception ex) 76 | { 77 | Debug.WriteLine(ex.ToString()); 78 | ReturnWithoutRunning(args); 79 | } 80 | } 81 | 82 | 83 | protected LockResult TryGetLock(RedisClient client, string key, TimeSpan lockAgeTimeout, TimeSpan lockAcquisitionTimeout = new TimeSpan()) 84 | { 85 | 86 | var dlmLock = client.AcquireDlmLock(key, lockAcquisitionTimeout, lockAgeTimeout); 87 | 88 | if (!dlmLock.IsAcquired) 89 | { 90 | return LockResult.Fail(key); 91 | } 92 | 93 | var value = dlmLock.GetValue(client); 94 | 95 | return new LockResult(key, true, new LockHandleWrapper(key, dlmLock), value); 96 | 97 | } 98 | 99 | protected void RunMethod(MethodInterceptionArgs args) 100 | { 101 | try 102 | { 103 | args.ReturnValue = args.Invoke(args.Arguments); 104 | } 105 | catch 106 | { 107 | ReturnWithoutRunning(args); 108 | } 109 | } 110 | 111 | protected void ReturnWithoutRunning(MethodInterceptionArgs args) 112 | { 113 | if (_returnType == typeof (bool)) 114 | { 115 | args.ReturnValue = false; 116 | } 117 | else if (_returnType == typeof(Task)) 118 | { 119 | args.ReturnValue = Task.Factory.StartNew(() => false); 120 | } 121 | } 122 | 123 | 124 | protected static string DeriveCacheKey(MethodInterceptionArgs intercepted) 125 | { 126 | var method = intercepted.Method; 127 | var arguments = intercepted.Arguments; 128 | 129 | string methodName = method.DeclaringType.FullName + "." + method.Name; 130 | 131 | List argStrings = new List(arguments.Count); 132 | for (int i = 0; i < arguments.Count; i++) 133 | { 134 | object obj = arguments.GetArgument(i); 135 | 136 | if (obj == null) 137 | { 138 | argStrings.Add("null"); 139 | } 140 | else 141 | { 142 | argStrings.Add(obj.ToString()); 143 | } 144 | } 145 | 146 | var stringBuilder = new StringBuilder("Mutex-" + methodName); 147 | stringBuilder.Append('('); 148 | 149 | for (int i = 0; i < argStrings.Count; i++) 150 | { 151 | string val = argStrings[i]; 152 | if (i > 0) stringBuilder.Append(","); 153 | stringBuilder.Append(val ?? "null"); 154 | } 155 | 156 | return stringBuilder.ToString() + ")"; 157 | } 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /src/RedisWithTaggingAndLocking/RedisClientTaggingExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Globalization; 4 | using System.Text; 5 | using ServiceStack; 6 | using ServiceStack.Redis; 7 | using ServiceStack.Text; 8 | 9 | namespace RedisWithTaggingAndLocking 10 | { 11 | public static class RedisClientTaggingExtensions 12 | { 13 | private readonly static Random Rng = new Random(); 14 | 15 | /// 16 | /// Sets a key/value pair, and marks the key with one or more specified tags. 17 | /// 18 | public static int SetWithTags(this IRedisClient client, string key, T value, IList tags, TimeSpan? expiresIn = null) 19 | { 20 | if(client == null) throw new ArgumentNullException("client"); 21 | if(key == null) throw new ArgumentNullException("key"); 22 | if(tags == null) throw new ArgumentNullException("tags"); 23 | if(value == null) throw new ArgumentNullException("value", "Use Remove(key) intead."); 24 | if (tags.Count == 0) 25 | { 26 | if (expiresIn.HasValue) 27 | client.Set(key, value, expiresIn.Value); 28 | else 29 | client.Set(key, value); 30 | 31 | return 0; 32 | } 33 | 34 | string[] keys = new string[tags.Count + 1]; 35 | keys[0] = key; 36 | tags.CopyTo(keys, 1); 37 | 38 | string serializedValue = SerializeValue(value); 39 | var args = new List {serializedValue}; 40 | if (expiresIn.HasValue) 41 | { 42 | int seconds = (int) expiresIn.Value.TotalSeconds; 43 | args.Add(seconds.ToString(CultureInfo.InvariantCulture)); 44 | } 45 | 46 | CleanupByProbability(client); 47 | 48 | // NOTE: Scripts should be executed by their SHA1 hash rather than by sending 49 | // the full text across the wire each time. Left as an excercise for the reader. 50 | int numberOfTagsAdded = (int) client.ExecLuaAsInt(Script(LuaResources.AddWithTags), keys, args.ToArray()); 51 | return numberOfTagsAdded; 52 | } 53 | 54 | public static void CleanupTags(this IRedisClient client) 55 | { 56 | if (client == null) throw new ArgumentNullException("client"); 57 | client.ExecLuaAsInt(Script(LuaResources.CleanupTags)); 58 | } 59 | 60 | /// 61 | /// Gets a list of all keys which are associated with any of the supplied tags. 62 | /// 63 | public static HashSet GetKeysByAnyTag(this IRedisClient client, params string[] tags) 64 | { 65 | if (client == null) throw new ArgumentNullException("client"); 66 | 67 | if (tags == null || tags.Length == 0) 68 | return new HashSet(); 69 | 70 | CleanupByProbability(client); 71 | 72 | HashSet taggedKeys = (tags.Length == 1) 73 | ? client.GetAllItemsFromSet(tags[0]) 74 | : client.GetUnionFromSets(tags); 75 | 76 | // NOTE: depending on your specific use case, you may want to 77 | // check that the returned keys are still valid by checking 78 | // EXISTS or TTL on each one, or you may want to call the tag 79 | // cleanup script prior to retrieving the keys in the first 80 | // place. 81 | 82 | return taggedKeys; 83 | } 84 | 85 | /// 86 | /// Gets a list of those keys which are associated with ALL of the supplied tags. 87 | /// 88 | public static HashSet GetKeysByAllTags(this IRedisClient client, params string[] tags) 89 | { 90 | if (client == null) throw new ArgumentNullException("client"); 91 | 92 | if (tags == null || tags.Length == 0) 93 | return new HashSet(); 94 | 95 | CleanupByProbability(client); 96 | 97 | HashSet taggedKeys = (tags.Length == 1) 98 | ? client.GetAllItemsFromSet(tags[0]) 99 | : client.GetIntersectFromSets(tags); 100 | 101 | // NOTE: depending on your specific use case, you may want to 102 | // check that the returned keys are still valid by checking 103 | // EXISTS or TTL on each one, or you may want to call the tag 104 | // cleanup script prior to retrieving the keys in the first 105 | // place. 106 | 107 | return taggedKeys; 108 | } 109 | 110 | /// 111 | /// Basic stub for cleaning up tags on a specific interval. 112 | /// 113 | private static void CleanupByProbability(IRedisClient client) 114 | { 115 | // NOTE: better probability and guarantees about min/max intervals left as excercise for reader. 116 | var p = Rng.NextDouble(); 117 | if (p > 0.05d) return; 118 | client.CleanupTags(); 119 | } 120 | 121 | /// 122 | /// Given the binary encoded script (as from a resource file), decodes it 123 | /// back to a string for use with ServiceStack's library methods. 124 | /// 125 | private static string Script(byte[] binaryScript) 126 | { 127 | // trim UTF8 BOM in case they exist. 128 | return binaryScript.FromUtf8Bytes().Trim(new[] { '\uFEFF', '\u200B' }); 129 | } 130 | 131 | /// 132 | /// Pre-serializes an object in a way that is compatible with ServiceStack.Redis' deserialization. 133 | /// For use when you need to pass a value to a lua script that may later be retrieved by other methods on a RedisClient. 134 | /// 135 | private static string SerializeValue(T value) 136 | { 137 | byte[] s = (value as byte[]); 138 | if (s != null) 139 | { 140 | return Encoding.UTF8.GetString(s); // probably won't happen, but for completeness, this should make it transparent to servicestack 141 | } 142 | 143 | string jsonified = JsonSerializer.SerializeToString(value); 144 | return jsonified; 145 | } 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /src/RedisWithTaggingAndLocking/LuaResources.resx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | text/microsoft-resx 110 | 111 | 112 | 2.0 113 | 114 | 115 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 116 | 117 | 118 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 119 | 120 | 121 | 122 | scripts\addwithtags.lua;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 123 | 124 | 125 | scripts\cleanuptags.lua;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 126 | 127 | -------------------------------------------------------------------------------- /src/packages/PostSharp.4.0.39/tools/PostSharp.tasks: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | <_PostSharp30ImportedTwice Condition="'$(PostSharp30Imported)'=='True'">True 19 | True 20 | 21 | 22 | $(MSBuildProjectDirectory)\$(MSBuildProjectName).$(Configuration).$(Platform).psproj 23 | $(MSBuildProjectDirectory)\$(MSBuildProjectName).$(Configuration).psproj 24 | $(MSBuildProjectDirectory)\$(MSBuildProjectName).psproj 25 | 26 | 27 | 28 | 29 | Release 30 | False 31 | False 32 | $(IntermediateOutputPath)PostSharp 33 | $(IntermediateOutputPath)Before-PostSharp 34 | False 35 | $(SignAssembly) 36 | False 37 | False 38 | True 39 | $(OutDir)$(AssemblyName).pssym 40 | False 41 | 4.0 42 | x64 43 | x86 44 | $(PostSharpTargetFrameworkVersion)-$(PostSharpTargetProcessor) 45 | OptimizeForBuildTime 46 | OptimizeForSize 47 | True 48 | Native 49 | PipeServer 50 | True 51 | True 52 | False 53 | False 54 | False 55 | 56 | 57 | 58 | <_PostSharp30BinDirectoryName>bin.$(PostSharpBuild) 59 | <_PostSharp30MSBuildAssemblyName>PostSharp.MSBuild.v$(PostSharp30Version).$(PostSharpBuild).dll 60 | <_PostSharp30MSBuildAssemblyInternalPath>$(MSBuildThisFileDirectory)\$(_PostSharp30BinDirectoryName)\$(_PostSharp30MSBuildAssemblyName) 61 | 62 | 63 | 64 | 65 | $(MSBuildThisFileDirectory) 66 | $(_PostSharp30MSBuildAssemblyInternalPath) 67 | 68 | 69 | 70 | 71 | $(PROGRAMDATA) 72 | 73 | $(LOCALAPPDATA) 74 | 75 | $(TEMP) 76 | <_PostSharpExtractDirectory>$(PostSharpExtractDirectory)\PostSharp\$(PostSharp30Version) 77 | $(_PostSharpExtractDirectory) 78 | 79 | 80 | 81 | 82 | $(PostSharp30ToolDirectory)\$(_PostSharp30BinDirectoryName)\$(_PostSharp30MSBuildAssemblyName) 83 | 84 | $(PostSharp30ToolDirectory)\$(_PostSharp30BinDirectoryName) 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | -------------------------------------------------------------------------------- /bin/DistributedLockingPerMethod/ServiceStack.Common.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ServiceStack.Common 5 | 6 | 7 | 8 | 9 | Useful .NET Encryption Utils from: 10 | http://andrewlocatelliwoodcock.com/2011/08/01/implementing-rsa-asymmetric-public-private-key-encryption-in-c-encrypting-under-the-public-key/ 11 | 12 | 13 | 14 | 15 | Encrypt an arbitrary string of data under the supplied public key 16 | 17 | The public key to encrypt under 18 | The data to encrypt 19 | The bit length or strength of the public key: 1024, 2048 or 4096 bits. This must match the 20 | value actually used to create the publicKey 21 | 22 | 23 | 24 | 25 | Create Public and Private Key Pair based on settings already in static class. 26 | 27 | RsaKeyPair 28 | 29 | 30 | 31 | Return T[0] when enumerable is null, safe to use in enumerations like foreach 32 | 33 | 34 | 35 | 36 | Gets the textual description of the enum if it has one. e.g. 37 | 38 | 39 | enum UserColors 40 | { 41 | [Description("Bright Red")] 42 | BrightRed 43 | } 44 | UserColors.BrightRed.ToDescription(); 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | Creates a Console Logger, that logs all messages to: System.Console 53 | 54 | Made public so its testable 55 | 56 | 57 | 58 | 59 | Default logger is to Console.WriteLine 60 | 61 | Made public so its testable 62 | 63 | 64 | 65 | 66 | Initializes a new instance of the class. 67 | 68 | 69 | 70 | 71 | Initializes a new instance of the class. 72 | 73 | 74 | 75 | 76 | Logs the specified message. 77 | 78 | 79 | 80 | 81 | Logs the format. 82 | 83 | 84 | 85 | 86 | Logs the specified message. 87 | 88 | 89 | 90 | 91 | Useful IPAddressExtensions from: 92 | http://blogs.msdn.com/knom/archive/2008/12/31/ip-address-calculations-with-c-subnetmasks-networks.aspx 93 | 94 | 95 | 96 | 97 | 98 | Gets the ipv4 addresses from all Network Interfaces that have Subnet masks. 99 | 100 | 101 | 102 | 103 | 104 | Gets the ipv6 addresses from all Network Interfaces. 105 | 106 | 107 | 108 | 109 | 110 | Func to get the Strongly-typed field 111 | 112 | 113 | 114 | 115 | Required to cast the return ValueType to an object for caching 116 | 117 | 118 | 119 | 120 | Func to set the Strongly-typed field 121 | 122 | 123 | 124 | 125 | Required to cast the ValueType to an object for caching 126 | 127 | 128 | 129 | 130 | Required to cast the ValueType to an object for caching 131 | 132 | 133 | 134 | 135 | Func to get the Strongly-typed field 136 | 137 | 138 | 139 | 140 | Required to cast the return ValueType to an object for caching 141 | 142 | 143 | 144 | 145 | Func to set the Strongly-typed field 146 | 147 | 148 | 149 | 150 | Required to cast the ValueType to an object for caching 151 | 152 | 153 | 154 | 155 | Required to cast the ValueType to an object for caching 156 | 157 | 158 | 159 | 160 | Common functionality when creating adapters 161 | 162 | 163 | 164 | 165 | Executes the specified expression. 166 | 167 | 168 | The action. 169 | 170 | 171 | 172 | 173 | Executes the specified action (for void methods). 174 | 175 | The action. 176 | 177 | 178 | 179 | Note: InMemoryLog keeps all logs in memory, so don't use it long running exceptions 180 | 181 | Returns a thread-safe InMemoryLog which you can use while *TESTING* 182 | to provide a detailed analysis of your logs. 183 | 184 | 185 | 186 | 187 | Creates a Unified Resource Name (URN) with the following formats: 188 | 189 | - urn:{TypeName}:{IdFieldValue} e.g. urn:UserSession:1 190 | - urn:{TypeName}:{IdFieldName}:{IdFieldValue} e.g. urn:UserSession:UserId:1 191 | 192 | 193 | 194 | 195 | 196 | 197 | Provide the an option for the callee to block until all commands are executed 198 | 199 | 200 | 201 | 202 | 203 | 204 | Invokes the action provided and returns true if no excpetion was thrown. 205 | Otherwise logs the exception and returns false if an exception was thrown. 206 | 207 | The action. 208 | 209 | 210 | 211 | 212 | Runs an action for a minimum of runForMs 213 | 214 | What to run 215 | Minimum ms to run for 216 | time elapsed in micro seconds 217 | 218 | 219 | 220 | Returns average microseconds an action takes when run for the specified runForMs 221 | 222 | What to run 223 | How many times to run for each iteration 224 | Minimum ms to run for 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | -------------------------------------------------------------------------------- /src/packages/ServiceStack.Common.4.0.33/lib/net40/ServiceStack.Common.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ServiceStack.Common 5 | 6 | 7 | 8 | 9 | Useful .NET Encryption Utils from: 10 | http://andrewlocatelliwoodcock.com/2011/08/01/implementing-rsa-asymmetric-public-private-key-encryption-in-c-encrypting-under-the-public-key/ 11 | 12 | 13 | 14 | 15 | Encrypt an arbitrary string of data under the supplied public key 16 | 17 | The public key to encrypt under 18 | The data to encrypt 19 | The bit length or strength of the public key: 1024, 2048 or 4096 bits. This must match the 20 | value actually used to create the publicKey 21 | 22 | 23 | 24 | 25 | Create Public and Private Key Pair based on settings already in static class. 26 | 27 | RsaKeyPair 28 | 29 | 30 | 31 | Return T[0] when enumerable is null, safe to use in enumerations like foreach 32 | 33 | 34 | 35 | 36 | Gets the textual description of the enum if it has one. e.g. 37 | 38 | 39 | enum UserColors 40 | { 41 | [Description("Bright Red")] 42 | BrightRed 43 | } 44 | UserColors.BrightRed.ToDescription(); 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | Creates a Console Logger, that logs all messages to: System.Console 53 | 54 | Made public so its testable 55 | 56 | 57 | 58 | 59 | Default logger is to Console.WriteLine 60 | 61 | Made public so its testable 62 | 63 | 64 | 65 | 66 | Initializes a new instance of the class. 67 | 68 | 69 | 70 | 71 | Initializes a new instance of the class. 72 | 73 | 74 | 75 | 76 | Logs the specified message. 77 | 78 | 79 | 80 | 81 | Logs the format. 82 | 83 | 84 | 85 | 86 | Logs the specified message. 87 | 88 | 89 | 90 | 91 | Useful IPAddressExtensions from: 92 | http://blogs.msdn.com/knom/archive/2008/12/31/ip-address-calculations-with-c-subnetmasks-networks.aspx 93 | 94 | 95 | 96 | 97 | 98 | Gets the ipv4 addresses from all Network Interfaces that have Subnet masks. 99 | 100 | 101 | 102 | 103 | 104 | Gets the ipv6 addresses from all Network Interfaces. 105 | 106 | 107 | 108 | 109 | 110 | Func to get the Strongly-typed field 111 | 112 | 113 | 114 | 115 | Required to cast the return ValueType to an object for caching 116 | 117 | 118 | 119 | 120 | Func to set the Strongly-typed field 121 | 122 | 123 | 124 | 125 | Required to cast the ValueType to an object for caching 126 | 127 | 128 | 129 | 130 | Required to cast the ValueType to an object for caching 131 | 132 | 133 | 134 | 135 | Func to get the Strongly-typed field 136 | 137 | 138 | 139 | 140 | Required to cast the return ValueType to an object for caching 141 | 142 | 143 | 144 | 145 | Func to set the Strongly-typed field 146 | 147 | 148 | 149 | 150 | Required to cast the ValueType to an object for caching 151 | 152 | 153 | 154 | 155 | Required to cast the ValueType to an object for caching 156 | 157 | 158 | 159 | 160 | Common functionality when creating adapters 161 | 162 | 163 | 164 | 165 | Executes the specified expression. 166 | 167 | 168 | The action. 169 | 170 | 171 | 172 | 173 | Executes the specified action (for void methods). 174 | 175 | The action. 176 | 177 | 178 | 179 | Note: InMemoryLog keeps all logs in memory, so don't use it long running exceptions 180 | 181 | Returns a thread-safe InMemoryLog which you can use while *TESTING* 182 | to provide a detailed analysis of your logs. 183 | 184 | 185 | 186 | 187 | Creates a Unified Resource Name (URN) with the following formats: 188 | 189 | - urn:{TypeName}:{IdFieldValue} e.g. urn:UserSession:1 190 | - urn:{TypeName}:{IdFieldName}:{IdFieldValue} e.g. urn:UserSession:UserId:1 191 | 192 | 193 | 194 | 195 | 196 | 197 | Provide the an option for the callee to block until all commands are executed 198 | 199 | 200 | 201 | 202 | 203 | 204 | Invokes the action provided and returns true if no excpetion was thrown. 205 | Otherwise logs the exception and returns false if an exception was thrown. 206 | 207 | The action. 208 | 209 | 210 | 211 | 212 | Runs an action for a minimum of runForMs 213 | 214 | What to run 215 | Minimum ms to run for 216 | time elapsed in micro seconds 217 | 218 | 219 | 220 | Returns average microseconds an action takes when run for the specified runForMs 221 | 222 | What to run 223 | How many times to run for each iteration 224 | Minimum ms to run for 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | -------------------------------------------------------------------------------- /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, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | 203 | -------------------------------------------------------------------------------- /src/RedisWithTaggingAndLockingTests/RedisTestsAndExamples.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Reflection.Emit; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | using RedisWithTaggingAndLocking; 7 | using NUnit.Framework; 8 | using ServiceStack.Redis; 9 | 10 | namespace RedisWithTaggingAndLockingTests 11 | { 12 | [TestFixture] 13 | public class RedisTestsAndExamples 14 | { 15 | private static readonly PooledRedisClientManager TestsClientManager = new PooledRedisClientManager("localhost"); 16 | 17 | #region Tagging Tests 18 | 19 | [Test] 20 | public void SetWithTags() 21 | { 22 | string cacheKeyToSet = "tests:SetWithTags:somecachekey"; 23 | string[] tags = 24 | { 25 | "tests:tag:SetWithTags:firsttag", 26 | "tests:tag:SetWithTags:secondtag" 27 | }; 28 | 29 | string cacheValueToSet = "some really interesting value"; 30 | 31 | Dictionary> tagToKeysMap = new Dictionary>(); 32 | int numberOfTagsSet; 33 | using (var cache = TestsClientManager.GetClient()) 34 | { 35 | // Set tags using extensions 36 | numberOfTagsSet = cache.SetWithTags(cacheKeyToSet, cacheValueToSet, tags, TimeSpan.FromSeconds(5)); 37 | // returns tags.Length unless some of the tags are already associated with this key. 38 | } 39 | Assert.AreEqual(tags.Length, numberOfTagsSet, "The SetWithTags() extension method should have returned a value equal to the number of tags we requested to be set."); 40 | 41 | 42 | // Retrieve tag contents _without_ using extensions, as an example 43 | string[] keysMarkedWithTag = {cacheKeyToSet}; 44 | using (var cache = TestsClientManager.GetClient()) 45 | { 46 | foreach (var tag in tags) 47 | { 48 | var tagContents = cache.GetAllItemsFromSet(tag); 49 | CollectionAssert.AreEqual(keysMarkedWithTag, tagContents, "The tag '{0}' be associated with exactly one cache key, '{1}'.", tag, cacheKeyToSet); 50 | } 51 | } 52 | 53 | Console.WriteLine("Set and read successfully associated tags ({0}) to cache key '{1}'.", String.Join(",", tags), cacheKeyToSet); 54 | } 55 | 56 | [Test] 57 | public void GetValuesByTag() 58 | { 59 | // NOTE: this is a two-step retrieval, so be careful & aware of the lack of atomicity. 60 | // This could be made atomic with a lua script and taking advantage of one of redis-lua's packing/serializing strategies. 61 | 62 | string key1 = "tests:GetValuesByTag:keyone"; 63 | string key2 = "tests:GetValuesByTag:keytwo"; 64 | string key3 = "tests:GetValuesByTag:keythree"; 65 | 66 | string tag = "tests:tag:GetValuesByTag:thetag"; 67 | 68 | string value1 = "Red"; 69 | string value2 = "Green"; 70 | string value3 = "Blue"; 71 | 72 | using (var cache = TestsClientManager.GetClient()) 73 | { 74 | cache.SetWithTags(key1, value1, new[] { tag }, TimeSpan.FromSeconds(5)); 75 | cache.SetWithTags(key2, value2, new[] { tag }, TimeSpan.FromSeconds(1)); // note 76 | cache.SetWithTags(key3, value3, new[] { tag }, TimeSpan.FromSeconds(5)); 77 | } 78 | 79 | HashSet taggedKeys; 80 | // retrieve list of keys 81 | using (var cache = TestsClientManager.GetClient()) 82 | { 83 | taggedKeys = cache.GetKeysByAnyTag(tag); 84 | } 85 | CollectionAssert.AreEquivalent(new[] {key1, key2, key3}, taggedKeys, "All 3 keys should be retrieved by the tag."); 86 | 87 | Thread.Sleep(1000); // key2 will expire 88 | IDictionary cacheValues; 89 | // retrieve the values using the list of keys retrieved by GetKeysByAnyTag() 90 | using (var cache = TestsClientManager.GetClient()) 91 | { 92 | cacheValues = cache.GetAll(taggedKeys); 93 | } 94 | 95 | CollectionAssert.AreEquivalent(new[] { key1, key2, key3 }, cacheValues.Keys, "All of the keys that you supplied to GetAll() will be in the returned dictionary, even if they've expired or never existed!"); 96 | 97 | Assert.AreEqual(cacheValues[key1], value1, "The value retrieved for key1 should match value1."); 98 | Assert.AreEqual(cacheValues[key2], null, "The value stored at key2 should have expired."); 99 | Assert.AreEqual(cacheValues[key3], value3, "The value retrieved for key3 should match value3."); 100 | 101 | Console.WriteLine("All steps in the GetValuesByTag example have completed without error."); 102 | } 103 | 104 | [Test] 105 | public void GetKeysByAnyTag() 106 | { 107 | string tag1 = "tests:tag:GetKeysByAnyTag:tagone"; 108 | string tag2 = "tests:tag:GetKeysByAnyTag:tagtwo"; 109 | string tag3 = "tests:tag:GetKeysByAnyTag:tagthree"; 110 | 111 | string key1 = "tests:GetKeysByAnyTag:keyone"; 112 | string key2 = "tests:GetKeysByAnyTag:keytwo"; 113 | string key3 = "tests:GetKeysByAnyTag:keythree"; 114 | string key4 = "tests:GetKeysByAnyTag:keyfour"; 115 | 116 | string cacheValue = "something intersting {0}"; 117 | 118 | string[] tagsForKey1 = { tag1 }; 119 | string[] tagsForKey2 = { tag1, tag3 }; 120 | string[] tagsForKey3 = { tag1, tag2 }; 121 | string[] tagsForKey4 = { tag1, tag2, tag3 }; 122 | 123 | using (var cache = TestsClientManager.GetClient()) 124 | { 125 | cache.SetWithTags(key1, String.Format(cacheValue, 1), tagsForKey1, TimeSpan.FromSeconds(5)); 126 | cache.SetWithTags(key2, String.Format(cacheValue, 2), tagsForKey2, TimeSpan.FromSeconds(5)); 127 | cache.SetWithTags(key3, String.Format(cacheValue, 3), tagsForKey3, TimeSpan.FromSeconds(5)); 128 | cache.SetWithTags(key4, String.Format(cacheValue, 4), tagsForKey4, TimeSpan.FromSeconds(5)); 129 | } 130 | 131 | HashSet tag1Contents, tag2Contents, tag3Contents; 132 | 133 | // retrieve individual tags 134 | using (var cache = TestsClientManager.GetClient()) 135 | { 136 | tag1Contents = cache.GetKeysByAnyTag(new[] { tag1 }); 137 | tag2Contents = cache.GetKeysByAnyTag(new[] { tag2 }); 138 | tag3Contents = cache.GetKeysByAnyTag(new[] { tag3 }); 139 | } 140 | 141 | CollectionAssert.AreEquivalent(new[] {key1, key2, key3, key4}, tag1Contents, "Tag1 should have been associated with all 4 keys."); 142 | CollectionAssert.AreEquivalent(new[] { key3, key4 }, tag2Contents, "Tag2 should have been associated only with keys 3 and 4."); 143 | CollectionAssert.AreEquivalent(new[] { key2, key4 }, tag3Contents, "Tag3 should have been associated only with keys 2 and 4."); 144 | 145 | 146 | HashSet tag23Contents, tag13Contents; 147 | // now retrieve combinations of tags 148 | using (var cache = TestsClientManager.GetClient()) 149 | { 150 | tag23Contents = cache.GetKeysByAnyTag(new[] { tag2, tag3 }); 151 | tag13Contents = cache.GetKeysByAnyTag(new[] { tag1, tag3 }); // ensure that overlapping keys are returned exactly once 152 | } 153 | 154 | CollectionAssert.AreEquivalent(new[] { key2, key3, key4 }, tag23Contents, "Tag2 and Tag3 should, combined, be associated keys 2, 3, and 4."); 155 | CollectionAssert.AreEquivalent(new[] { key1, key2, key3, key4 }, tag13Contents, "Tag1 and Tag3 should, combined, have been associated with all 4 keys."); 156 | 157 | Console.WriteLine("All scenarios for GetKeysByAnyTag passed."); 158 | 159 | } 160 | 161 | [Test] 162 | public void GetKeysByAllTags() 163 | { 164 | string tag1 = "tests:tag:GetKeysByAllTags:tagone"; 165 | string tag2 = "tests:tag:GetKeysByAllTags:tagtwo"; 166 | string tag3 = "tests:tag:GetKeysByAllTags:tagthree"; 167 | string tag4 = "tests:tag:GetKeysByAllTags:tagfour"; 168 | 169 | string key1 = "tests:GetKeysByAllTags:keyone"; 170 | string key2 = "tests:GetKeysByAllTags:keytwo"; 171 | string key3 = "tests:GetKeysByAllTags:keythree"; 172 | string key4 = "tests:GetKeysByAllTags:keyfour"; 173 | string key5 = "tests:GetKeysByAllTags:keyfive"; 174 | 175 | string cacheValue = "something intersting {0}"; 176 | 177 | string[] tagsForKey1 = { tag1, tag4 }; 178 | string[] tagsForKey2 = { tag1, tag3 }; 179 | string[] tagsForKey3 = { tag1, tag2 }; 180 | string[] tagsForKey4 = { tag2, tag3 }; 181 | string[] tagsForKey5 = { tag1, tag2, tag3 }; 182 | 183 | using (var cache = TestsClientManager.GetClient()) 184 | { 185 | cache.SetWithTags(key1, String.Format(cacheValue, 1), tagsForKey1, TimeSpan.FromSeconds(5)); 186 | cache.SetWithTags(key2, String.Format(cacheValue, 2), tagsForKey2, TimeSpan.FromSeconds(5)); 187 | cache.SetWithTags(key3, String.Format(cacheValue, 3), tagsForKey3, TimeSpan.FromSeconds(5)); 188 | cache.SetWithTags(key4, String.Format(cacheValue, 4), tagsForKey4, TimeSpan.FromSeconds(5)); 189 | cache.SetWithTags(key5, String.Format(cacheValue, 5), tagsForKey5, TimeSpan.FromSeconds(5)); 190 | } 191 | 192 | 193 | HashSet tag12Contents, tag23Contents, tag13Contents, tag123Contents, tag34Contents; 194 | // retrieve combinations of tags 195 | using (var cache = TestsClientManager.GetClient()) 196 | { 197 | tag12Contents = cache.GetKeysByAllTags(new[] { tag1, tag2 }); 198 | tag13Contents = cache.GetKeysByAllTags(new[] { tag1, tag3 }); 199 | tag23Contents = cache.GetKeysByAllTags(new[] { tag2, tag3 }); 200 | tag123Contents = cache.GetKeysByAllTags(new[] { tag1, tag2, tag3 }); 201 | tag34Contents = cache.GetKeysByAllTags(new[] { tag3, tag4 }); 202 | } 203 | 204 | CollectionAssert.AreEquivalent(new[] { key3, key5 }, tag12Contents, "The only keys tagged with both tag 1 and 2 should have been key 3 and 5."); 205 | CollectionAssert.AreEquivalent(new[] { key2, key5 }, tag13Contents, "The only keys tagged with both tag 1 and 3 should have been key 2 and 5."); 206 | CollectionAssert.AreEquivalent(new[] { key4, key5 }, tag23Contents, "The only keys tagged with both tag 2 and 3 should have been key 4 and 5."); 207 | CollectionAssert.AreEquivalent(new[] { key5 }, tag123Contents, "The only key tagged with both tags 1, 2, and 3 should have been key 5."); 208 | CollectionAssert.AreEquivalent(new String[0], tag34Contents, "No keys should have been tagged with tags 3 AND 4."); 209 | 210 | Console.WriteLine("All scenarios for GetKeysByAllTags passed."); 211 | } 212 | 213 | #endregion 214 | 215 | 216 | 217 | 218 | #region Locking Tests 219 | 220 | [TestCase(2)] // test with 2 workers 221 | [TestCase(10)] // test with 10 workers 222 | [TestCase(50)] // test with 50 workers 223 | public void LockSimulation(int numberOfWorkers) 224 | { 225 | string lockName = "lock" + Guid.NewGuid().GetHashCode(); 226 | string counterName = lockName + "ExecutionCount"; 227 | 228 | using (var client = TestsClientManager.GetClient()) 229 | { 230 | client.Remove(counterName); 231 | client.Increment(counterName, 0); 232 | } 233 | 234 | Action simulatedDistributedClientCode = (workerId) => 235 | { 236 | try 237 | { 238 | 239 | using (var redisClient = TestsClientManager.GetClient()) 240 | { 241 | try 242 | { 243 | using ( 244 | var mylock = redisClient.AcquireDlmLock(lockName, TimeSpan.FromTicks(1), 245 | TimeSpan.FromSeconds(2))) 246 | // lock must be acquired immediately, and is valid for 2 sec. 247 | { 248 | Console.WriteLine("Worker '{0}' was able to acquire lock '{1}'.", workerId, lockName); 249 | Thread.Sleep(1800); // sleep for most of the lock validity period 250 | redisClient.Increment(counterName, 1); 251 | } 252 | } 253 | catch (TimeoutException) 254 | { 255 | Console.WriteLine("Worker '{0}' was NOT able to acquire lock '{1}'.", workerId, lockName); 256 | } 257 | } 258 | } 259 | catch (Exception ex) 260 | { 261 | Assert.Fail(ex.Message); 262 | } 263 | }; 264 | 265 | Parallel.For(0, numberOfWorkers, i => simulatedDistributedClientCode("worker#" + i)); 266 | 267 | long totalExecutions; 268 | using (var cache = TestsClientManager.GetClient()) 269 | { 270 | totalExecutions = cache.Get(counterName); 271 | } 272 | 273 | Assert.AreEqual(1, totalExecutions, "With a lock acquisition timeout of zero, and all code started within 1800ms, only one worker should have ever acquired the lock."); 274 | 275 | Console.WriteLine("All assertions passed and only one worker received the lock, as expected."); 276 | } 277 | 278 | #endregion 279 | 280 | } 281 | } 282 | -------------------------------------------------------------------------------- /src/RedisWithTaggingAndLocking/StackifyRedisLocker.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Globalization; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading; 7 | using System.Threading.Tasks; 8 | using ServiceStack.Redis; 9 | using ServiceStack.Text; 10 | 11 | namespace RedisWithTaggingAndLocking 12 | { 13 | public class StackifyRedisLocker : IDisposable 14 | { 15 | // based on ServiceStack.Redis.RedisLock 16 | // https://github.com/ServiceStack/ServiceStack.Redis/blob/master/src/ServiceStack.Redis/RedisLock.cs 17 | 18 | #region Static & Const Members 19 | 20 | public static readonly TimeSpan DefaultLockAcquisitionTimeout = TimeSpan.FromSeconds(30); 21 | public static readonly TimeSpan DefaultLockMaxAge = TimeSpan.FromHours(2); 22 | 23 | // namespace isolating the storage of lock metadata; should always have a value. 24 | private const string LockPrefix = "syscachelock:"; 25 | 26 | // populate ValuePrefix to privately isolate your locked values in a namespace 27 | // so that they aren't mistakenly (or intentionally) accessed directly by callers 28 | // without going through the locking protections. 29 | private const string ValuePrefix = ""; 30 | 31 | #endregion 32 | 33 | #region Private Fields 34 | 35 | private readonly IRedisClient _client; 36 | // BUG?: don't use the _client member except in Dispose, and only within try/catch 37 | 38 | private readonly string _rawKey; 39 | private readonly string _valueKey; 40 | private readonly string _lockKey; 41 | private string _lockValue; 42 | private int _disposed; 43 | private readonly object _releaseLockObj = new object(); 44 | private readonly bool _acquired; 45 | 46 | #endregion 47 | 48 | // TODO: refactor to TryGet factory pattern & take acquisition logic out of constructor 49 | #region Ctor 50 | 51 | /// 52 | /// Acquires a lock on the specified key. 53 | /// 54 | /// The client to use to acquire the lock. 55 | /// The key to acquire the lock on. 56 | /// After this amount of time expires, the lock will be invalidated and other clients will be allowed to establish a new lock on the same key. Deafults to . 57 | /// The amount of time to wait while trying to acquire the lock. Defaults to . 58 | public StackifyRedisLocker(IRedisClient redisClient, string key, TimeSpan? lockMaxAge = null, 59 | TimeSpan? acquisitionTimeOut = null) 60 | { 61 | _client = redisClient; 62 | _rawKey = key; 63 | _valueKey = ValuePrefix + key; 64 | _lockKey = LockPrefix + key; 65 | var lockSpinTime = acquisitionTimeOut ?? DefaultLockAcquisitionTimeout; 66 | 67 | _acquired = RetryUntilTrue( 68 | () => 69 | { 70 | //Modified from ServiceStack.Redis.RedisLock 71 | //This pattern is taken from the redis command for SETNX http://redis.io/commands/setnx 72 | //Calculate a unix time for when the lock should expire 73 | 74 | lockMaxAge = lockMaxAge ?? DefaultLockMaxAge; 75 | // hold the lock for the default amount of time if not specified. 76 | DateTime expireTime = DateTime.UtcNow.Add(lockMaxAge.Value); 77 | _lockValue = (expireTime.ToUnixTimeMs() + 1).ToString(CultureInfo.InvariantCulture); 78 | 79 | //Try to set the lock, if it does not exist this will succeed and the lock is obtained 80 | var nx = redisClient.SetEntryIfNotExists(_lockKey, _lockValue); 81 | if (nx) 82 | return true; 83 | 84 | //If we've gotten here then a key for the lock is present. This could be because the lock is 85 | //correctly acquired or it could be because a client that had acquired the lock crashed (or didn't release it properly). 86 | //Therefore we need to get the value of the lock to see when it should expire 87 | string existingLockValue = redisClient.Get(_lockKey); 88 | long lockExpireTime; 89 | if (!long.TryParse(existingLockValue, out lockExpireTime)) 90 | return false; 91 | //If the expire time is greater than the current time then we can't let the lock go yet 92 | if (lockExpireTime > DateTime.UtcNow.ToUnixTimeMs()) 93 | return false; 94 | 95 | //If the expire time is less than the current time then it wasn't released properly and we can attempt to 96 | //acquire the lock. This is done by setting the lock to our timeout string AND checking to make sure 97 | //that what is returned is the old timeout string in order to account for a possible race condition. 98 | return redisClient.GetAndSetEntry(_lockKey, _lockValue) == existingLockValue; 99 | }, 100 | lockSpinTime // loop attempting to get the lock for this amount of time. 101 | ); 102 | } 103 | 104 | #endregion 105 | 106 | #region Properties 107 | 108 | private bool IsDisposed 109 | { 110 | get { return _disposed == 1; } 111 | } 112 | 113 | /// 114 | /// True if the lock has already been explicitly released, or if it has determined that another client has superceded this lock. 115 | /// 116 | public bool IsReleased { get; private set; } 117 | 118 | public bool IsAcquired { get { return _acquired; } } 119 | 120 | public string Key { get { return _rawKey; } } 121 | 122 | #endregion 123 | 124 | #region Methods 125 | 126 | /// 127 | /// Gets the value of the locked key. 128 | /// 129 | public TValue GetValue(IRedisClient client) 130 | { 131 | AssertLockIsValid(client); 132 | AssertNotReleased("GetValue"); 133 | return client.Get(_valueKey); 134 | } 135 | 136 | /// 137 | /// Updates the value in the cache but does not release the lock. 138 | /// 139 | public bool PutValue(IRedisClient client, TValue cacheValue, TimeSpan? expiresIn = null) 140 | { 141 | AssertLockIsValid(client, true); 142 | AssertNotReleased("PutValue"); 143 | 144 | bool success; 145 | using (var tx = client.CreateTransaction()) 146 | { 147 | if (expiresIn.HasValue) 148 | { 149 | tx.QueueCommand(c => c.Set(_valueKey, cacheValue, expiresIn.Value)); 150 | } 151 | else 152 | { 153 | tx.QueueCommand(c => c.Set(_valueKey, cacheValue)); 154 | } 155 | success = tx.Commit(); 156 | } 157 | return success; 158 | } 159 | 160 | /// 161 | /// Puts a value and releases the lock. 162 | /// 163 | public bool PutAndRelease(IRedisClient client, TValue cacheValue, TimeSpan? expiresIn = null) 164 | { 165 | AssertLockIsValid(client, true); 166 | AssertNotReleased("PutAndRelease"); 167 | lock (_releaseLockObj) 168 | { 169 | AssertNotReleased("PutAndRelease"); 170 | bool success; 171 | 172 | using (var tx = client.CreateTransaction()) 173 | { 174 | if (expiresIn.HasValue) 175 | { 176 | tx.QueueCommand(c => c.Set(_valueKey, cacheValue, expiresIn.Value)); 177 | } 178 | else 179 | { 180 | tx.QueueCommand(c => c.Set(_valueKey, cacheValue)); 181 | } 182 | 183 | tx.QueueCommand(c => c.Remove(_lockKey)); 184 | success = tx.Commit(); 185 | } 186 | if (success) IsReleased = true; // if false, IsRelease is not determined because cause of failure is not determined. 187 | } 188 | return IsReleased; 189 | } 190 | 191 | public bool DeleteAndRelease(IRedisClient client) 192 | { 193 | AssertLockIsValid(client, true); 194 | AssertNotReleased("DeleteAndRelease"); 195 | lock (_releaseLockObj) 196 | { 197 | AssertNotReleased("DeleteAndRelease"); 198 | bool success; 199 | 200 | using (var tx = client.CreateTransaction()) 201 | { 202 | tx.QueueCommand(c => c.Remove(_valueKey)); 203 | tx.QueueCommand(c => c.Remove(_lockKey)); 204 | success = tx.Commit(); 205 | } 206 | if (success) IsReleased = true; // if false, IsRelease is not determined because cause of failure is not determined. 207 | } 208 | return IsReleased; 209 | } 210 | 211 | /// 212 | /// Releases lock. Guaranteed to set this object's state to released, even if 213 | /// exception is thrown while attempting to manipulate the lock key in Redis. 214 | /// 215 | public void Release(IRedisClient client) 216 | { 217 | if (IsReleased) return; 218 | lock (_releaseLockObj) 219 | { 220 | if (IsReleased) return; 221 | IsReleased = true; // with either branch below, the result is always that this lock is considered released. 222 | 223 | client.Watch(_lockKey); 224 | if (!LockValueIsCurrent(client)) 225 | { 226 | // the lock doesn't have our value anymore, and should be considered expired, and by implication, is released. 227 | client.UnWatch(); 228 | return; 229 | } 230 | 231 | using (var tx = client.CreateTransaction()) 232 | { 233 | tx.QueueCommand(c => c.Remove(_lockKey)); 234 | tx.Commit(); 235 | } 236 | } 237 | } 238 | 239 | /// 240 | /// Asserts that the lock has not been released 241 | /// 242 | private void AssertNotReleased(string op) 243 | { 244 | if (IsReleased) 245 | throw new InvalidOperationException( 246 | String.Format("You cannot perform this operation ({0}) once the lock has been released.", op)); 247 | } 248 | 249 | /// 250 | /// Verifies that lock is in a valid state or throws an exception indicating the invalid state. 251 | /// 252 | /// The to use for accessing the cache. 253 | /// Set to true if you will use a transaction to manipulate the lock following this call. 254 | /// When is set to true, you must complete a call to Exec() or Unwatch() following this call. 255 | private void AssertLockIsValid(IRedisClient client, bool addWatch = false) 256 | { 257 | // If never acquired, it's automatically invalid. 258 | if (!IsAcquired) throw new InvalidOperationException("You cannot operate on a lock which was not granted/acquired."); 259 | 260 | // If disposed, it's automatically invalid. 261 | if (IsDisposed) throw new ObjectDisposedException(ToString()); 262 | 263 | // If the lock is released, then we don't care about the rest of its state ... other methods may throw InvalidOp exceptions. 264 | if (IsReleased) return; 265 | 266 | // If the lock has our original value, then we still hold it (even if it might be expired, no newer clients have asked for one, so go ahead) // BUG? 267 | string currentValue; 268 | if (addWatch) client.Watch(_lockKey); 269 | if (LockValueIsCurrent(client, out currentValue)) return; 270 | if (addWatch) client.UnWatch(); // if the value was current & addWatch was true, caller is required to call EXEC after Assert. 271 | 272 | 273 | // If we get past here, the lock is definitely invalid, it's just a question of why. 274 | // If the value is null, this lock 275 | if (currentValue == null) 276 | { 277 | IsReleased = true; // permanent condition, mark lock released 278 | throw new LockNotFoundException( 279 | "The lock seems to have be removed from the cache through unsupported means.", _valueKey, this); 280 | } 281 | 282 | // If the cache didn't contain an Int64, or if it represents an earlier time than the lock was promised to be good through, something is wrong with it's value 283 | long retrievedNumeric; 284 | long thisLockGoodThrough = long.Parse(_lockValue); 285 | if (!long.TryParse(currentValue, out retrievedNumeric) || retrievedNumeric < thisLockGoodThrough) 286 | { 287 | IsReleased = true; // permanent condition, mark lock released 288 | throw new LockCorruptedException( 289 | String.Format("The lock was found but is in an incosistent state. Value: {0}, Expected: {1}", 290 | currentValue, _lockValue), _valueKey, this); 291 | } 292 | 293 | 294 | // The remaining case is that the lock value was found, isn't null, is a long, and its 295 | // value supercedes our own. That means our lock expired and another client grabbed one. 296 | 297 | // NOTE: deltaMs will also reflect any differences in the TimeSpan supplied when acquiring 298 | // the lock, though this shouldn't happen (don't request the same lock from multiple 299 | // places with different timeouts!) 300 | 301 | IsReleased = true; // permanent condition, mark lock released 302 | var deltaMs = retrievedNumeric - thisLockGoodThrough; 303 | throw new LockExpiredException( 304 | String.Format("Attempted access to lock {1} when expired and held by a lock {0}ms newer.", 305 | deltaMs, ToString()), _valueKey, this); 306 | 307 | } 308 | 309 | /// 310 | /// Returns true if the value in the cache matches the value that is expected for this lock. 311 | /// 312 | private bool LockValueIsCurrent(IRedisClient client) 313 | { 314 | string newValue; 315 | return LockValueIsCurrent(client, out newValue); 316 | } 317 | 318 | /// 319 | /// Returns true if the value in the cache matches the value that is expected for this lock. 320 | /// 321 | private bool LockValueIsCurrent(IRedisClient client, out string value) 322 | { 323 | value = client.Get(_lockKey); 324 | return (value == _lockValue); 325 | } 326 | 327 | // Use these instead of the ServiceStack implementation, because they guarantee that your 328 | // action will be called at least once, even if timeOut == TimeSpan.Zero 329 | private static bool RetryUntilTrue(Func action, TimeSpan timeOut) 330 | { 331 | var i = 0; 332 | var firstAttempt = DateTime.UtcNow; 333 | 334 | do 335 | { 336 | i++; 337 | if (action()) 338 | { 339 | return true; 340 | } 341 | SleepBackOffMultiplier(i); 342 | } while (DateTime.UtcNow - firstAttempt < timeOut); 343 | 344 | return false; 345 | } 346 | 347 | private static void SleepBackOffMultiplier(int i) 348 | { 349 | //exponential/random retry back-off. 350 | var rand = new Random(Guid.NewGuid().GetHashCode()); 351 | var nextTry = rand.Next( 352 | (int)Math.Pow(i, 2), (int)Math.Pow(i + 1, 2) + 1); 353 | 354 | Thread.Sleep(nextTry); 355 | } 356 | 357 | #endregion 358 | 359 | #region Overrides & Interface Impls 360 | 361 | public override string ToString() 362 | { 363 | return String.Format("KnockLock:{0}:{1}", _valueKey, _lockValue); 364 | } 365 | 366 | public void Dispose() 367 | { 368 | Dispose(_client); 369 | } 370 | 371 | public void Dispose(IRedisClient client) 372 | { 373 | var alreadyDisposed = System.Threading.Interlocked.Exchange(ref _disposed, 1); 374 | if (alreadyDisposed == 1) return; 375 | try 376 | { 377 | IsReleased = true; 378 | 379 | // use watch & transaction to only remove the entry if it still contains OUR value 380 | client.Watch(_lockKey); 381 | if (!LockValueIsCurrent(client)) 382 | { 383 | client.UnWatch(); 384 | return; 385 | } 386 | 387 | using (var tx = client.CreateTransaction()) 388 | { 389 | tx.QueueCommand(r => r.Remove(_lockKey)); 390 | tx.Commit(); 391 | } 392 | } 393 | catch (Exception ex) 394 | { 395 | //Log.Error(String.Format("An error occurred while cleaning up {0}.", ToString()), ex); 396 | } 397 | } 398 | 399 | #endregion 400 | } 401 | 402 | } 403 | -------------------------------------------------------------------------------- /src/packages/PostSharp.4.0.39/tools/PostSharp.targets: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 23 | 24 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 139 | 140 | 141 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 154 | 155 | 157 | 158 | 160 | 161 | 162 | 163 | <_PostSharpTargetFrameworkIdentifier>$(TargetFrameworkIdentifier) 164 | <_PostSharpTargetFrameworkIdentifier Condition="'$(TargetFrameworkIdentifier)'==''">.NETFramework 165 | <_PostSharpTargetFrameworkVersion>$(TargetFrameworkVersion) 166 | <_PostSharpTargetFrameworkVersion Condition="'$(_PostSharpTargetFrameworkIdentifier)'=='.NETFramework' AND ( '$(TargetFrameworkVersion)'=='v2.0' OR '$(TargetFrameworkVersion)'=='v3.0' )">v3.5 167 | $(_PostSharpTargetFrameworkIdentifier),$(TargetFrameworkVersion) 168 | <_NetFramework4Version>$(registry:HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\NET Framework Setup\NDP\v4\Full@Release) 169 | .NETFramework,v4.0 170 | .NETFramework,v4.5 171 | .NETFramework,v4.5.1 172 | True 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 193 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 269 | 270 | 271 | 272 | 273 | 274 | $(SolutionDir)\$(SolutionName).$(Configuration).$(Platform).pssln 275 | $(SolutionDir)\$(SolutionName).$(Configuration).pssln 276 | $(SolutionDir)\$(SolutionName).pssln 277 | 278 | 279 | 280 | <_PostSharpProject Include="$(PostSharpSolutionProject)" Condition="$(PostSharpSolutionProject)!=''" /> 281 | <_PostSharpProject Include="$(PostSharpProject)"/> 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 314 | 315 | 316 | 317 | 319 | 320 | 321 | 323 | 327 | 328 | 329 | 330 | 331 | 332 | 333 | 334 | 335 | 336 | 337 | 338 | 339 | 340 | 341 | 342 | 343 | 344 | 345 | 346 | 348 | 350 | 352 | 353 | 354 | 355 | 356 | 357 | 358 | 359 | 360 | 361 | 364 | 365 | 366 | 367 | 368 | 369 | 370 | 371 | 372 | 373 | 374 | 375 | 376 | 377 | 378 | $(PostSharp30DependsOn); 379 | PostSharp30ExtractBinaries; 380 | PostSharp30InstallVsx; 381 | 382 | 383 | $(PostSharpInspectDependsOn); 384 | PostSharp30InspectConstants; 385 | PostSharp30InspectReferences; 386 | PostSharp30DisablePreviousVersions 387 | 388 | 389 | PostSharpInspect; 390 | PostSharp30DefineConstant; 391 | $(CoreCompileDependsOn) 392 | 393 | 394 | PostSharp30TimestampBeforeCompile; 395 | $(CompileDependsOn); 396 | PostSharp30TimestampAfterCompile; 397 | PostSharp30 398 | 399 | 400 | $(BuildDependsOn); 401 | PostSharp30Verify 402 | 403 | 404 | $(CleanDependsOn); 405 | PostSharp30Clean 406 | 407 | 408 | 409 | 410 | 411 | 412 | 413 | $(PostSharp30DependsOn); 414 | CodeContractInstrument; 415 | CodeContractsPerformCodeAnalysis 416 | 417 | False 418 | 419 | 420 | 421 | 422 | $(PrepareForRunDependsOn); 423 | PostSharp30ChangeAppConfig 424 | 425 | 426 | 427 | 428 | 429 | 431 | 432 | 433 | $(RunCodeAnalysisDependsOn);PostSharp30CopyOutputToCodeAnalysis 434 | 435 | 436 | 437 | 438 | 439 | 441 | 446 | 447 | 448 | 449 | 450 | 451 | 452 | 453 | 454 | 455 | 456 | 457 | 458 | 459 | 460 | 463 | 464 | 465 | 466 | 467 | 468 | 469 | 470 | --------------------------------------------------------------------------------