mirror of
https://github.com/MAKS-IT-COM/maksit-hamode.git
synced 2026-06-30 22:36:42 +02:00
(refactor): centralize lease validation, update core/results/redis deps
This commit is contained in:
parent
147461edd8
commit
a3b4778a15
@ -5,6 +5,13 @@ All notable changes to this project will be documented in this file.
|
|||||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
||||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## [1.0.5] - 2026-06-28
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Centralized lease input validation and provider-configuration checks in shared `LeaseInputValidation` helpers used by PostgreSQL, Redis, and etcd lease services.
|
||||||
|
- Centralized lease failure result mapping in `LeaseResultErrors`, using `MaksIT.Core` exception message extraction for acquire/release errors.
|
||||||
|
- Updated dependencies: `MaksIT.Core` (`1.6.8`), `MaksIT.Results` (`2.0.3`), `Microsoft.Extensions.DependencyInjection.Abstractions` and `Microsoft.Extensions.Logging.Abstractions` (`11.0.0-preview.5.26302.115`), and `StackExchange.Redis` (`3.0.7`).
|
||||||
|
|
||||||
## [1.0.4] - 2026-06-20
|
## [1.0.4] - 2026-06-20
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="150" height="20" role="img" aria-label="Branch Coverage: 43.6%">
|
<svg xmlns="http://www.w3.org/2000/svg" width="150" height="20" role="img" aria-label="Branch Coverage: 49.1%">
|
||||||
<title>Branch Coverage: 43.6%</title>
|
<title>Branch Coverage: 49.1%</title>
|
||||||
<linearGradient id="s" x2="0" y2="100%">
|
<linearGradient id="s" x2="0" y2="100%">
|
||||||
<stop offset="0" stop-color="#bbb" stop-opacity=".1"/>
|
<stop offset="0" stop-color="#bbb" stop-opacity=".1"/>
|
||||||
<stop offset="1" stop-opacity=".1"/>
|
<stop offset="1" stop-opacity=".1"/>
|
||||||
@ -15,7 +15,7 @@
|
|||||||
<g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="11">
|
<g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="11">
|
||||||
<text aria-hidden="true" x="53.75" y="15" fill="#010101" fill-opacity=".3">Branch Coverage</text>
|
<text aria-hidden="true" x="53.75" y="15" fill="#010101" fill-opacity=".3">Branch Coverage</text>
|
||||||
<text x="53.75" y="14" fill="#fff">Branch Coverage</text>
|
<text x="53.75" y="14" fill="#fff">Branch Coverage</text>
|
||||||
<text aria-hidden="true" x="128.75" y="15" fill="#010101" fill-opacity=".3">43.6%</text>
|
<text aria-hidden="true" x="128.75" y="15" fill="#010101" fill-opacity=".3">49.1%</text>
|
||||||
<text x="128.75" y="14" fill="#fff">43.6%</text>
|
<text x="128.75" y="14" fill="#fff">49.1%</text>
|
||||||
</g>
|
</g>
|
||||||
</svg>
|
</svg>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 1.0 KiB |
@ -1,21 +1,21 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="134.5" height="20" role="img" aria-label="Line Coverage: 56%">
|
<svg xmlns="http://www.w3.org/2000/svg" width="137" height="20" role="img" aria-label="Line Coverage: 57.1%">
|
||||||
<title>Line Coverage: 56%</title>
|
<title>Line Coverage: 57.1%</title>
|
||||||
<linearGradient id="s" x2="0" y2="100%">
|
<linearGradient id="s" x2="0" y2="100%">
|
||||||
<stop offset="0" stop-color="#bbb" stop-opacity=".1"/>
|
<stop offset="0" stop-color="#bbb" stop-opacity=".1"/>
|
||||||
<stop offset="1" stop-opacity=".1"/>
|
<stop offset="1" stop-opacity=".1"/>
|
||||||
</linearGradient>
|
</linearGradient>
|
||||||
<clipPath id="r">
|
<clipPath id="r">
|
||||||
<rect width="134.5" height="20" rx="3" fill="#fff"/>
|
<rect width="137" height="20" rx="3" fill="#fff"/>
|
||||||
</clipPath>
|
</clipPath>
|
||||||
<g clip-path="url(#r)">
|
<g clip-path="url(#r)">
|
||||||
<rect width="94.5" height="20" fill="#555"/>
|
<rect width="94.5" height="20" fill="#555"/>
|
||||||
<rect x="94.5" width="40" height="20" fill="#a4a61d"/>
|
<rect x="94.5" width="42.5" height="20" fill="#a4a61d"/>
|
||||||
<rect width="134.5" height="20" fill="url(#s)"/>
|
<rect width="137" height="20" fill="url(#s)"/>
|
||||||
</g>
|
</g>
|
||||||
<g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="11">
|
<g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="11">
|
||||||
<text aria-hidden="true" x="47.25" y="15" fill="#010101" fill-opacity=".3">Line Coverage</text>
|
<text aria-hidden="true" x="47.25" y="15" fill="#010101" fill-opacity=".3">Line Coverage</text>
|
||||||
<text x="47.25" y="14" fill="#fff">Line Coverage</text>
|
<text x="47.25" y="14" fill="#fff">Line Coverage</text>
|
||||||
<text aria-hidden="true" x="114.5" y="15" fill="#010101" fill-opacity=".3">56%</text>
|
<text aria-hidden="true" x="115.75" y="15" fill="#010101" fill-opacity=".3">57.1%</text>
|
||||||
<text x="114.5" y="14" fill="#fff">56%</text>
|
<text x="115.75" y="14" fill="#fff">57.1%</text>
|
||||||
</g>
|
</g>
|
||||||
</svg>
|
</svg>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 1.0 KiB |
@ -1,5 +1,5 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="150" height="20" role="img" aria-label="Method Coverage: 76.1%">
|
<svg xmlns="http://www.w3.org/2000/svg" width="150" height="20" role="img" aria-label="Method Coverage: 70.7%">
|
||||||
<title>Method Coverage: 76.1%</title>
|
<title>Method Coverage: 70.7%</title>
|
||||||
<linearGradient id="s" x2="0" y2="100%">
|
<linearGradient id="s" x2="0" y2="100%">
|
||||||
<stop offset="0" stop-color="#bbb" stop-opacity=".1"/>
|
<stop offset="0" stop-color="#bbb" stop-opacity=".1"/>
|
||||||
<stop offset="1" stop-opacity=".1"/>
|
<stop offset="1" stop-opacity=".1"/>
|
||||||
@ -15,7 +15,7 @@
|
|||||||
<g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="11">
|
<g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="11">
|
||||||
<text aria-hidden="true" x="53.75" y="15" fill="#010101" fill-opacity=".3">Method Coverage</text>
|
<text aria-hidden="true" x="53.75" y="15" fill="#010101" fill-opacity=".3">Method Coverage</text>
|
||||||
<text x="53.75" y="14" fill="#fff">Method Coverage</text>
|
<text x="53.75" y="14" fill="#fff">Method Coverage</text>
|
||||||
<text aria-hidden="true" x="128.75" y="15" fill="#010101" fill-opacity=".3">76.1%</text>
|
<text aria-hidden="true" x="128.75" y="15" fill="#010101" fill-opacity=".3">70.7%</text>
|
||||||
<text x="128.75" y="14" fill="#fff">76.1%</text>
|
<text x="128.75" y="14" fill="#fff">70.7%</text>
|
||||||
</g>
|
</g>
|
||||||
</svg>
|
</svg>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 1.0 KiB |
@ -13,9 +13,9 @@
|
|||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.8" />
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="11.0.0-preview.5.26302.115" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Logging" Version="10.0.8" />
|
<PackageReference Include="Microsoft.Extensions.Logging" Version="11.0.0-preview.5.26302.115" />
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.6.0" />
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.7.0" />
|
||||||
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.5">
|
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.5">
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
|
|||||||
112
src/MaksIT.HAMode/Abstractions/LeaseInputValidation.cs
Normal file
112
src/MaksIT.HAMode/Abstractions/LeaseInputValidation.cs
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
using MaksIT.Results;
|
||||||
|
|
||||||
|
namespace MaksIT.HAMode.Abstractions;
|
||||||
|
|
||||||
|
internal static class LeaseInputValidation {
|
||||||
|
internal static Result<bool>? ValidateAcquireInputs(string leaseName, string holderId, TimeSpan ttl) {
|
||||||
|
if (string.IsNullOrWhiteSpace(leaseName))
|
||||||
|
return Result<bool>.BadRequest(false, "leaseName is required.");
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(holderId))
|
||||||
|
return Result<bool>.BadRequest(false, "holderId is required.");
|
||||||
|
|
||||||
|
if (ttl <= TimeSpan.Zero)
|
||||||
|
return Result<bool>.BadRequest(false, "ttl must be positive.");
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static Result? ValidateReleaseInputs(string leaseName, string holderId) {
|
||||||
|
if (string.IsNullOrWhiteSpace(leaseName))
|
||||||
|
return Result.BadRequest("leaseName is required.");
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(holderId))
|
||||||
|
return Result.BadRequest("holderId is required.");
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static Result<bool>? ValidatePostgreSqlProvider(
|
||||||
|
IRuntimeLeaseConnectionStringProvider connectionStringProvider,
|
||||||
|
bool hasSharedDataSource
|
||||||
|
) {
|
||||||
|
if (!hasSharedDataSource && string.IsNullOrWhiteSpace(connectionStringProvider.ConnectionString))
|
||||||
|
return Result<bool>.BadRequest(false, "connection string is required.");
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(connectionStringProvider.Schema))
|
||||||
|
return Result<bool>.BadRequest(false, "schema is required.");
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(connectionStringProvider.Table))
|
||||||
|
return Result<bool>.BadRequest(false, "table is required.");
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static Result? ValidatePostgreSqlProviderForRelease(
|
||||||
|
IRuntimeLeaseConnectionStringProvider connectionStringProvider,
|
||||||
|
bool hasSharedDataSource
|
||||||
|
) {
|
||||||
|
if (!hasSharedDataSource && string.IsNullOrWhiteSpace(connectionStringProvider.ConnectionString))
|
||||||
|
return Result.BadRequest("connection string is required.");
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(connectionStringProvider.Schema))
|
||||||
|
return Result.BadRequest("schema is required.");
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(connectionStringProvider.Table))
|
||||||
|
return Result.BadRequest("table is required.");
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static Result<bool>? ValidateRedisProvider(
|
||||||
|
IRuntimeLeaseRedisConnectionProvider connectionProvider,
|
||||||
|
bool hasSharedMultiplexer
|
||||||
|
) {
|
||||||
|
if (!hasSharedMultiplexer && string.IsNullOrWhiteSpace(connectionProvider.Configuration))
|
||||||
|
return Result<bool>.BadRequest(false, "redis configuration is required.");
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(connectionProvider.KeyPrefix))
|
||||||
|
return Result<bool>.BadRequest(false, "redis key prefix is required.");
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static Result? ValidateRedisProviderForRelease(
|
||||||
|
IRuntimeLeaseRedisConnectionProvider connectionProvider,
|
||||||
|
bool hasSharedMultiplexer
|
||||||
|
) {
|
||||||
|
if (!hasSharedMultiplexer && string.IsNullOrWhiteSpace(connectionProvider.Configuration))
|
||||||
|
return Result.BadRequest("redis configuration is required.");
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(connectionProvider.KeyPrefix))
|
||||||
|
return Result.BadRequest("redis key prefix is required.");
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static Result<bool>? ValidateEtcdProvider(
|
||||||
|
IRuntimeLeaseEtcdConnectionProvider connectionProvider,
|
||||||
|
bool hasSharedClient
|
||||||
|
) {
|
||||||
|
if (!hasSharedClient && string.IsNullOrWhiteSpace(connectionProvider.Endpoints))
|
||||||
|
return Result<bool>.BadRequest(false, "etcd endpoints are required.");
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(connectionProvider.KeyPrefix))
|
||||||
|
return Result<bool>.BadRequest(false, "etcd key prefix is required.");
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static Result? ValidateEtcdProviderForRelease(
|
||||||
|
IRuntimeLeaseEtcdConnectionProvider connectionProvider,
|
||||||
|
bool hasSharedClient
|
||||||
|
) {
|
||||||
|
if (!hasSharedClient && string.IsNullOrWhiteSpace(connectionProvider.Endpoints))
|
||||||
|
return Result.BadRequest("etcd endpoints are required.");
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(connectionProvider.KeyPrefix))
|
||||||
|
return Result.BadRequest("etcd key prefix is required.");
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
24
src/MaksIT.HAMode/Abstractions/LeaseResultErrors.cs
Normal file
24
src/MaksIT.HAMode/Abstractions/LeaseResultErrors.cs
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
using MaksIT.Core.Extensions;
|
||||||
|
using MaksIT.Results;
|
||||||
|
|
||||||
|
namespace MaksIT.HAMode.Abstractions;
|
||||||
|
|
||||||
|
internal static class LeaseResultErrors {
|
||||||
|
internal static Result<bool> AcquireFailed(Exception exception) =>
|
||||||
|
Result<bool>.InternalServerError(false, ["Lease acquire failed.", .. exception.ExtractMessages()]);
|
||||||
|
|
||||||
|
internal static Result ReleaseFailed(Exception exception) =>
|
||||||
|
Result.InternalServerError(["Lease release failed.", .. exception.ExtractMessages()]);
|
||||||
|
|
||||||
|
internal static Result<bool> AcquireTableMissing(string qualifiedTableName) =>
|
||||||
|
Result<bool>.InternalServerError(false, [
|
||||||
|
$"Lease table '{qualifiedTableName}' was not found.",
|
||||||
|
"Create the table or set Schema/Table in the PostgreSQL connection provider."
|
||||||
|
]);
|
||||||
|
|
||||||
|
internal static Result ReleaseTableMissing(string qualifiedTableName) =>
|
||||||
|
Result.InternalServerError([
|
||||||
|
$"Lease table '{qualifiedTableName}' was not found.",
|
||||||
|
"Create the table or set Schema/Table in the PostgreSQL connection provider."
|
||||||
|
]);
|
||||||
|
}
|
||||||
@ -22,20 +22,11 @@ public sealed class RuntimeLeaseServiceEtcd(
|
|||||||
: new EtcdClient(connectionProvider.Endpoints)));
|
: new EtcdClient(connectionProvider.Endpoints)));
|
||||||
|
|
||||||
public async Task<Result<bool>> TryAcquireAsync(string leaseName, string holderId, TimeSpan ttl, CancellationToken cancellationToken = default) {
|
public async Task<Result<bool>> TryAcquireAsync(string leaseName, string holderId, TimeSpan ttl, CancellationToken cancellationToken = default) {
|
||||||
if (string.IsNullOrWhiteSpace(leaseName))
|
if (LeaseInputValidation.ValidateAcquireInputs(leaseName, holderId, ttl) is { } acquireValidation)
|
||||||
|
return acquireValidation;
|
||||||
|
|
||||||
return Result<bool>.BadRequest(false, "leaseName is required.");
|
if (LeaseInputValidation.ValidateEtcdProvider(connectionProvider, sharedClient is not null) is { } providerValidation)
|
||||||
if (string.IsNullOrWhiteSpace(holderId))
|
return providerValidation;
|
||||||
return Result<bool>.BadRequest(false, "holderId is required.");
|
|
||||||
|
|
||||||
if (ttl <= TimeSpan.Zero)
|
|
||||||
return Result<bool>.BadRequest(false, "ttl must be positive.");
|
|
||||||
|
|
||||||
if (sharedClient is null && string.IsNullOrWhiteSpace(connectionProvider.Endpoints))
|
|
||||||
return Result<bool>.BadRequest(false, "etcd endpoints are required.");
|
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(connectionProvider.KeyPrefix))
|
|
||||||
return Result<bool>.BadRequest(false, "etcd key prefix is required.");
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
var key = BuildKey(leaseName);
|
var key = BuildKey(leaseName);
|
||||||
@ -90,22 +81,16 @@ public sealed class RuntimeLeaseServiceEtcd(
|
|||||||
}
|
}
|
||||||
catch (Exception ex) {
|
catch (Exception ex) {
|
||||||
logger.LogError(ex, "etcd TryAcquire lease failed for {LeaseName}", leaseName);
|
logger.LogError(ex, "etcd TryAcquire lease failed for {LeaseName}", leaseName);
|
||||||
return Result<bool>.InternalServerError(false, ["Lease acquire failed.", ex.Message]);
|
return LeaseResultErrors.AcquireFailed(ex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<Result> ReleaseAsync(string leaseName, string holderId, CancellationToken cancellationToken = default) {
|
public async Task<Result> ReleaseAsync(string leaseName, string holderId, CancellationToken cancellationToken = default) {
|
||||||
if (string.IsNullOrWhiteSpace(leaseName))
|
if (LeaseInputValidation.ValidateReleaseInputs(leaseName, holderId) is { } releaseValidation)
|
||||||
return Result.BadRequest("leaseName is required.");
|
return releaseValidation;
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(holderId))
|
if (LeaseInputValidation.ValidateEtcdProviderForRelease(connectionProvider, sharedClient is not null) is { } providerValidation)
|
||||||
return Result.BadRequest("holderId is required.");
|
return providerValidation;
|
||||||
|
|
||||||
if (sharedClient is null && string.IsNullOrWhiteSpace(connectionProvider.Endpoints))
|
|
||||||
return Result.BadRequest("etcd endpoints are required.");
|
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(connectionProvider.KeyPrefix))
|
|
||||||
return Result.BadRequest("etcd key prefix is required.");
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
var keyBytes = ByteString.CopyFromUtf8(BuildKey(leaseName));
|
var keyBytes = ByteString.CopyFromUtf8(BuildKey(leaseName));
|
||||||
@ -130,7 +115,7 @@ public sealed class RuntimeLeaseServiceEtcd(
|
|||||||
}
|
}
|
||||||
catch (Exception ex) {
|
catch (Exception ex) {
|
||||||
logger.LogWarning(ex, "etcd Release lease failed for {LeaseName} (ignored).", leaseName);
|
logger.LogWarning(ex, "etcd Release lease failed for {LeaseName} (ignored).", leaseName);
|
||||||
return Result.InternalServerError(["Lease release failed.", ex.Message]);
|
return LeaseResultErrors.ReleaseFailed(ex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -8,7 +8,7 @@
|
|||||||
<NoWarn>$(NoWarn);CS1591</NoWarn>
|
<NoWarn>$(NoWarn);CS1591</NoWarn>
|
||||||
|
|
||||||
<PackageId>MaksIT.HAMode</PackageId>
|
<PackageId>MaksIT.HAMode</PackageId>
|
||||||
<Version>1.0.4</Version>
|
<Version>1.0.5</Version>
|
||||||
<Authors>Maksym Sadovnychyy</Authors>
|
<Authors>Maksym Sadovnychyy</Authors>
|
||||||
<Company>MAKS-IT</Company>
|
<Company>MAKS-IT</Company>
|
||||||
<Product>MaksIT.HAMode</Product>
|
<Product>MaksIT.HAMode</Product>
|
||||||
@ -33,11 +33,12 @@
|
|||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="dotnet-etcd" Version="8.1.0" />
|
<PackageReference Include="dotnet-etcd" Version="8.1.0" />
|
||||||
<PackageReference Include="Google.Protobuf" Version="3.35.1" />
|
<PackageReference Include="Google.Protobuf" Version="3.35.1" />
|
||||||
<PackageReference Include="MaksIT.Results" Version="2.0.2" />
|
<PackageReference Include="MaksIT.Core" Version="1.6.8" />
|
||||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.8" />
|
<PackageReference Include="MaksIT.Results" Version="2.0.3" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.8" />
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="11.0.0-preview.5.26302.115" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="11.0.0-preview.5.26302.115" />
|
||||||
<PackageReference Include="Npgsql" Version="10.0.3" />
|
<PackageReference Include="Npgsql" Version="10.0.3" />
|
||||||
<PackageReference Include="StackExchange.Redis" Version="2.8.58" />
|
<PackageReference Include="StackExchange.Redis" Version="3.0.7" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@ -14,22 +14,11 @@ public sealed class RuntimeLeaseServiceNpgsql(
|
|||||||
NpgsqlDataSource? dataSource = null
|
NpgsqlDataSource? dataSource = null
|
||||||
) : IRuntimeLeaseService {
|
) : IRuntimeLeaseService {
|
||||||
public async Task<Result<bool>> TryAcquireAsync(string leaseName, string holderId, TimeSpan ttl, CancellationToken cancellationToken = default) {
|
public async Task<Result<bool>> TryAcquireAsync(string leaseName, string holderId, TimeSpan ttl, CancellationToken cancellationToken = default) {
|
||||||
if (string.IsNullOrWhiteSpace(leaseName))
|
if (LeaseInputValidation.ValidateAcquireInputs(leaseName, holderId, ttl) is { } acquireValidation)
|
||||||
return Result<bool>.BadRequest(false, "leaseName is required.");
|
return acquireValidation;
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(holderId))
|
if (LeaseInputValidation.ValidatePostgreSqlProvider(connectionStringProvider, dataSource is not null) is { } providerValidation)
|
||||||
return Result<bool>.BadRequest(false, "holderId is required.");
|
return providerValidation;
|
||||||
|
|
||||||
if (ttl <= TimeSpan.Zero)
|
|
||||||
return Result<bool>.BadRequest(false, "ttl must be positive.");
|
|
||||||
if (dataSource is null && string.IsNullOrWhiteSpace(connectionStringProvider.ConnectionString))
|
|
||||||
return Result<bool>.BadRequest(false, "connection string is required.");
|
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(connectionStringProvider.Schema))
|
|
||||||
return Result<bool>.BadRequest(false, "schema is required.");
|
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(connectionStringProvider.Table))
|
|
||||||
return Result<bool>.BadRequest(false, "table is required.");
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await using var conn = await OpenConnectionAsync(cancellationToken).ConfigureAwait(false);
|
await using var conn = await OpenConnectionAsync(cancellationToken).ConfigureAwait(false);
|
||||||
@ -68,32 +57,20 @@ public sealed class RuntimeLeaseServiceNpgsql(
|
|||||||
catch (PostgresException ex) when (ex.SqlState == PostgresErrorCodes.UndefinedTable) {
|
catch (PostgresException ex) when (ex.SqlState == PostgresErrorCodes.UndefinedTable) {
|
||||||
var qualifiedTableName = $"{connectionStringProvider.Schema}.{connectionStringProvider.Table}";
|
var qualifiedTableName = $"{connectionStringProvider.Schema}.{connectionStringProvider.Table}";
|
||||||
logger.LogError(ex, "Lease table {TableName} was not found while acquiring lease {LeaseName}", qualifiedTableName, leaseName);
|
logger.LogError(ex, "Lease table {TableName} was not found while acquiring lease {LeaseName}", qualifiedTableName, leaseName);
|
||||||
return Result<bool>.InternalServerError(false, [
|
return LeaseResultErrors.AcquireTableMissing(qualifiedTableName);
|
||||||
$"Lease table '{qualifiedTableName}' was not found.",
|
|
||||||
"Create the table or set Schema/Table in the PostgreSQL connection provider."
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
catch (Exception ex) {
|
catch (Exception ex) {
|
||||||
logger.LogError(ex, "TryAcquire lease failed for {LeaseName}", leaseName);
|
logger.LogError(ex, "TryAcquire lease failed for {LeaseName}", leaseName);
|
||||||
return Result<bool>.InternalServerError(false, ["Lease acquire failed.", ex.Message]);
|
return LeaseResultErrors.AcquireFailed(ex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<Result> ReleaseAsync(string leaseName, string holderId, CancellationToken cancellationToken = default) {
|
public async Task<Result> ReleaseAsync(string leaseName, string holderId, CancellationToken cancellationToken = default) {
|
||||||
if (string.IsNullOrWhiteSpace(leaseName))
|
if (LeaseInputValidation.ValidateReleaseInputs(leaseName, holderId) is { } releaseValidation)
|
||||||
return Result.BadRequest("leaseName is required.");
|
return releaseValidation;
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(holderId))
|
if (LeaseInputValidation.ValidatePostgreSqlProviderForRelease(connectionStringProvider, dataSource is not null) is { } providerValidation)
|
||||||
return Result.BadRequest("holderId is required.");
|
return providerValidation;
|
||||||
|
|
||||||
if (dataSource is null && string.IsNullOrWhiteSpace(connectionStringProvider.ConnectionString))
|
|
||||||
return Result.BadRequest("connection string is required.");
|
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(connectionStringProvider.Schema))
|
|
||||||
return Result.BadRequest("schema is required.");
|
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(connectionStringProvider.Table))
|
|
||||||
return Result.BadRequest("table is required.");
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await using var conn = await OpenConnectionAsync(cancellationToken).ConfigureAwait(false);
|
await using var conn = await OpenConnectionAsync(cancellationToken).ConfigureAwait(false);
|
||||||
@ -114,14 +91,11 @@ public sealed class RuntimeLeaseServiceNpgsql(
|
|||||||
catch (PostgresException ex) when (ex.SqlState == PostgresErrorCodes.UndefinedTable) {
|
catch (PostgresException ex) when (ex.SqlState == PostgresErrorCodes.UndefinedTable) {
|
||||||
var qualifiedTableName = $"{connectionStringProvider.Schema}.{connectionStringProvider.Table}";
|
var qualifiedTableName = $"{connectionStringProvider.Schema}.{connectionStringProvider.Table}";
|
||||||
logger.LogWarning(ex, "Lease table {TableName} was not found while releasing lease {LeaseName}", qualifiedTableName, leaseName);
|
logger.LogWarning(ex, "Lease table {TableName} was not found while releasing lease {LeaseName}", qualifiedTableName, leaseName);
|
||||||
return Result.InternalServerError([
|
return LeaseResultErrors.ReleaseTableMissing(qualifiedTableName);
|
||||||
$"Lease table '{qualifiedTableName}' was not found.",
|
|
||||||
"Create the table or set Schema/Table in the PostgreSQL connection provider."
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
catch (Exception ex) {
|
catch (Exception ex) {
|
||||||
logger.LogWarning(ex, "Release lease failed for {LeaseName} (ignored).", leaseName);
|
logger.LogWarning(ex, "Release lease failed for {LeaseName} (ignored).", leaseName);
|
||||||
return Result.InternalServerError(["Lease release failed.", ex.Message]);
|
return LeaseResultErrors.ReleaseFailed(ex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -37,20 +37,11 @@ public sealed class RuntimeLeaseServiceRedis(
|
|||||||
private ConnectionMultiplexer? _ownedMultiplexer;
|
private ConnectionMultiplexer? _ownedMultiplexer;
|
||||||
|
|
||||||
public async Task<Result<bool>> TryAcquireAsync(string leaseName, string holderId, TimeSpan ttl, CancellationToken cancellationToken = default) {
|
public async Task<Result<bool>> TryAcquireAsync(string leaseName, string holderId, TimeSpan ttl, CancellationToken cancellationToken = default) {
|
||||||
if (string.IsNullOrWhiteSpace(leaseName))
|
if (LeaseInputValidation.ValidateAcquireInputs(leaseName, holderId, ttl) is { } acquireValidation)
|
||||||
return Result<bool>.BadRequest(false, "leaseName is required.");
|
return acquireValidation;
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(holderId))
|
if (LeaseInputValidation.ValidateRedisProvider(connectionProvider, sharedMultiplexer is not null) is { } providerValidation)
|
||||||
return Result<bool>.BadRequest(false, "holderId is required.");
|
return providerValidation;
|
||||||
|
|
||||||
if (ttl <= TimeSpan.Zero)
|
|
||||||
return Result<bool>.BadRequest(false, "ttl must be positive.");
|
|
||||||
|
|
||||||
if (sharedMultiplexer is null && string.IsNullOrWhiteSpace(connectionProvider.Configuration))
|
|
||||||
return Result<bool>.BadRequest(false, "redis configuration is required.");
|
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(connectionProvider.KeyPrefix))
|
|
||||||
return Result<bool>.BadRequest(false, "redis key prefix is required.");
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
var db = (await GetDatabaseAsync(cancellationToken).ConfigureAwait(false)).Database;
|
var db = (await GetDatabaseAsync(cancellationToken).ConfigureAwait(false)).Database;
|
||||||
@ -64,22 +55,16 @@ public sealed class RuntimeLeaseServiceRedis(
|
|||||||
}
|
}
|
||||||
catch (Exception ex) {
|
catch (Exception ex) {
|
||||||
logger.LogError(ex, "Redis TryAcquire lease failed for {LeaseName}", leaseName);
|
logger.LogError(ex, "Redis TryAcquire lease failed for {LeaseName}", leaseName);
|
||||||
return Result<bool>.InternalServerError(false, ["Lease acquire failed.", ex.Message]);
|
return LeaseResultErrors.AcquireFailed(ex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<Result> ReleaseAsync(string leaseName, string holderId, CancellationToken cancellationToken = default) {
|
public async Task<Result> ReleaseAsync(string leaseName, string holderId, CancellationToken cancellationToken = default) {
|
||||||
if (string.IsNullOrWhiteSpace(leaseName))
|
if (LeaseInputValidation.ValidateReleaseInputs(leaseName, holderId) is { } releaseValidation)
|
||||||
return Result.BadRequest("leaseName is required.");
|
return releaseValidation;
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(holderId))
|
if (LeaseInputValidation.ValidateRedisProviderForRelease(connectionProvider, sharedMultiplexer is not null) is { } providerValidation)
|
||||||
return Result.BadRequest("holderId is required.");
|
return providerValidation;
|
||||||
|
|
||||||
if (sharedMultiplexer is null && string.IsNullOrWhiteSpace(connectionProvider.Configuration))
|
|
||||||
return Result.BadRequest("redis configuration is required.");
|
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(connectionProvider.KeyPrefix))
|
|
||||||
return Result.BadRequest("redis key prefix is required.");
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
var db = (await GetDatabaseAsync(cancellationToken).ConfigureAwait(false)).Database;
|
var db = (await GetDatabaseAsync(cancellationToken).ConfigureAwait(false)).Database;
|
||||||
@ -89,7 +74,7 @@ public sealed class RuntimeLeaseServiceRedis(
|
|||||||
}
|
}
|
||||||
catch (Exception ex) {
|
catch (Exception ex) {
|
||||||
logger.LogWarning(ex, "Redis Release lease failed for {LeaseName} (ignored).", leaseName);
|
logger.LogWarning(ex, "Redis Release lease failed for {LeaseName} (ignored).", leaseName);
|
||||||
return Result.InternalServerError(["Lease release failed.", ex.Message]);
|
return LeaseResultErrors.ReleaseFailed(ex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user