Skip to content

Commit e168584

Browse files
authored
Merge pull request #114 from madelson/release-2.3
Release 2.3
2 parents ccc1e1e + 1fa98a2 commit e168584

File tree

68 files changed

+2125
-255
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

68 files changed

+2125
-255
lines changed

DistributedLock.Core/AssemblyAttributes.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,5 @@
1414
[assembly: InternalsVisibleTo("DistributedLock.Redis, PublicKey=0024000004800000940000000602000000240000525341310004000001000100fd3af56ccc8ed94fffe25bfd651e6a5674f8f20a76d37de800dd0f7380e04f0fde2da6fa200380b14fe398605b6f470c87e5e0a0bf39ae871f07536a4994aa7a0057c4d3bcedc8fef3eecb0c88c2024a1b3289305c2393acd9fb9f9a42d0bd7826738ce864d507575ea3a1fe1746ab19823303269f79379d767949807f494be8")]
1515
[assembly: InternalsVisibleTo("DistributedLock.ZooKeeper, PublicKey=0024000004800000940000000602000000240000525341310004000001000100fd3af56ccc8ed94fffe25bfd651e6a5674f8f20a76d37de800dd0f7380e04f0fde2da6fa200380b14fe398605b6f470c87e5e0a0bf39ae871f07536a4994aa7a0057c4d3bcedc8fef3eecb0c88c2024a1b3289305c2393acd9fb9f9a42d0bd7826738ce864d507575ea3a1fe1746ab19823303269f79379d767949807f494be8")]
1616
[assembly: InternalsVisibleTo("DistributedLock.MySql, PublicKey=0024000004800000940000000602000000240000525341310004000001000100fd3af56ccc8ed94fffe25bfd651e6a5674f8f20a76d37de800dd0f7380e04f0fde2da6fa200380b14fe398605b6f470c87e5e0a0bf39ae871f07536a4994aa7a0057c4d3bcedc8fef3eecb0c88c2024a1b3289305c2393acd9fb9f9a42d0bd7826738ce864d507575ea3a1fe1746ab19823303269f79379d767949807f494be8")]
17+
[assembly: InternalsVisibleTo("DistributedLock.Oracle, PublicKey=0024000004800000940000000602000000240000525341310004000001000100fd3af56ccc8ed94fffe25bfd651e6a5674f8f20a76d37de800dd0f7380e04f0fde2da6fa200380b14fe398605b6f470c87e5e0a0bf39ae871f07536a4994aa7a0057c4d3bcedc8fef3eecb0c88c2024a1b3289305c2393acd9fb9f9a42d0bd7826738ce864d507575ea3a1fe1746ab19823303269f79379d767949807f494be8")]
1718
#endif

DistributedLock.Core/DistributedLock.Core.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
</PropertyGroup>
1111

1212
<PropertyGroup>
13-
<Version>1.0.3</Version>
13+
<Version>1.0.4</Version>
1414
<AssemblyVersion>1.0.0.0</AssemblyVersion>
1515
<Authors>Michael Adelson</Authors>
1616
<Description>Core interfaces and utilities that support the DistributedLock.* family of packages</Description>

DistributedLock.Core/Internal/Data/DatabaseCommand.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,9 @@ private async ValueTask<TResult> InternalExecuteAndPropagateCancellationAsync<TS
159159
Invariant.Require(cancellationToken.CanBeCanceled);
160160

