From 2b250b56fb26fb581283da97a3ba73f1e323ef9f Mon Sep 17 00:00:00 2001 From: Mercurial Date: Sun, 21 Jun 2026 00:32:31 +0800 Subject: [PATCH 1/3] chore(deps): refresh NuGet dependencies to latest within net10 Bump Microsoft.Extensions.* and the EF Core trio to 10.0.9, Npgsql.EFCore to 10.0.2, AspNetCore.OpenApi to 10.0.9, Swashbuckle to 10.2.2, Spectre.Console to 0.57.0, Test.Sdk to 18.6.0, coverlet.collector to 10.0.1. All within the net10 line; xunit stays on 2.x (no v3 migration). Chrysalis and Utxorpc alpha pins and the already-latest Mongo/bench packages unchanged. Co-Authored-By: Claude Opus 4.8 --- Directory.Packages.props | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index a764182..020c794 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -9,26 +9,26 @@ - - + + - - - - + + + + - - - + + + - + - + From e072e4f263db5720d7feaac3d1fbad02c849da0c Mon Sep 17 00:00:00 2001 From: Mercurial Date: Sun, 21 Jun 2026 00:48:46 +0800 Subject: [PATCH 2/3] refactor(ef): split EntityFramework into a provider-neutral core + Postgres package MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Argus.Sync.EntityFramework becomes the provider-neutral EF Core backend — CardanoDbContext, the EF unit-of-work, and a new public AddCardanoEntityFrameworkIndexer(configureProvider) seam — and now depends on Microsoft.EntityFrameworkCore.Relational instead of Npgsql. The PostgreSQL/Npgsql specifics move to a new Argus.Sync.EntityFramework.Postgres package: AddCardanoPostgresIndexer (a thin UseNpgsql wrapper over the core seam) plus the advisory-lock worker, renamed PostgresSingleInstanceLockWorker -> PostgresSingleInstanceLock to match the sibling MongoSingleInstanceLock. This mirrors the EF Core ecosystem (core + per-provider packages) and makes adding SQLite/SQL Server later a sibling wrapper. Implements the structural half of #178. BREAKING: AddCardanoPostgresIndexer consumers must add the Argus.Sync.EntityFramework.Postgres package and a `using Argus.Sync.EntityFramework.Postgres;`. Also fixes the commandTimeout typo, makes the core docs provider-neutral, and wires the new package into the Example, Tests, release.yml, ci.yml, and README. Co-Authored-By: Claude Opus 4.8 --- .github/workflows/ci.yml | 2 +- .github/workflows/release.yml | 1 + README.md | 27 +++---- ...Argus.Sync.EntityFramework.Postgres.csproj | 16 +++++ .../PostgresServiceCollectionExtensions.cs | 71 +++++++++++++++++++ .../PostgresSingleInstanceLock.cs} | 10 +-- .../Argus.Sync.EntityFramework.csproj | 4 +- .../CardanoDbContext.cs | 2 +- .../EfBlockUnitOfWorkFactory.cs | 3 +- .../EfServiceCollectionExtensions.cs | 65 ++++++----------- .../Argus.Sync.Example.csproj | 1 + src/Argus.Sync.Example/Program.cs | 2 +- src/Argus.Sync.Tests/Argus.Sync.Tests.csproj | 1 + .../EndToEnd/IndexerRegistrationTest.cs | 3 +- .../EndToEnd/SingleInstanceLockTest.cs | 12 ++-- 15 files changed, 146 insertions(+), 74 deletions(-) create mode 100644 src/Argus.Sync.EntityFramework.Postgres/Argus.Sync.EntityFramework.Postgres.csproj create mode 100644 src/Argus.Sync.EntityFramework.Postgres/PostgresServiceCollectionExtensions.cs rename src/{Argus.Sync.EntityFramework/PostgresSingleInstanceLockWorker.cs => Argus.Sync.EntityFramework.Postgres/PostgresSingleInstanceLock.cs} (96%) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6cf766c..e5a57eb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -64,7 +64,7 @@ jobs: if: steps.changes.outputs.code == 'true' run: | mkdir -p out - for proj in src/Argus.Sync/Argus.Sync.csproj src/Argus.Sync.EntityFramework/Argus.Sync.EntityFramework.csproj src/Argus.Sync.MongoDb/Argus.Sync.MongoDb.csproj; do + for proj in src/Argus.Sync/Argus.Sync.csproj src/Argus.Sync.EntityFramework/Argus.Sync.EntityFramework.csproj src/Argus.Sync.EntityFramework.Postgres/Argus.Sync.EntityFramework.Postgres.csproj src/Argus.Sync.MongoDb/Argus.Sync.MongoDb.csproj; do echo "Packing $proj..." dotnet pack "$proj" -c Release -o out done diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5adeb7c..9a58681 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -42,6 +42,7 @@ jobs: V="${{ steps.version.outputs.version }}" dotnet pack src/Argus.Sync/Argus.Sync.csproj -c Release -o out -p:Version="$V" dotnet pack src/Argus.Sync.EntityFramework/Argus.Sync.EntityFramework.csproj -c Release -o out -p:Version="$V" + dotnet pack src/Argus.Sync.EntityFramework.Postgres/Argus.Sync.EntityFramework.Postgres.csproj -c Release -o out -p:Version="$V" dotnet pack src/Argus.Sync.MongoDb/Argus.Sync.MongoDb.csproj -c Release -o out -p:Version="$V" - name: Push to NuGet diff --git a/README.md b/README.md index eafda80..a09ae8b 100644 --- a/README.md +++ b/README.md @@ -82,9 +82,8 @@ Argus is a .NET library that turns the Cardano blockchain into structured, query dotnet add package Argus.Sync # PostgreSQL backend (Entity Framework Core) + EF tooling for migrations -dotnet add package Argus.Sync.EntityFramework +dotnet add package Argus.Sync.EntityFramework.Postgres # pulls in the provider-neutral Argus.Sync.EntityFramework core + Npgsql dotnet add package Microsoft.EntityFrameworkCore.Design -dotnet add package Npgsql.EntityFrameworkCore.PostgreSQL # Or, instead: MongoDB backend dotnet add package Argus.Sync.MongoDb @@ -201,8 +200,8 @@ public class BlockReducer : IReducer ### 6️⃣ Register services ```csharp -using Argus.Sync.EntityFramework; // AddCardanoPostgresIndexer + CardanoDbContext -using Argus.Sync.Extensions; // AddReducers +using Argus.Sync.EntityFramework.Postgres; // AddCardanoPostgresIndexer +using Argus.Sync.Extensions; // AddReducers WebApplicationBuilder builder = WebApplication.CreateBuilder(args); @@ -285,9 +284,11 @@ A backend is one implementation of `IBlockUnitOfWorkFactory` (create a batched t ### PostgreSQL (Entity Framework Core) -Add the `Argus.Sync.EntityFramework` package. Your `CardanoDbContext`-derived context *is* the storage handle: +Add the `Argus.Sync.EntityFramework.Postgres` package (it brings in the provider-neutral `Argus.Sync.EntityFramework` core). Your `CardanoDbContext`-derived context *is* the storage handle: ```csharp +using Argus.Sync.EntityFramework.Postgres; + builder.Services.AddCardanoPostgresIndexer(builder.Configuration); builder.Services.AddReducers(builder.Configuration); ``` @@ -402,9 +403,10 @@ dotnet test dotnet test --filter "Category!=Integration" # Pack the NuGet packages -dotnet pack src/Argus.Sync --configuration Release -dotnet pack src/Argus.Sync.EntityFramework --configuration Release -dotnet pack src/Argus.Sync.MongoDb --configuration Release +dotnet pack src/Argus.Sync --configuration Release +dotnet pack src/Argus.Sync.EntityFramework --configuration Release +dotnet pack src/Argus.Sync.EntityFramework.Postgres --configuration Release +dotnet pack src/Argus.Sync.MongoDb --configuration Release ``` Integration tests run against a real preprod/preview node and a local PostgreSQL (and, for the Mongo suite, a MongoDB replica set); they self-skip when those aren't reachable. The end-to-end suite under `src/Argus.Sync.Tests/EndToEnd` exercises the worker, the dependency graph, whole-graph atomicity, batch commits, crash-recovery, N2N pipelining, the single-instance lock, and both storage backends against real Conway-era blocks. @@ -414,7 +416,8 @@ Integration tests run against a real preprod/preview node and a local PostgreSQL | Project | Purpose | | ------- | ------- | | `src/Argus.Sync` | Core library (storage-agnostic): worker, reducer graph, unit-of-work seam, chain providers. | -| `src/Argus.Sync.EntityFramework` | PostgreSQL / EF Core backend (`AddCardanoPostgresIndexer`, `CardanoDbContext`). | +| `src/Argus.Sync.EntityFramework` | Provider-neutral EF Core backend (`CardanoDbContext`, EF unit-of-work, the `AddCardanoEntityFrameworkIndexer` seam). | +| `src/Argus.Sync.EntityFramework.Postgres` | PostgreSQL / Npgsql provider (`AddCardanoPostgresIndexer`, advisory single-instance lock). | | `src/Argus.Sync.MongoDb` | MongoDB storage backend (`AddCardanoMongoIndexer`). | | `src/Argus.Sync.Example` | Runnable reference app with example models and reducers. | | `src/Argus.Sync.Tests` | Unit + end-to-end tests. | @@ -434,15 +437,15 @@ The rearchitecture — channel pipeline, storage-agnostic unit of work, and the | `RollBackwardAsync` | `RollBackwardAsync(ulong slot)` | `RollBackwardAsync(ulong slot, IBlockUnitOfWork uow, CancellationToken ct)` | | Block type | `Block` (`Chrysalis.Cbor.Types…`) | `IBlock` (`Chrysalis.Codec.Types.Cardano.Core`) | | Data access | inject `IDbContextFactory`; call `db.SaveChangesAsync()` | `uow.GetStorage()`; the framework commits — **never** call `SaveChangesAsync` | -| Postgres registration | `AddCardanoIndexer()` (core package) | `AddCardanoPostgresIndexer()` from the **`Argus.Sync.EntityFramework`** package | +| Postgres registration | `AddCardanoIndexer()` (core package) | `AddCardanoPostgresIndexer()` from the **`Argus.Sync.EntityFramework.Postgres`** package | | Reducer registration | `AddReducers(config)` | `AddReducers(config)` (non-generic) | -| Packages | `Argus.Sync` (EF baked in) | `Argus.Sync` (core) **+** `Argus.Sync.EntityFramework` (Postgres) or `Argus.Sync.MongoDb` | +| Packages | `Argus.Sync` (EF baked in) | `Argus.Sync` (core) **+** `Argus.Sync.EntityFramework.Postgres` or `Argus.Sync.MongoDb` | | `IReducerModel` | marker interface | now requires `ulong Slot { get; }` | | Rollback-mode config | `CardanoIndexReducers:RollbackMode:*` | `Sync:Rollback:*` | | Removed config | `Sync:State:ReducerStateSyncInterval` | gone | | N2N (TCP) provider | not implemented | supported (`ConnectionType: "TCP"`) | -**To upgrade a reducer in practice:** drop the `IDbContextFactory` constructor parameter and the `` on `IReducer`; change both methods to take `(…, IBlockUnitOfWork uow, CancellationToken ct)`; replace `dbContextFactory.CreateDbContext()` with `uow.GetStorage()`; and delete every `SaveChangesAsync` call. Then add the `Argus.Sync.EntityFramework` package reference, and switch `AddCardanoIndexer` → `AddCardanoPostgresIndexer` and `AddReducers` → `AddReducers`. +**To upgrade a reducer in practice:** drop the `IDbContextFactory` constructor parameter and the `` on `IReducer`; change both methods to take `(…, IBlockUnitOfWork uow, CancellationToken ct)`; replace `dbContextFactory.CreateDbContext()` with `uow.GetStorage()`; and delete every `SaveChangesAsync` call. Then add the `Argus.Sync.EntityFramework.Postgres` package reference, and switch `AddCardanoIndexer` → `AddCardanoPostgresIndexer` and `AddReducers` → `AddReducers`. ## 🤝 Contributing diff --git a/src/Argus.Sync.EntityFramework.Postgres/Argus.Sync.EntityFramework.Postgres.csproj b/src/Argus.Sync.EntityFramework.Postgres/Argus.Sync.EntityFramework.Postgres.csproj new file mode 100644 index 0000000..eca568d --- /dev/null +++ b/src/Argus.Sync.EntityFramework.Postgres/Argus.Sync.EntityFramework.Postgres.csproj @@ -0,0 +1,16 @@ + + + + Argus.Sync.EntityFramework.Postgres + clark@saib.dev + SAIB Inc. + PostgreSQL / Npgsql provider for the Argus.Sync Entity Framework Core backend — the AddCardanoPostgresIndexer entry point and its Postgres advisory single-instance lock, layered over the provider-neutral Argus.Sync.EntityFramework core. + https://github.com/SAIB-Inc/Argus + + + + + + + + diff --git a/src/Argus.Sync.EntityFramework.Postgres/PostgresServiceCollectionExtensions.cs b/src/Argus.Sync.EntityFramework.Postgres/PostgresServiceCollectionExtensions.cs new file mode 100644 index 0000000..68e5296 --- /dev/null +++ b/src/Argus.Sync.EntityFramework.Postgres/PostgresServiceCollectionExtensions.cs @@ -0,0 +1,71 @@ +using System.Reflection; +using Argus.Sync.Providers; +using Argus.Sync.Workers; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; + +namespace Argus.Sync.EntityFramework.Postgres; + +/// +/// Dependency-injection entry point for running the Cardano indexer on the PostgreSQL / Npgsql backend. +/// Call with your -derived context, +/// then AddReducers. This is a thin Npgsql wrapper over the provider-neutral +/// : it supplies the +/// UseNpgsql provider and registers a Postgres session-advisory single-instance lock. +/// +public static class PostgresServiceCollectionExtensions +{ + /// + /// Registers the Cardano indexer on the EF Core / PostgreSQL backend: an Npgsql-backed DbContext factory, + /// the EF unit-of-work factory (data + checkpoint storage), a Postgres single-instance advisory lock, the + /// chain-provider factory, and the indexer worker as a hosted service. + /// + /// The database context type inheriting from . + /// The service collection. + /// The application configuration. + /// The database command timeout in seconds. + /// An optional custom chain provider factory; defaults to configuration-based if null. + /// The service collection for method chaining. + public static IServiceCollection AddCardanoPostgresIndexer( + this IServiceCollection services, + IConfiguration configuration, + int commandTimeout = 60, + IChainProviderFactory? chainProviderFactory = null) where T : CardanoDbContext + { + ArgumentNullException.ThrowIfNull(services); + ArgumentNullException.ThrowIfNull(configuration); + + // Postgres single-instance guard (advisory lock): ONE shared singleton, exposed both as + // ISingleInstanceLock (the indexer's gate) and as a hosted service (runs the acquire/hold/release loop). + // The factory indirection keeps both roles on the SAME instance. Opt out via Sync:SingleInstanceLock:Enabled=false. + if (configuration.GetValue("Sync:SingleInstanceLock:Enabled", true)) + { + _ = services.AddSingleton(); + _ = services.AddSingleton(sp => sp.GetRequiredService()); + _ = services.AddHostedService(sp => sp.GetRequiredService()); + } + + return services.AddCardanoEntityFrameworkIndexer( + configuration, + options => + { + Assembly? contextAssembly = typeof(T).Assembly; + + // EnableRetryOnFailure is intentionally NOT configured: EF's retrying execution strategy throws on + // user-initiated transactions not wrapped in CreateExecutionStrategy().ExecuteAsync(...), and the + // per-block-branch transaction spans multiple pipeline tasks (and captures raw ExecuteUpdate/SQL), + // so it cannot be a single retriable delegate. Transient faults are recovered out-of-process via + // fail-fast + restart + checkpoint-resume — see AddCardanoEntityFrameworkIndexer's remarks. + _ = options.UseNpgsql( + configuration.GetConnectionString("CardanoContext"), + x => + { + _ = x.MigrationsAssembly(contextAssembly!.FullName); + _ = x.CommandTimeout(commandTimeout); + _ = x.MigrationsHistoryTable("__EFMigrationsHistory", configuration.GetConnectionString("CardanoContextSchema")); + }); + }, + chainProviderFactory); + } +} diff --git a/src/Argus.Sync.EntityFramework/PostgresSingleInstanceLockWorker.cs b/src/Argus.Sync.EntityFramework.Postgres/PostgresSingleInstanceLock.cs similarity index 96% rename from src/Argus.Sync.EntityFramework/PostgresSingleInstanceLockWorker.cs rename to src/Argus.Sync.EntityFramework.Postgres/PostgresSingleInstanceLock.cs index 3da1498..aa46523 100644 --- a/src/Argus.Sync.EntityFramework/PostgresSingleInstanceLockWorker.cs +++ b/src/Argus.Sync.EntityFramework.Postgres/PostgresSingleInstanceLock.cs @@ -6,7 +6,7 @@ using Microsoft.Extensions.Logging; using Npgsql; -namespace Argus.Sync.EntityFramework; +namespace Argus.Sync.EntityFramework.Postgres; /// /// that holds a Postgres session-level advisory lock @@ -25,21 +25,21 @@ namespace Argus.Sync.EntityFramework; /// Requires a session-pinned connection: behind PgBouncer use session-pooling mode, not /// transaction-pooling, or the session advisory lock will not persist across statements. /// -public sealed partial class PostgresSingleInstanceLockWorker : BackgroundService, ISingleInstanceLock +public sealed partial class PostgresSingleInstanceLock : BackgroundService, ISingleInstanceLock { private readonly string _connectionString; private readonly long _key; private readonly TimeSpan _pollInterval; private readonly TimeSpan _healthInterval; - private readonly ILogger _logger; + private readonly ILogger _logger; private readonly IHostApplicationLifetime _lifetime; private readonly TaskCompletionSource _acquired = new(TaskCreationOptions.RunContinuationsAsynchronously); private NpgsqlConnection? _connection; /// Creates the lock worker from configuration (connection string, schema, intervals). - public PostgresSingleInstanceLockWorker( + public PostgresSingleInstanceLock( IConfiguration configuration, - ILogger logger, + ILogger logger, IHostApplicationLifetime lifetime) { ArgumentNullException.ThrowIfNull(configuration); diff --git a/src/Argus.Sync.EntityFramework/Argus.Sync.EntityFramework.csproj b/src/Argus.Sync.EntityFramework/Argus.Sync.EntityFramework.csproj index 9c873cc..a23a4ab 100644 --- a/src/Argus.Sync.EntityFramework/Argus.Sync.EntityFramework.csproj +++ b/src/Argus.Sync.EntityFramework/Argus.Sync.EntityFramework.csproj @@ -4,13 +4,13 @@ Argus.Sync.EntityFramework clark@saib.dev SAIB Inc. - Entity Framework Core (PostgreSQL / Npgsql) storage backend for Argus.Sync — the AddCardanoPostgresIndexer entry point and its EF unit-of-work, DbContext base, and advisory-lock. + Provider-neutral Entity Framework Core storage backend for Argus.Sync — the EF unit-of-work, DbContext base, and the AddCardanoEntityFrameworkIndexer registration seam. Pair with a provider package such as Argus.Sync.EntityFramework.Postgres. https://github.com/SAIB-Inc/Argus - + diff --git a/src/Argus.Sync.EntityFramework/CardanoDbContext.cs b/src/Argus.Sync.EntityFramework/CardanoDbContext.cs index 070348a..fcd9872 100644 --- a/src/Argus.Sync.EntityFramework/CardanoDbContext.cs +++ b/src/Argus.Sync.EntityFramework/CardanoDbContext.cs @@ -8,7 +8,7 @@ namespace Argus.Sync.EntityFramework; /// /// Base database context for EF Core consumers. Inherit from this to colocate /// your reducer-data tables with the framework's -/// table in a single Postgres schema. Non-EF consumers do not need this type; +/// table in a single database schema. Non-EF consumers do not need this type; /// implement directly instead. /// /// The database context options. diff --git a/src/Argus.Sync.EntityFramework/EfBlockUnitOfWorkFactory.cs b/src/Argus.Sync.EntityFramework/EfBlockUnitOfWorkFactory.cs index c47ce62..aeef059 100644 --- a/src/Argus.Sync.EntityFramework/EfBlockUnitOfWorkFactory.cs +++ b/src/Argus.Sync.EntityFramework/EfBlockUnitOfWorkFactory.cs @@ -2,6 +2,7 @@ using Argus.Sync.Data.Models; using Argus.Sync.Reducers; using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Storage; using Microsoft.Extensions.Configuration; namespace Argus.Sync.EntityFramework; @@ -38,7 +39,7 @@ public EfBlockUnitOfWorkFactory( public async Task CreateAsync(CancellationToken ct = default) { TContext dbContext = await _dbContextFactory.CreateDbContextAsync(ct).ConfigureAwait(false); - Microsoft.EntityFrameworkCore.Storage.IDbContextTransaction transaction = + IDbContextTransaction transaction = await dbContext.Database.BeginTransactionAsync(ct).ConfigureAwait(false); return new EfBlockUnitOfWork(dbContext, transaction, _rollbackBuffer); } diff --git a/src/Argus.Sync.EntityFramework/EfServiceCollectionExtensions.cs b/src/Argus.Sync.EntityFramework/EfServiceCollectionExtensions.cs index 9ac2393..80cf3bc 100644 --- a/src/Argus.Sync.EntityFramework/EfServiceCollectionExtensions.cs +++ b/src/Argus.Sync.EntityFramework/EfServiceCollectionExtensions.cs @@ -1,8 +1,6 @@ -using System.Reflection; using Argus.Sync.Extensions; using Argus.Sync.Providers; using Argus.Sync.Reducers; -using Argus.Sync.Workers; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; @@ -10,30 +8,35 @@ namespace Argus.Sync.EntityFramework; /// -/// Dependency-injection entry point for running the Cardano indexer on the Entity Framework Core / -/// PostgreSQL backend. Call with your -/// -derived context, then AddReducers. The Postgres counterpart of -/// AddCardanoMongoIndexer; both register their backend-specific storage seam and then share -/// AddCardanoIndexerCore from the core Argus.Sync library. +/// Dependency-injection entry point for running the Cardano indexer on a provider-neutral Entity +/// Framework Core backend. A provider package supplies the concrete EF provider via +/// configureProvider and registers its own single-instance lock, then calls +/// — for example AddCardanoPostgresIndexer +/// from Argus.Sync.EntityFramework.Postgres. Both this and the Mongo backend register their +/// backend-specific storage seam and then share AddCardanoIndexerCore from the core +/// Argus.Sync library. /// public static class EfServiceCollectionExtensions { /// - /// Registers the Cardano indexer on the EF Core / PostgreSQL backend: a pooled DbContext factory, the EF - /// unit-of-work factory (data + checkpoint storage), a Postgres single-instance advisory lock, the - /// chain-provider factory, and the indexer worker as a hosted service. + /// Registers the Cardano indexer on a provider-neutral EF Core backend: a DbContext factory (configured + /// by ), the EF unit-of-work factory (data + checkpoint storage), the + /// chain-provider factory, and the indexer worker as a hosted service. It does not register a + /// single-instance lock — a provider package adds the lock appropriate to its database (e.g. a Postgres + /// session advisory lock) before calling this. /// /// /// Transient-fault recovery. Argus does not retry database faults in-process. EF Core's /// EnableRetryOnFailure execution strategy hard-throws on user-initiated transactions, and Argus /// opens one manual transaction per block-branch (it spans multiple pipeline tasks and captures raw /// ExecuteUpdate/SQL atomically) — a unit that cannot be expressed as a single retriable delegate. - /// See the UseNpgsql note below. Recovery is fail-fast and crash-safe instead: + /// Provider registrations therefore do not enable a retrying execution strategy. Recovery is fail-fast and + /// crash-safe instead: /// /// A fault while processing a block rolls that block-branch's transaction back atomically — no partial /// writes (tracked rows, raw SQL, and the reducer-state checkpoint all roll back together). - /// The fault propagates out of 's execute loop, which stops the host - /// (the default behavior). + /// The fault propagates out of 's execute loop, + /// which stops the host (the default behavior). /// An external supervisor (systemd, Kubernetes, Docker restart:) restarts the process, which /// resumes from the last atomically-committed checkpoint and replays the failed block. /// @@ -43,51 +46,25 @@ public static class EfServiceCollectionExtensions /// The database context type inheriting from . /// The service collection. /// The application configuration. - /// The database command timeout in seconds. + /// Configures the EF Core provider on the DbContext options (e.g. o => o.UseNpgsql(...)). /// An optional custom chain provider factory; defaults to configuration-based if null. /// The service collection for method chaining. - public static IServiceCollection AddCardanoPostgresIndexer( + public static IServiceCollection AddCardanoEntityFrameworkIndexer( this IServiceCollection services, IConfiguration configuration, - int commandTimout = 60, + Action configureProvider, IChainProviderFactory? chainProviderFactory = null) where T : CardanoDbContext { ArgumentNullException.ThrowIfNull(services); ArgumentNullException.ThrowIfNull(configuration); + ArgumentNullException.ThrowIfNull(configureProvider); - _ = services.AddDbContextFactory(options => - { - Assembly? contextAssembly = typeof(T).Assembly; - - // EnableRetryOnFailure is intentionally NOT configured: EF's retrying execution strategy throws on - // user-initiated transactions not wrapped in CreateExecutionStrategy().ExecuteAsync(...), and the - // per-block-branch transaction spans multiple pipeline tasks (and captures raw ExecuteUpdate/SQL), - // so it cannot be a single retriable delegate. Transient faults are recovered out-of-process via - // fail-fast + restart + checkpoint-resume — see the remarks above. - _ = options.UseNpgsql( - configuration.GetConnectionString("CardanoContext"), - x => - { - _ = x.MigrationsAssembly(contextAssembly!.FullName); - _ = x.CommandTimeout(commandTimout); - _ = x.MigrationsHistoryTable("__EFMigrationsHistory", configuration!.GetConnectionString("CardanoContextSchema")); - }); - }); + _ = services.AddDbContextFactory(configureProvider); // EF unit-of-work factory — the storage-backend seam: transactional per-branch units + checkpoint reads. _ = services.AddSingleton(sp => new EfBlockUnitOfWorkFactory(sp.GetRequiredService>(), configuration)); - // Postgres single-instance guard (advisory lock): ONE shared singleton, exposed both as - // ISingleInstanceLock (the indexer's gate) and as a hosted service (runs the acquire/hold/release loop). - // The factory indirection keeps both roles on the SAME instance. Opt out via Sync:SingleInstanceLock:Enabled=false. - if (configuration.GetValue("Sync:SingleInstanceLock:Enabled", true)) - { - _ = services.AddSingleton(); - _ = services.AddSingleton(sp => sp.GetRequiredService()); - _ = services.AddHostedService(sp => sp.GetRequiredService()); - } - return services.AddCardanoIndexerCore(chainProviderFactory); } } diff --git a/src/Argus.Sync.Example/Argus.Sync.Example.csproj b/src/Argus.Sync.Example/Argus.Sync.Example.csproj index e4e7361..6760481 100644 --- a/src/Argus.Sync.Example/Argus.Sync.Example.csproj +++ b/src/Argus.Sync.Example/Argus.Sync.Example.csproj @@ -17,6 +17,7 @@ + diff --git a/src/Argus.Sync.Example/Program.cs b/src/Argus.Sync.Example/Program.cs index 2e66f48..ad7b88c 100644 --- a/src/Argus.Sync.Example/Program.cs +++ b/src/Argus.Sync.Example/Program.cs @@ -1,4 +1,4 @@ -using Argus.Sync.EntityFramework; +using Argus.Sync.EntityFramework.Postgres; using Argus.Sync.Example.Data; using Argus.Sync.Example.Services; using Argus.Sync.Extensions; diff --git a/src/Argus.Sync.Tests/Argus.Sync.Tests.csproj b/src/Argus.Sync.Tests/Argus.Sync.Tests.csproj index dc0ac68..08ed0c8 100644 --- a/src/Argus.Sync.Tests/Argus.Sync.Tests.csproj +++ b/src/Argus.Sync.Tests/Argus.Sync.Tests.csproj @@ -26,6 +26,7 @@ + diff --git a/src/Argus.Sync.Tests/EndToEnd/IndexerRegistrationTest.cs b/src/Argus.Sync.Tests/EndToEnd/IndexerRegistrationTest.cs index 189d504..93fc191 100644 --- a/src/Argus.Sync.Tests/EndToEnd/IndexerRegistrationTest.cs +++ b/src/Argus.Sync.Tests/EndToEnd/IndexerRegistrationTest.cs @@ -1,4 +1,5 @@ using Argus.Sync.EntityFramework; +using Argus.Sync.EntityFramework.Postgres; using Argus.Sync.Example.Data; using Argus.Sync.MongoDb; using Argus.Sync.Reducers; @@ -53,7 +54,7 @@ public void AddCardanoPostgresIndexer_WiresEfStorageLockAndWorker() // The lock is the Postgres advisory lock, shared across both roles. ISingleInstanceLock gate = provider.GetRequiredService(); - PostgresSingleInstanceLockWorker concrete = provider.GetRequiredService(); + PostgresSingleInstanceLock concrete = provider.GetRequiredService(); Assert.Same(concrete, gate); AssertWorkerHosted(services); diff --git a/src/Argus.Sync.Tests/EndToEnd/SingleInstanceLockTest.cs b/src/Argus.Sync.Tests/EndToEnd/SingleInstanceLockTest.cs index b34d0e8..fc32ceb 100644 --- a/src/Argus.Sync.Tests/EndToEnd/SingleInstanceLockTest.cs +++ b/src/Argus.Sync.Tests/EndToEnd/SingleInstanceLockTest.cs @@ -1,4 +1,4 @@ -using Argus.Sync.EntityFramework; +using Argus.Sync.EntityFramework.Postgres; using Argus.Sync.Tests.Infrastructure; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; @@ -48,8 +48,8 @@ public async Task SecondInstance_ParksUntilFirstReleasesTheLock() using ILoggerFactory loggerFactory = LoggerFactory.Create(b => b.SetMinimumLevel(LogLevel.Warning)); FakeLifetime lifetime = new(); - using PostgresSingleInstanceLockWorker first = CreateWorker(loggerFactory, lifetime); - using PostgresSingleInstanceLockWorker second = CreateWorker(loggerFactory, lifetime); + using PostgresSingleInstanceLock first = CreateWorker(loggerFactory, lifetime); + using PostgresSingleInstanceLock second = CreateWorker(loggerFactory, lifetime); // 1. First instance starts and acquires the (uncontended) lock. await first.StartAsync(CancellationToken.None); @@ -74,7 +74,7 @@ public async Task SecondInstance_ParksUntilFirstReleasesTheLock() Assert.False(lifetime.StopCalled, "the host should never be asked to stop in the happy path"); } - private PostgresSingleInstanceLockWorker CreateWorker(ILoggerFactory loggerFactory, IHostApplicationLifetime lifetime) + private PostgresSingleInstanceLock CreateWorker(ILoggerFactory loggerFactory, IHostApplicationLifetime lifetime) { // Both workers point at the same test database → same lock key → they contend. IConfiguration config = new ConfigurationBuilder().AddInMemoryCollection(new Dictionary @@ -84,9 +84,9 @@ private PostgresSingleInstanceLockWorker CreateWorker(ILoggerFactory loggerFacto ["Sync:SingleInstanceLock:HealthCheckSeconds"] = "1", }).Build(); - return new PostgresSingleInstanceLockWorker( + return new PostgresSingleInstanceLock( config, - loggerFactory.CreateLogger(), + loggerFactory.CreateLogger(), lifetime); } From 0691cc863451d62cb450f4ea45bdb236908c7e82 Mon Sep 17 00:00:00 2001 From: Mercurial Date: Sun, 21 Jun 2026 00:53:46 +0800 Subject: [PATCH 3/3] chore(slnx): add the backend projects as solution members Argus.Sync.EntityFramework, Argus.Sync.EntityFramework.Postgres, and Argus.Sync.MongoDb were building only transitively (via the Example/Tests project references) and were absent from Argus.slnx. List all 7 projects so a solution build compiles every backend in the chosen configuration directly (they now emit to bin/Release on a Release build instead of bin/Debug). Co-Authored-By: Claude Opus 4.8 --- Argus.slnx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Argus.slnx b/Argus.slnx index 76f0c8e..a22330a 100644 --- a/Argus.slnx +++ b/Argus.slnx @@ -1,6 +1,9 @@ + + +