Skip to content
Merged
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
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions Argus.slnx
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
<Solution>
<Folder Name="/src/">
<Project Path="src/Argus.Sync/Argus.Sync.csproj" />
<Project Path="src/Argus.Sync.EntityFramework/Argus.Sync.EntityFramework.csproj" />
<Project Path="src/Argus.Sync.EntityFramework.Postgres/Argus.Sync.EntityFramework.Postgres.csproj" />
<Project Path="src/Argus.Sync.MongoDb/Argus.Sync.MongoDb.csproj" />
<Project Path="src/Argus.Sync.Example/Argus.Sync.Example.csproj" />
<Project Path="src/Argus.Sync.Tests/Argus.Sync.Tests.csproj" />
<Project Path="src/Argus.Sync.Bench/Argus.Sync.Bench.csproj" />
Expand Down
22 changes: 11 additions & 11 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -9,26 +9,26 @@
<PackageVersion Include="Utxorpc.Sdk" Version="1.7.0-alpha" />

<!-- Microsoft Extensions -->
<PackageVersion Include="Microsoft.Extensions.Hosting" Version="10.0.5" />
<PackageVersion Include="Microsoft.Extensions.Configuration" Version="10.0.5" />
<PackageVersion Include="Microsoft.Extensions.Hosting" Version="10.0.9" />
<PackageVersion Include="Microsoft.Extensions.Configuration" Version="10.0.9" />

<!-- Database -->
<PackageVersion Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.0" />
<PackageVersion Include="Microsoft.EntityFrameworkCore" Version="10.0.0" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="10.0.0" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.0" />
<PackageVersion Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.2" />
<PackageVersion Include="Microsoft.EntityFrameworkCore" Version="10.0.9" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="10.0.9" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.9" />
<PackageVersion Include="MongoDB.Driver" Version="3.9.0" />

<!-- UI / API -->
<PackageVersion Include="Spectre.Console" Version="0.53.1" />
<PackageVersion Include="Microsoft.AspNetCore.OpenApi" Version="10.0.5" />
<PackageVersion Include="Swashbuckle.AspNetCore" Version="10.1.5" />
<PackageVersion Include="Spectre.Console" Version="0.57.0" />
<PackageVersion Include="Microsoft.AspNetCore.OpenApi" Version="10.0.9" />
<PackageVersion Include="Swashbuckle.AspNetCore" Version="10.2.2" />

<!-- Testing -->
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.3.0" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.6.0" />
<PackageVersion Include="xunit" Version="2.9.3" />
<PackageVersion Include="xunit.runner.visualstudio" Version="3.1.5" />
<PackageVersion Include="coverlet.collector" Version="8.0.1" />
<PackageVersion Include="coverlet.collector" Version="10.0.1" />