161161
using var _ = await this.AcquireConnectionLockIfNeeded(isConnectionMonitoringQuery).ConfigureAwait(false);
162-
await this.PrepareIfNeededAsync(cancellationToken).ConfigureAwait(false);
162+
// Note: for now we cannot pass cancellationToken to PrepareAsync() because this will break on Postgres which
163+
// is the only db we currently support that needs Prepare currently. See https://github.com/npgsql/npgsql/issues/4209
164+
await this.PrepareIfNeededAsync(CancellationToken.None).ConfigureAwait(false);
163165
try
164166
{
165167
return await executeAsync(state, cancellationToken).ConfigureAwait(false);

DistributedLock.FileSystem/DistributedLock.FileSystem.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
</PropertyGroup>
1111

1212
<PropertyGroup>
13-
<Version>1.0.0</Version>
13+
<Version>1.0.1</Version>
1414
<AssemblyVersion>1.0.0.0</AssemblyVersion>
1515
<Authors>Michael Adelson</Authors>
1616
<Description>Provides a distributed lock implementation based on file locks</Description>

DistributedLock.FileSystem/FileDistributedLock.cs

Lines changed: 60 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,13 @@ namespace Medallion.Threading.FileSystem
1313
/// </summary>
1414
public sealed partial class FileDistributedLock : IInternalDistributedLock<FileDistributedLockHandle>
1515
{
16+
/// <summary>
17+
/// Since <see cref="UnauthorizedAccessException"/> can be thrown EITHER transiently or for permissions issues, we retry up to this many times
18+
/// before we assume that the issue is non-transient. Empirically I've found this value to be reliable both locally and on AppVeyor (if there
19+
/// IS a problem there's little risk to trying more times because we'll eventually be failing hard).
20+
/// </summary>
21+
private const int MaxUnauthorizedAccessExceptionRetries = 400;
22+
1623
// These are not configurable currently because in the future we may want to change the implementation of FileDistributedLock
1724
// to leverage native methods which may allow for actual blocking. The values here reflect the idea that we expect file locks
1825
// to be used in cases where contention is rare
@@ -64,15 +71,13 @@ public FileDistributedLock(DirectoryInfo lockFileDirectory, string name)
6471

6572
private FileDistributedLockHandle? TryAcquire(CancellationToken cancellationToken)
6673
{
74+
var retryCount = 0;
75+
6776
while (true)
6877
{
6978
cancellationToken.ThrowIfCancellationRequested();
7079

71-
try { System.IO.Directory.CreateDirectory(this.Directory); }
72-
catch (Exception ex)
73-
{
74-
throw new InvalidOperationException($"Failed to ensure that lock file directory {this.Directory} exists", ex);
75-
}
80+
this.EnsureDirectoryExists();
7681

7782
FileStream lockFileStream;
7883
try
@@ -88,9 +93,36 @@ public FileDistributedLock(DirectoryInfo lockFileDirectory, string name)
8893
// this should almost never happen because we just created the directory but in a race condition it could. Just retry
8994
continue;
9095
}
91-
catch (UnauthorizedAccessException) when (System.IO.Directory.Exists(this.Name))
96+
catch (UnauthorizedAccessException)
9297
{
93-
throw new InvalidOperationException($"Failed to create lock file '{this.Name}' because it is already the name of a directory");
98+
// This can happen in few cases:
99+
100+
// The path is already directory, so we'll never be able to open a handle of it as a file
101+
if (System.IO.Directory.Exists(this.Name))
102+
{
103+
throw new InvalidOperationException($"Failed to create lock file '{this.Name}' because it is already the name of a directory");
104+
}
105+
106+
// The file exists and is read-only
107+
FileAttributes attributes;
108+
try { attributes = File.GetAttributes(this.Name); }
109+
catch { attributes = FileAttributes.Normal; } // e. g. could fail with FileNotFoundException
110+
if (attributes.HasFlag(FileAttributes.ReadOnly))
111+
{
112+
// We could support this by eschewing DeleteOnClose once we detect that a file is read-only,
113+
// but absent interest or a use-case we'll just throw for now
114+
throw new NotSupportedException($"Locking on read-only file '{this.Name}' is not supported");
115+
}
116+
117+
// Frustratingly, this error can be thrown transiently due to concurrent creation/deletion. Initially assume
118+
// that it is transient and just retry
119+
if (++retryCount <= MaxUnauthorizedAccessExceptionRetries)
120+
{
121+
continue;
122+
}
123+
124+
// If we get here, we've exhausted our retries: assume that it is a legitimate permissions issue
125+
throw;
94126
}
95127
// this should never happen because we validate. However if it does (e. g. due to some system configuration change?), throw so that
96128
// this doesn't end up in the IOException block (PathTooLongException is IOException)
@@ -104,5 +136,26 @@ public FileDistributedLock(DirectoryInfo lockFileDirectory, string name)
104136
return new FileDistributedLockHandle(lockFileStream);
105137
}
106138
}
139+
140+
private void EnsureDirectoryExists()
141+
{
142+
var retryCount = 0;
143+
144+
while (true)
145+
{
146+
try
147+
{
148+
System.IO.Directory.CreateDirectory(this.Directory);
149+
return;
150+
}
151+
// This can indicate either a transient failure during concurrent creation/deletion or a permissions issue.
152+
// If we encounter it, assume it is transient unless it persists.
153+
catch (UnauthorizedAccessException) when (++retryCount <= MaxUnauthorizedAccessExceptionRetries) { }
154+
catch (Exception ex)
155+
{
156+
throw new InvalidOperationException($"Failed to ensure that lock file directory {this.Directory} exists", ex);
157+
}
158+
}
159+
}
107160
}
108161
}

