Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
<CentralPackageTransitivePinningEnabled>true</CentralPackageTransitivePinningEnabled>
</PropertyGroup>
<ItemGroup>
<PackageVersion Include="GeneralData.GBase8s.DataProvider" Version="1.0.0.1" />
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This library isn't well-established enough for use here (< 1K downloads, < 2 months old).

Looks like Odbc seems to be the more common pattern for GBase interactions in the NuGet world

<PackageVersion Include="Microsoft.SourceLink.GitHub" Version="8.0.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.PublicApiAnalyzers" Version="3.3.4" />
<PackageVersion Include="Azure.Storage.Blobs" Version="12.19.1" />
Expand Down
3 changes: 3 additions & 0 deletions src/DistributedLock.GBase/AssemblyAttributes.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
using System.Runtime.CompilerServices;

[assembly: InternalsVisibleTo("DistributedLock.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100fd3af56ccc8ed94fffe25bfd651e6a5674f8f20a76d37de800dd0f7380e04f0fde2da6fa200380b14fe398605b6f470c87e5e0a0bf39ae871f07536a4994aa7a0057c4d3bcedc8fef3eecb0c88c2024a1b3289305c2393acd9fb9f9a42d0bd7826738ce864d507575ea3a1fe1746ab19823303269f79379d767949807f494be8")]
67 changes: 67 additions & 0 deletions src/DistributedLock.GBase/DistributedLock.GBase.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFrameworks>netstandard2.1;net472</TargetFrameworks>
<RootNamespace>Medallion.Threading.GBase</RootNamespace>
<GenerateDocumentationFile>True</GenerateDocumentationFile>
<WarningLevel>4</WarningLevel>
<LangVersion>Latest</LangVersion>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>

<PropertyGroup>
<Version>2.7.0.2</Version>
<AssemblyVersion>1.0.0.0</AssemblyVersion>
<Authors>Michael Adelson</Authors>
<Description>Provides a distributed lock implementation based on GBase Database</Description>
<Copyright>Copyright © 2021 Michael Adelson</Copyright>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<PackageTags>distributed lock async mutex reader writer sql GBase</PackageTags>
<PackageProjectUrl>https://github.com/madelson/DistributedLock</PackageProjectUrl>
<RepositoryUrl>https://github.com/madelson/DistributedLock</RepositoryUrl>
<FileVersion>1.0.0.0</FileVersion>
<PackageReleaseNotes>See https://github.com/madelson/DistributedLock#release-notes</PackageReleaseNotes>
<SignAssembly>true</SignAssembly>
<AssemblyOriginatorKeyFile>..\DistributedLock.snk</AssemblyOriginatorKeyFile>
</PropertyGroup>

<PropertyGroup Condition="'$(Configuration)' == 'Release'">
<Optimize>True</Optimize>
<GeneratePackageOnBuild>True</GeneratePackageOnBuild>
<TreatWarningsAsErrors>True</TreatWarningsAsErrors>
<TreatSpecificWarningsAsErrors />
<!-- see https://github.com/dotnet/sdk/issues/2679 -->
<DebugType>embedded</DebugType>
<!-- see https://mitchelsellers.com/blog/article/net-5-deterministic-builds-source-linking -->
<ContinuousIntegrationBuild>true</ContinuousIntegrationBuild>
<EmbedUntrackedSources>true</EmbedUntrackedSources>
</PropertyGroup>

<PropertyGroup Condition="'$(Configuration)' == 'Debug'">
<Optimize>False</Optimize>
<NoWarn>1591</NoWarn>
<DefineConstants>TRACE;DEBUG</DefineConstants>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="GeneralData.GBase8s.DataProvider" />
Copy link
Owner

@madelson madelson Jan 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove

<PackageReference Include="Microsoft.SourceLink.GitHub" PrivateAssets="All" />
<PackageReference Include="Microsoft.CodeAnalysis.PublicApiAnalyzers" PrivateAssets="All" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\DistributedLock.Core\DistributedLock.Core.csproj" />
</ItemGroup>

<ItemGroup>
<Using Remove="System.Net.Http" />
</ItemGroup>
<ItemGroup>
<Content Include="..\..\..\..\GeneralData.EntityFrameworkCore.GBase.DataProvider.dll" PackagePath="lib\netstandard2.1">
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should not be included

<Pack>true</Pack>
<PackageCopyToOutput>true</PackageCopyToOutput>
</Content>
</ItemGroup>
<Import Project="..\FixDistributedLockCoreDependencyVersion.targets" />
</Project>
68 changes: 68 additions & 0 deletions src/DistributedLock.GBase/GBaseConnectionOptionsBuilder.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
using Medallion.Threading.Internal;

namespace Medallion.Threading.GBase;