<!-- Benchmarking -->
<PackageVersion Include="BenchmarkDotNet" Version="0.15.8" />
Expand Down
27 changes: 15 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -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<MyDbContext>(builder.Configuration);
builder.Services.AddReducers(builder.Configuration);
```
Expand Down Expand Up @@ -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.
Expand All @@ -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. |
Expand All @@ -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<T>`; call `db.SaveChangesAsync()` | `uow.GetStorage<T>()`; the framework commits — **never** call `SaveChangesAsync` |
| Postgres registration | `AddCardanoIndexer<T>()` (core package) | `AddCardanoPostgresIndexer<T>()` from the **`Argus.Sync.EntityFramework`** package |
| Postgres registration | `AddCardanoIndexer<T>()` (core package) | `AddCardanoPostgresIndexer<T>()` from the **`Argus.Sync.EntityFramework.Postgres`** package |
| Reducer registration | `AddReducers<T, V>(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 `<T>` on `IReducer`; change both methods to take `(…, IBlockUnitOfWork uow, CancellationToken ct)`; replace `dbContextFactory.CreateDbContext()` with `uow.GetStorage<YourDbContext>()`; and delete every `SaveChangesAsync` call. Then add the `Argus.Sync.EntityFramework` package reference, and switch `AddCardanoIndexer<T>` → `AddCardanoPostgresIndexer<T>` and `AddReducers<T, V>` → `AddReducers`.
**To upgrade a reducer in practice:** drop the `IDbContextFactory` constructor parameter and the `<T>` on `IReducer`; change both methods to take `(…, IBlockUnitOfWork uow, CancellationToken ct)`; replace `dbContextFactory.CreateDbContext()` with `uow.GetStorage<YourDbContext>()`; and delete every `SaveChangesAsync` call. Then add the `Argus.Sync.EntityFramework.Postgres` package reference, and switch `AddCardanoIndexer<T>` → `AddCardanoPostgresIndexer<T>` and `AddReducers<T, V>` → `AddReducers`.

## 🤝 Contributing

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<PackageId>Argus.Sync.EntityFramework.Postgres</PackageId>
<Authors>clark@saib.dev</Authors>
<Company>SAIB Inc.</Company>
<PackageDescription>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.</PackageDescription>
<RepositoryUrl>https://github.com/SAIB-Inc/Argus</RepositoryUrl>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\Argus.Sync.EntityFramework\Argus.Sync.EntityFramework.csproj" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// Dependency-injection entry point for running the Cardano indexer on the PostgreSQL / Npgsql backend.
/// Call <see cref="AddCardanoPostgresIndexer{T}"/> with your <see cref="CardanoDbContext"/>-derived context,
/// then <c>AddReducers</c>. This is a thin Npgsql wrapper over the provider-neutral
/// <see cref="EfServiceCollectionExtensions.AddCardanoEntityFrameworkIndexer{T}"/>: it supplies the
/// <c>UseNpgsql</c> provider and registers a Postgres session-advisory single-instance lock.
/// </summary>
public static class PostgresServiceCollectionExtensions
{
/// <summary>
/// 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.
/// </summary>
/// <typeparam name="T">The database context type inheriting from <see cref="CardanoDbContext"/>.</typeparam>
/// <param name="services">The service collection.</param>
/// <param name="configuration">The application configuration.</param>
/// <param name="commandTimeout">The database command timeout in seconds.</param>
/// <param name="chainProviderFactory">An optional custom chain provider factory; defaults to configuration-based if null.</param>
/// <returns>The service collection for method chaining.</returns>
public static IServiceCollection AddCardanoPostgresIndexer<T>(
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<PostgresSingleInstanceLock>();
_ = services.AddSingleton<ISingleInstanceLock>(sp => sp.GetRequiredService<PostgresSingleInstanceLock>());
_ = services.AddHostedService(sp => sp.GetRequiredService<PostgresSingleInstanceLock>());
}

return services.AddCardanoEntityFrameworkIndexer<T>(
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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
using Microsoft.Extensions.Logging;
using Npgsql;

namespace Argus.Sync.EntityFramework;
namespace Argus.Sync.EntityFramework.Postgres;

/// <summary>
/// <see cref="BackgroundService"/> that holds a Postgres <em>session-level advisory lock</em>
Expand All @@ -25,21 +25,21 @@ namespace Argus.Sync.EntityFramework;
/// <para>Requires a session-pinned connection: behind PgBouncer use session-pooling mode, not
/// transaction-pooling, or the session advisory lock will not persist across statements.</para>
/// </remarks>
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<PostgresSingleInstanceLockWorker> _logger;
private readonly ILogger<PostgresSingleInstanceLock> _logger;
private readonly IHostApplicationLifetime _lifetime;
private readonly TaskCompletionSource _acquired = new(TaskCreationOptions.RunContinuationsAsynchronously);
private NpgsqlConnection? _connection;

/// <summary>Creates the lock worker from configuration (connection string, schema, intervals).</summary>
public PostgresSingleInstanceLockWorker(
public PostgresSingleInstanceLock(
IConfiguration configuration,
ILogger<PostgresSingleInstanceLockWorker> logger,
ILogger<PostgresSingleInstanceLock> logger,
IHostApplicationLifetime lifetime)
{
ArgumentNullException.ThrowIfNull(configuration);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,13 @@
<PackageId>Argus.Sync.EntityFramework</PackageId>
<Authors>clark@saib.dev</Authors>
<Company>SAIB Inc.</Company>
<PackageDescription>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.</PackageDescription>
<PackageDescription>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.</PackageDescription>
<RepositoryUrl>https://github.com/SAIB-Inc/Argus</RepositoryUrl>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\Argus.Sync\Argus.Sync.csproj" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" />
</ItemGroup>

</Project>
2 changes: 1 addition & 1 deletion src/Argus.Sync.EntityFramework/CardanoDbContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ namespace Argus.Sync.EntityFramework;
/// <summary>
/// Base database context for EF Core consumers. Inherit from this to colocate
/// your reducer-data tables with the framework's <see cref="ReducerState"/>
/// 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 <see cref="Reducers.IBlockUnitOfWorkFactory"/> directly instead.
/// </summary>
/// <param name="Options">The database context options.</param>
Expand Down
3 changes: 2 additions & 1 deletion src/Argus.Sync.EntityFramework/EfBlockUnitOfWorkFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -38,7 +39,7 @@ public EfBlockUnitOfWorkFactory(
public async Task<IBlockUnitOfWork> 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<TContext>(dbContext, transaction, _rollbackBuffer);
}
Expand Down
Loading
Loading