DistributedLock.MySql/MySqlDistributedLock.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -164,14 +164,14 @@ private static IDbDistributedLock CreateInternalLock(string name, string connect
164164
return new DedicatedConnectionOrTransactionDbDistributedLock(name, () => new MySqlDatabaseConnection(connectionString), useTransaction: false, keepaliveCadence);
165165
}
166166

167-
static IDbDistributedLock CreateInternalLock(string name, IDbConnection connection)
167+
private static IDbDistributedLock CreateInternalLock(string name, IDbConnection connection)
168168
{
169169
if (connection == null) { throw new ArgumentNullException(nameof(connection)); }
170170

171171
return new DedicatedConnectionOrTransactionDbDistributedLock(name, () => new MySqlDatabaseConnection(connection));
172172
}
173173

174-
static IDbDistributedLock CreateInternalLock(string name, IDbTransaction transaction)
174+
private static IDbDistributedLock CreateInternalLock(string name, IDbTransaction transaction)
175175
{
176176
if (transaction == null) { throw new ArgumentNullException(nameof(transaction)); }
177177

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
using System.Runtime.CompilerServices;
2+
3+
[assembly: InternalsVisibleTo("DistributedLock.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100fd3af56ccc8ed94fffe25bfd651e6a5674f8f20a76d37de800dd0f7380e04f0fde2da6fa200380b14fe398605b6f470c87e5e0a0bf39ae871f07536a4994aa7a0057c4d3bcedc8fef3eecb0c88c2024a1b3289305c2393acd9fb9f9a42d0bd7826738ce864d507575ea3a1fe1746ab19823303269f79379d767949807f494be8")]
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<TargetFrameworks>netstandard2.1;net462</TargetFrameworks>
5+
<RootNamespace>Medallion.Threading.Oracle</RootNamespace>
6+
<GenerateDocumentationFile>True</GenerateDocumentationFile>
7+
<WarningLevel>4</WarningLevel>
8+
<LangVersion>Latest</LangVersion>
9+
<Nullable>enable</Nullable>
10+
</PropertyGroup>
11+
12+
<PropertyGroup>
13+
<Version>1.0.0</Version>
14+
<AssemblyVersion>1.0.0.0</AssemblyVersion>
15+
<Authors>Michael Adelson</Authors>
16+
<Description>Provides a distributed lock implementation based on Oracle Database</Description>
17+
<Copyright>Copyright © 2021 Michael Adelson</Copyright>
18+
<PackageLicenseExpression>MIT</PackageLicenseExpression>
19+
<PackageTags>distributed lock async mutex reader writer sql oracle</PackageTags>
20+
<PackageProjectUrl>https://github.com/madelson/DistributedLock</PackageProjectUrl>
21+
<RepositoryUrl>https://github.com/madelson/DistributedLock</RepositoryUrl>
22+
<FileVersion>1.0.0.0</FileVersion>
23+
<PackageReleaseNotes>See https://github.com/madelson/DistributedLock#release-notes</PackageReleaseNotes>
24+
<SignAssembly>true</SignAssembly>
25+
<AssemblyOriginatorKeyFile>..\DistributedLock.snk</AssemblyOriginatorKeyFile>
26+
</PropertyGroup>
27+
28+
<PropertyGroup Condition="'$(Configuration)' == 'Release'">
29+
<Optimize>True</Optimize>
30+
<GeneratePackageOnBuild>True</GeneratePackageOnBuild>
31+
<TreatWarningsAsErrors>True</TreatWarningsAsErrors>
32+
<TreatSpecificWarningsAsErrors />
33+
<!-- see https://github.com/dotnet/sdk/issues/2679 -->
34+
<DebugType>embedded</DebugType>
35+
</PropertyGroup>
36+
37+
<PropertyGroup Condition="'$(Configuration)' == 'Debug'">
38+
<Optimize>False</Optimize>
39+
<NoWarn>1591</NoWarn>
40+
<DefineConstants>TRACE;DEBUG</DefineConstants>
41+
</PropertyGroup>
42+
43+
<ItemGroup>
44+
<PackageReference Include="Oracle.ManagedDataAccess.Core" Version="3.21.4" Condition="'$(TargetFramework)' == 'netstandard2.1'" />
45+
<PackageReference Include="Oracle.ManagedDataAccess" Version="21.4.0" Condition="'$(TargetFramework)' == 'net462'"/>
46+
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="All" />
47+
</ItemGroup>
48+
49+
<ItemGroup>
50+
<ProjectReference Include="..\DistributedLock.Core\DistributedLock.Core.csproj" />
51+
</ItemGroup>
52+
53+
<Import Project="..\CopyPackageToPublishDirectory.targets" />
54+
<Import Project="..\FixDistributedLockCoreDependencyVersion.targets" />
55+
</Project>
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
using Medallion.Threading.Internal;
2+
using System;
3+
using System.Collections.Generic;
4+
using System.Text;
5+
using System.Threading;
6+
7+
namespace Medallion.Threading.Oracle
8+
{
9+
/// <summary>
10+
/// Specifies options for connecting to and locking against an Oracle database
11+
/// </summary>
12+
public sealed class OracleConnectionOptionsBuilder
13+
{
14+
private TimeoutValue? _keepaliveCadence;
15+
private bool? _useMultiplexing;
16+
17+
internal OracleConnectionOptionsBuilder() { }
18+
19+
/// <summary>
20+
/// Oracle does not kill idle connections by default, so by default keepalive is disabled (set to <see cref="Timeout.InfiniteTimeSpan"/>).
21+
///
22+
/// However, if you are using the IDLE_TIME setting in Oracle or if your network is dropping connections that are idle holding locks for
23+
/// a long time, you can set a value for keepalive to prevent this from happening.
24+
///
25+
/// See https://stackoverflow.com/questions/1966247/idle-timeout-parameter-in-oracle.
26+
/// </summary>
27+
public OracleConnectionOptionsBuilder KeepaliveCadence(TimeSpan keepaliveCadence)
28+
{
29+
this._keepaliveCadence = new TimeoutValue(keepaliveCadence, nameof(keepaliveCadence));
30+
return this;
31+
}
32+
33+
/// <summary>
34+
/// This mode takes advantage of the fact that while "holding" a lock (or other synchronization primitive)
35+
/// a connection is essentially idle. Thus, rather than creating a new connection for each held lock it is
36+
/// often possible to multiplex a shared connection so that that connection can hold multiple locks at the same time.
37+
///
38+
/// Multiplexing is on by default.
39+
///
40+
/// This is implemented in such a way that releasing a lock held on such a connection will never be blocked by an
41+
/// Acquire() call that is waiting to acquire a lock on that same connection. For this reason, the multiplexing
42+
/// strategy is "optimistic": if the lock can't be acquired instantaneously on the shared connection, a new (shareable)
43+
/// connection will be allocated.
44+
///
45+
/// This option can improve performance and avoid connection pool starvation in high-load scenarios. It is also
46+
/// particularly applicable to cases where <see cref="IDistributedLock.TryAcquire(TimeSpan, System.Threading.CancellationToken)"/>
47+
/// semantics are used with a zero-length timeout.
48+
/// </summary>
49+
public OracleConnectionOptionsBuilder UseMultiplexing(bool useMultiplexing = true)
50+
{
51+
this._useMultiplexing = useMultiplexing;
52+
return this;
53+
}
54+
55+
internal static (TimeoutValue keepaliveCadence, bool useMultiplexing) GetOptions(Action<OracleConnectionOptionsBuilder>? optionsBuilder)
56+
{
57+
OracleConnectionOptionsBuilder? options;
58+
if (optionsBuilder != null)
59+
{
60+
options = new OracleConnectionOptionsBuilder();
61+
optionsBuilder(options);
62+
}
63+
else
64+
{
65+
options = null;
66+
}
67+
68+
var keepaliveCadence = options?._keepaliveCadence ?? Timeout.InfiniteTimeSpan;
69+
var useMultiplexing = options?._useMultiplexing ?? true;
70+
71+
return (keepaliveCadence, useMultiplexing);
72+
}
73+
}
74+
}
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
using Medallion.Threading.Internal.Data;
2+
using Oracle.ManagedDataAccess.Client;
3+
using System;
4+
using System.Collections.Generic;
5+
using System.Data;
6+
using System.Linq;
7+
using System.Text;
8+
using System.Threading;
9+
using System.Threading.Tasks;
10+
11+
namespace Medallion.Threading.Oracle
12+
{
13+
internal class OracleDatabaseConnection : DatabaseConnection
14+
{
15+
public const string ApplicationNameIndicatorPrefix = "__DistributedLock.ApplicationName=";
16+
17+
// see SleepAsync() for why we need this
18+
private readonly IDbConnection _innerConnection;
19+
20+
public OracleDatabaseConnection(IDbConnection connection)
21+
: this(connection, isExternallyOwned: true)
22+
{
23+
}
24+
25+
public OracleDatabaseConnection(IDbTransaction transaction)
26+
: base(transaction, isExternallyOwned: true)
27+
{
28+
this._innerConnection = transaction.Connection;
29+
}
30+
31+
public OracleDatabaseConnection(string connectionString)
32+
: this(CreateConnection(connectionString), isExternallyOwned: false)
33+
{
34+
}
35+
36+
private OracleDatabaseConnection(IDbConnection connection, bool isExternallyOwned)
37+
: base(connection, isExternallyOwned)
38+
{
39+
this._innerConnection = connection;
40+
}
41+
42+
// from https://docs.oracle.com/html/E10927_01/OracleCommandClass.htm "this method is a no-op" wrt "Prepare()"
43+
public override bool ShouldPrepareCommands => false;
44+
45+
public override bool IsCommandCancellationException(Exception exception) =>
46+
exception is OracleException oracleException
47+
// based on https://docs.oracle.com/cd/E85694_01/ODPNT/CommandCancel.htm
48+
&& (oracleException.Number == 01013 || oracleException.Number == 00936 || oracleException.Number == 00604);
49+
50+
public override async Task SleepAsync(TimeSpan sleepTime, CancellationToken cancellationToken, Func<DatabaseCommand, CancellationToken, ValueTask<int>> executor)
51+
{
52+
using var sleepCommand = this.CreateCommand();
53+
sleepCommand.SetCommandText("BEGIN sys.DBMS_SESSION.SLEEP(:seconds) END;");
54+
sleepCommand.AddParameter("seconds", sleepTime.TotalSeconds);
55+
56+
try
57+
{
58+
await executor(sleepCommand, cancellationToken).ConfigureAwait(false);
59+
}
60+
catch when (!cancellationToken.IsCancellationRequested)
61+
{
62+
// Oracle doesn't fire StateChange unless the State is observed or the connection is explicitly opened/closed. Therefore, we observe
63+
// the state on seeing any exception in order to for the event to fire. See https://github.com/oracle/dotnet-db-samples/issues/226
64+
_ = this._innerConnection.State;
65+
throw;
66+
}
67+
}
68+
69+
public static OracleConnection CreateConnection(string connectionString)
70+
{
71+
if (connectionString == null) { throw new ArgumentNullException(connectionString, nameof(connectionString)); }
72+
73+
// The .NET Oracle provider does not currently support ApplicationName natively as a connection string property.
74+
// However, that functionality is relied on by many of our tests. As a workaround, we permit the application name
75+
// to be included in the connection string using a custom encoding scheme. This is only intended to work in tests!
76+
// See https://github.com/oracle/dotnet-db-samples/issues/216 for more context.
77+
if (connectionString.StartsWith(ApplicationNameIndicatorPrefix, StringComparison.Ordinal))
78+
{
79+
var firstSeparatorIndex = connectionString.IndexOf(';');
80+
var applicationName = connectionString.Substring(startIndex: ApplicationNameIndicatorPrefix.Length, length: firstSeparatorIndex - ApplicationNameIndicatorPrefix.Length);
81+
var connection = new OracleConnection(connectionString.Substring(startIndex: firstSeparatorIndex + 1));
82+
connection.ConnectionOpen += _ => connection.ClientInfo = applicationName;
83+
return connection;
84+
}
85+
86+
return new OracleConnection(connectionString);
87+
}
88+
}
89+
}

0 commit comments

Comments
 (0)