/// <summary>
/// Specifies options for connecting to and locking against an GBase database
/// </summary>
public sealed class GBaseConnectionOptionsBuilder
{
private TimeoutValue? _keepaliveCadence;
private bool? _useMultiplexing;

internal GBaseConnectionOptionsBuilder() { }

/// <summary>
/// GBase does not kill idle connections by default, so by default keepalive is disabled (set to <see cref="Timeout.InfiniteTimeSpan"/>).
///
/// However, if you are using the IDLE_TIME setting in GBase or if your network is dropping connections that are idle holding locks for
/// a long time, you can set a value for keepalive to prevent this from happening.
///
/// </summary>
public GBaseConnectionOptionsBuilder KeepaliveCadence(TimeSpan keepaliveCadence)
{
this._keepaliveCadence = new TimeoutValue(keepaliveCadence, nameof(keepaliveCadence));
return this;
}

/// <summary>
/// This mode takes advantage of the fact that while "holding" a lock (or other synchronization primitive)
/// a connection is essentially idle. Thus, rather than creating a new connection for each held lock it is
/// often possible to multiplex a shared connection so that that connection can hold multiple locks at the same time.
///
/// Multiplexing is on by default.
///
/// This is implemented in such a way that releasing a lock held on such a connection will never be blocked by an
/// Acquire() call that is waiting to acquire a lock on that same connection. For this reason, the multiplexing
/// strategy is "optimistic": if the lock can't be acquired instantaneously on the shared connection, a new (shareable)
/// connection will be allocated.
///
/// This option can improve performance and avoid connection pool starvation in high-load scenarios. It is also
/// particularly applicable to cases where <see cref="IDistributedLock.TryAcquire(TimeSpan, System.Threading.CancellationToken)"/>
/// semantics are used with a zero-length timeout.
/// </summary>
public GBaseConnectionOptionsBuilder UseMultiplexing(bool useMultiplexing = true)
{
this._useMultiplexing = useMultiplexing;
return this;
}

internal static (TimeoutValue keepaliveCadence, bool useMultiplexing) GetOptions(Action<GBaseConnectionOptionsBuilder>? optionsBuilder)
{
GBaseConnectionOptionsBuilder? options;
if (optionsBuilder != null)
{
options = new GBaseConnectionOptionsBuilder();
optionsBuilder(options);
}
else
{
options = null;
}

var keepaliveCadence = options?._keepaliveCadence ?? Timeout.InfiniteTimeSpan;
var useMultiplexing = options?._useMultiplexing ?? true;

return (keepaliveCadence, useMultiplexing);
}
}
80 changes: 80 additions & 0 deletions src/DistributedLock.GBase/GBaseDatabaseConnection.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
using GBS.Data.GBasedbt;
using Medallion.Threading.Internal.Data;
using System.Data;

namespace Medallion.Threading.GBase;

internal class GBaseDatabaseConnection : DatabaseConnection
{
public const string ApplicationNameIndicatorPrefix = "__DistributedLock.ApplicationName=";

// see SleepAsync() for why we need this
private readonly IDbConnection _innerConnection;

public GBaseDatabaseConnection(IDbConnection connection)
: this(connection, isExternallyOwned: true)
{
}

public GBaseDatabaseConnection(IDbTransaction transaction)
: base(transaction, isExternallyOwned: true)
{
this._innerConnection = transaction.Connection;
}

public GBaseDatabaseConnection(string connectionString)
: this(CreateConnection(connectionString), isExternallyOwned: false)
{
}

private GBaseDatabaseConnection(IDbConnection connection, bool isExternallyOwned)
: base(connection, isExternallyOwned)
{
this._innerConnection = connection;
}

public override bool ShouldPrepareCommands => false;

public override bool IsCommandCancellationException(Exception exception) =>
exception is GbsException gbsException
&& (gbsException.ErrorCode == 01013 || gbsException.ErrorCode == 00936 || gbsException.ErrorCode == 00604);

public override async Task SleepAsync(TimeSpan sleepTime, CancellationToken cancellationToken, Func<DatabaseCommand, CancellationToken, ValueTask<int>> executor)
{
using var sleepCommand = this.CreateCommand();
sleepCommand.SetCommandText("dbms_lock_sleep(?)");
sleepCommand.AddParameter("seconds", sleepTime.TotalSeconds);

try
{
await executor(sleepCommand, cancellationToken).ConfigureAwait(false);
}
catch when (!cancellationToken.IsCancellationRequested)
{
// GBase doesn't fire StateChange unless the State is observed or the connection is explicitly opened/closed. Therefore, we observe
// the state on seeing any exception in order to for the event to fire.
_ = this._innerConnection.State;
throw;
}
}

public static GbsConnection CreateConnection(string connectionString)
{
if (connectionString == null) { throw new ArgumentNullException(connectionString, nameof(connectionString)); }

// The .NET GBase provider does not currently support ApplicationName natively as a connection string property.
// However, that functionality is relied on by many of our tests. As a workaround, we permit the application name
// to be included in the connection string using a custom encoding scheme. This is only intended to work in tests!
if (connectionString.StartsWith(ApplicationNameIndicatorPrefix, StringComparison.Ordinal))
{
var firstSeparatorIndex = connectionString.IndexOf(';');
var applicationName = connectionString.Substring(startIndex: ApplicationNameIndicatorPrefix.Length, length: firstSeparatorIndex - ApplicationNameIndicatorPrefix.Length);
// After upgrading the GBase client to 23.6.1, the connection pool sometimes seems to grow beyond what is strictly required.
// This causes issues if we're tracking connections by name. Therefore, we disable pooling on named connections
var connection = new GbsConnection(connectionString.Substring(startIndex: firstSeparatorIndex + 1));
return connection;
}

return new GbsConnection(connectionString);
}
}
Loading