diff --git a/src/Atomizer.EntityFrameworkCore/Providers/DatabaseProvider.cs b/src/Atomizer.EntityFrameworkCore/Providers/DatabaseProvider.cs
index 7803186..6e57d51 100644
--- a/src/Atomizer.EntityFrameworkCore/Providers/DatabaseProvider.cs
+++ b/src/Atomizer.EntityFrameworkCore/Providers/DatabaseProvider.cs
@@ -13,13 +13,4 @@ public enum DatabaseProvider
/// Microsoft SQL Server.
SqlServer,
-
- /// Oracle Database.
- Oracle,
-
- /// SQLite (unsafe fallback; not recommended for production).
- Sqlite,
-
- /// An unrecognized or unsupported provider.
- Unknown,
}
diff --git a/src/Atomizer.EntityFrameworkCore/Providers/EntityMap.cs b/src/Atomizer.EntityFrameworkCore/Providers/EntityMap.cs
index cc90317..2825ceb 100644
--- a/src/Atomizer.EntityFrameworkCore/Providers/EntityMap.cs
+++ b/src/Atomizer.EntityFrameworkCore/Providers/EntityMap.cs
@@ -48,8 +48,6 @@ public static EntityMap Build(IModel model, Type clrType, DatabaseProvider provi
DatabaseProvider.SqlServer => name => $"[{name}]",
DatabaseProvider.PostgreSql => name => $"\"{name}\"",
DatabaseProvider.MySql => name => $"`{name}`",
- DatabaseProvider.Oracle => name => $"\"{name}\"",
- DatabaseProvider.Unknown => name => name,
_ => throw new NotSupportedException($"Database provider {provider} is not supported."),
};
diff --git a/src/Atomizer.EntityFrameworkCore/Providers/RelationalProviderCache.cs b/src/Atomizer.EntityFrameworkCore/Providers/RelationalProviderCache.cs
index 862824f..625a4dd 100644
--- a/src/Atomizer.EntityFrameworkCore/Providers/RelationalProviderCache.cs
+++ b/src/Atomizer.EntityFrameworkCore/Providers/RelationalProviderCache.cs
@@ -7,16 +7,23 @@ namespace Atomizer.EntityFrameworkCore.Providers;
internal sealed class RelationalProviderCache
{
- public bool IsSupportedProvider => DetermineSupportedProvider(DatabaseProvider);
+ public bool IsSupportedProvider => _databaseProvider is not null;
+ public string ProviderName { get; }
public ISqlDialect? Dialect { get; }
- private DatabaseProvider DatabaseProvider { get; }
+ private readonly DatabaseProvider? _databaseProvider;
private readonly EntityMap? _jobs;
private readonly EntityMap? _schedules;
- private RelationalProviderCache(DatabaseProvider databaseProvider, EntityMap? jobs, EntityMap? schedules)
+ private RelationalProviderCache(
+ string providerName,
+ DatabaseProvider? databaseProvider,
+ EntityMap? jobs,
+ EntityMap? schedules
+ )
{
- DatabaseProvider = databaseProvider;
+ ProviderName = providerName;
+ _databaseProvider = databaseProvider;
_jobs = jobs;
_schedules = schedules;
@@ -26,30 +33,29 @@ private RelationalProviderCache(DatabaseProvider databaseProvider, EntityMap? jo
}
}
- private static readonly ConcurrentDictionary Instances = new();
+ private static readonly ConcurrentDictionary Instances = new();
public static RelationalProviderCache Create(TDbContext dbContext)
where TDbContext : DbContext
{
- // Determine provider for this DbContext
- var provider = DetectProvider(dbContext.Database.ProviderName ?? string.Empty);
+ var providerName = dbContext.Database.ProviderName ?? string.Empty;
+ var provider = DetectProvider(providerName);
- // Freeze Unknown per-provider as well (same semantics as before, but keyed).
return Instances.GetOrAdd(
- provider,
+ providerName,
_ =>
{
EntityMap? jobs = null,
schedules = null;
- if (DetermineSupportedProvider(provider))
+ if (provider is not null)
{
var model = dbContext.Model; // capture once
- jobs = EntityMap.Build(model, typeof(AtomizerJobEntity), provider);
- schedules = EntityMap.Build(model, typeof(AtomizerScheduleEntity), provider);
+ jobs = EntityMap.Build(model, typeof(AtomizerJobEntity), provider.Value);
+ schedules = EntityMap.Build(model, typeof(AtomizerScheduleEntity), provider.Value);
}
- return new RelationalProviderCache(provider, jobs, schedules);
+ return new RelationalProviderCache(providerName, provider, jobs, schedules);
}
);
}
@@ -61,34 +67,27 @@ private ISqlDialect CreateDialect()
throw new InvalidOperationException("Database provider is not supported or entity mappings are missing.");
}
- return DatabaseProvider switch
+ return _databaseProvider switch
{
DatabaseProvider.PostgreSql => new PostgreSqlDialect(_jobs, _schedules),
DatabaseProvider.MySql => new MySqlDialect(_jobs, _schedules),
DatabaseProvider.SqlServer => new SqlServerDialect(_jobs, _schedules),
- _ => throw new NotSupportedException($"Database provider {DatabaseProvider} is not supported."),
+ _ => throw new NotSupportedException($"Database provider {_databaseProvider} is not supported."),
};
}
- private static DatabaseProvider DetectProvider(string name) =>
+ private static DatabaseProvider? DetectProvider(string name) =>
name switch
{
"Microsoft.EntityFrameworkCore.SqlServer" => DatabaseProvider.SqlServer,
"Npgsql.EntityFrameworkCore.PostgreSQL" => DatabaseProvider.PostgreSql,
"Pomelo.EntityFrameworkCore.MySql" or "MySql.EntityFrameworkCore" => DatabaseProvider.MySql,
- "Oracle.EntityFrameworkCore" => DatabaseProvider.Oracle,
- "Microsoft.EntityFrameworkCore.Sqlite" => DatabaseProvider.Sqlite,
- _ => DatabaseProvider.Unknown,
+ _ => null,
};
- private static bool DetermineSupportedProvider(DatabaseProvider provider)
- {
- return provider is DatabaseProvider.PostgreSql or DatabaseProvider.MySql or DatabaseProvider.SqlServer;
- }
-
// Testing helpers
internal static bool TryGet(DatabaseProvider provider, out RelationalProviderCache? cache) =>
- Instances.TryGetValue(provider, out cache);
+ Instances.TryGetValue(GetProviderName(provider), out cache);
internal static void ResetInstanceForTests(DatabaseProvider? provider = null)
{
@@ -98,6 +97,15 @@ internal static void ResetInstanceForTests(DatabaseProvider? provider = null)
return;
}
- Instances.TryRemove(provider.Value, out _);
+ Instances.TryRemove(GetProviderName(provider.Value), out _);
}
+
+ private static string GetProviderName(DatabaseProvider provider) =>
+ provider switch
+ {
+ DatabaseProvider.SqlServer => "Microsoft.EntityFrameworkCore.SqlServer",
+ DatabaseProvider.PostgreSql => "Npgsql.EntityFrameworkCore.PostgreSQL",
+ DatabaseProvider.MySql => "Pomelo.EntityFrameworkCore.MySql",
+ _ => throw new NotSupportedException($"Database provider {provider} is not supported."),
+ };
}
diff --git a/src/Atomizer.EntityFrameworkCore/Storage/EntityFrameworkCoreJobStorageOptions.cs b/src/Atomizer.EntityFrameworkCore/Storage/EntityFrameworkCoreJobStorageOptions.cs
index d6abad5..f22c9ca 100644
--- a/src/Atomizer.EntityFrameworkCore/Storage/EntityFrameworkCoreJobStorageOptions.cs
+++ b/src/Atomizer.EntityFrameworkCore/Storage/EntityFrameworkCoreJobStorageOptions.cs
@@ -5,13 +5,6 @@
///
public class EntityFrameworkCoreJobStorageOptions
{
- ///
- /// If true, allows falling back to providers that may not be
- /// fully supported, tested or work in distributed environments (e.g. SQLite).
- ///
- /// Default is false. See documentation for details and implications.
- public bool AllowUnsafeProviderFallback { get; set; } = false;
-
///
/// Maximum time to wait when acquiring a database transaction lock before giving up.
///
diff --git a/src/Atomizer.EntityFrameworkCore/Storage/EntityFrameworkCoreStorage.cs b/src/Atomizer.EntityFrameworkCore/Storage/EntityFrameworkCoreStorage.cs
index 4423838..5edbf49 100644
--- a/src/Atomizer.EntityFrameworkCore/Storage/EntityFrameworkCoreStorage.cs
+++ b/src/Atomizer.EntityFrameworkCore/Storage/EntityFrameworkCoreStorage.cs
@@ -33,6 +33,10 @@ IAtomizerClock clock
_logger = logger;
_clock = clock;
_providerCache = RelationalProviderCache.Create(dbContext);
+ if (!_providerCache.IsSupportedProvider)
+ {
+ throw UnsupportedProviderException(_providerCache.ProviderName);
+ }
}
public async Task UpsertHeartbeatAsync(AtomizerActiveServer server, CancellationToken cancellationToken)
@@ -163,7 +167,7 @@ public async Task InsertAsync(AtomizerJob job, CancellationToken cancellat
}
}
- if (job.PartitionKey != null && _providerCache is { IsSupportedProvider: true, Dialect: not null })
+ if (job.PartitionKey != null && _providerCache.Dialect is not null)
{
var sql = _providerCache.Dialect.InsertJobWithSequence(job);
await _dbContext.Database.ExecuteSqlInterpolatedAsync(sql, cancellationToken);
@@ -176,19 +180,6 @@ public async Task InsertAsync(AtomizerJob job, CancellationToken cancellat
return job.Id;
}
- if (job.PartitionKey != null && !_providerCache.IsSupportedProvider && _options.AllowUnsafeProviderFallback)
- {
- // LINQ fallback sequence assignment: not atomic under concurrency but safe for single-process use.
- var partitionKeyStr = job.PartitionKey.ToString();
- var queueKeyStr = job.QueueKey.Key;
- var maxSeq = await JobEntities
- .AsNoTracking()
- .Where(j => j.QueueKey == queueKeyStr && j.PartitionKey == partitionKeyStr)
- .MaxAsync(j => j.SequenceNumber, cancellationToken);
- entity.SequenceNumber = (maxSeq ?? 0L) + 1L;
- job.SequenceNumber = entity.SequenceNumber;
- }
-
JobEntities.Add(entity);
await _dbContext.SaveChangesAsync(cancellationToken);
return entity.Id;
@@ -221,7 +212,7 @@ CancellationToken cancellationToken
{
cancellationToken.ThrowIfCancellationRequested();
- if (_providerCache is { IsSupportedProvider: true, Dialect: not null })
+ if (_providerCache.Dialect is not null)
{
var sql = _providerCache.Dialect.GetDueJobs(queueKey, now, batchSize);
@@ -230,62 +221,7 @@ CancellationToken cancellationToken
return entities.Select(job => job.ToAtomizerJob()).ToList();
}
- if (!_providerCache.IsSupportedProvider && _options.AllowUnsafeProviderFallback)
- {
- // WARNING: AsNoTracking() with no row lock means two concurrent QueuePumps
- // on the same process (or any second node) will both receive the same jobs.
- // AllowUnsafeProviderFallback is only safe with DegreeOfParallelism=1 and
- // a single process instance. It is not safe for production use.
- var allForQueue = await JobEntities
- .AsNoTracking()
- .Where(j => j.QueueKey == queueKey.Key)
- .ToListAsync(cancellationToken);
-
- // 1) Collect blocked partitions: any partition key with a Processing job
- // or a Pending job with prior attempts (retrying).
- var blockedPartitions = allForQueue
- .Where(j =>
- j.PartitionKey != null
- && (
- j.Status == AtomizerEntityJobStatus.Processing
- || (j.Status == AtomizerEntityJobStatus.Pending && j.Attempts > 0)
- )
- )
- .Select(j => j.PartitionKey!)
- .ToHashSet();
-
- // 2) Find the lowest sequence number per unblocked partition (partition heads).
- // Only consider Pending jobs that are due — Completed and Failed jobs must not
- // block the next job from becoming the partition head.
- var partitionHeads = allForQueue
- .Where(j =>
- j.PartitionKey != null
- && !blockedPartitions.Contains(j.PartitionKey)
- && j.Status == AtomizerEntityJobStatus.Pending
- && (j.VisibleAt == null || j.VisibleAt <= now)
- && j.ScheduledAt <= now
- )
- .GroupBy(j => j.PartitionKey!)
- .Select(g => g.OrderBy(j => j.SequenceNumber).First().Id)
- .ToHashSet();
-
- // 3) Apply eligibility filter, FIFO partition-head filter, and batch size limit.
- return allForQueue
- .Where(j =>
- (
- j.Status == AtomizerEntityJobStatus.Pending
- && (j.VisibleAt == null || j.VisibleAt <= now)
- && j.ScheduledAt <= now
- || (j.Status == AtomizerEntityJobStatus.Processing && j.VisibleAt <= now) // lease expired
- ) && (j.PartitionKey == null || partitionHeads.Contains(j.Id))
- )
- .OrderBy(j => j.ScheduledAt)
- .Take(batchSize)
- .Select(j => j.ToAtomizerJob())
- .ToList();
- }
-
- throw UnsupportedProviderException();
+ throw UnsupportedProviderException(_providerCache.ProviderName);
}
public async Task ReleaseLeasedAsync(
@@ -294,41 +230,21 @@ public async Task ReleaseLeasedAsync(
CancellationToken cancellationToken
)
{
- if (_providerCache is { IsSupportedProvider: true, Dialect: not null })
+ if (_providerCache.Dialect is not null)
{
var sql = _providerCache.Dialect.ReleaseLeasedJobs(leaseToken, now);
var result = await _dbContext.Database.ExecuteSqlInterpolatedAsync(sql, cancellationToken);
return result;
}
- var entities = await JobEntities
- .Where(j => j.LeaseToken == leaseToken.Token && j.Status == AtomizerEntityJobStatus.Processing)
- .ToListAsync(cancellationToken);
-
- foreach (var entity in entities)
- {
- entity.Status = AtomizerEntityJobStatus.Pending;
- entity.VisibleAt = null;
- entity.LeaseToken = null;
- entity.UpdatedAt = now;
- }
-
- try
- {
- return await _dbContext.SaveChangesAsync(cancellationToken);
- }
- catch (DbUpdateException ex)
- {
- _logger.LogError(ex, "Failed to release leased jobs for lease token {LeaseToken}", leaseToken.Token);
- return 0;
- }
+ throw UnsupportedProviderException(_providerCache.ProviderName);
}
public async Task UpsertScheduleAsync(AtomizerSchedule schedule, CancellationToken cancellationToken)
{
var entity = schedule.ToEntity();
- if (_providerCache is { IsSupportedProvider: true, Dialect: not null })
+ if (_providerCache.Dialect is not null)
{
var now = _clock.UtcNow;
var sql = _providerCache.Dialect.UpsertSchedule(schedule, now);
@@ -339,36 +255,7 @@ public async Task UpsertScheduleAsync(AtomizerSchedule schedule, Cancellat
.FirstAsync(cancellationToken);
}
- if (!_providerCache.IsSupportedProvider && _options.AllowUnsafeProviderFallback)
- {
- var existing = await ScheduleEntities
- .AsNoTracking()
- .FirstOrDefaultAsync(s => s.JobKey == entity.JobKey, cancellationToken);
-
- if (existing is not null)
- {
- entity.Id = existing.Id;
- ScheduleEntities.Update(entity);
- }
- else
- {
- ScheduleEntities.Add(entity);
- }
-
- try
- {
- await _dbContext.SaveChangesAsync(cancellationToken);
- }
- catch (DbUpdateException ex)
- {
- _logger.LogError(ex, "Failed to upsert schedule for job {JobKey}", schedule.JobKey);
- throw;
- }
-
- return entity.Id;
- }
-
- throw UnsupportedProviderException();
+ throw UnsupportedProviderException(_providerCache.ProviderName);
}
public async Task UpdateSchedulesAsync(IEnumerable schedules, CancellationToken cancellationToken)
@@ -396,7 +283,7 @@ CancellationToken cancellationToken
{
cancellationToken.ThrowIfCancellationRequested();
- if (_providerCache is { IsSupportedProvider: true, Dialect: not null })
+ if (_providerCache.Dialect is not null)
{
var sql = _providerCache.Dialect.GetDueSchedules(now);
@@ -408,17 +295,7 @@ CancellationToken cancellationToken
return entities.Select(s => s.ToAtomizerSchedule()).ToList();
}
- if (!_providerCache.IsSupportedProvider && _options.AllowUnsafeProviderFallback)
- {
- return await ScheduleEntities
- .AsNoTracking()
- .Where(s => s.Enabled && s.NextRunAt <= now)
- .OrderBy(s => s.NextRunAt)
- .Select(s => s.ToAtomizerSchedule())
- .ToListAsync(cancellationToken);
- }
-
- throw UnsupportedProviderException();
+ throw UnsupportedProviderException(_providerCache.ProviderName);
}
public async Task ExecuteInLeaseAsync(
@@ -476,10 +353,9 @@ CancellationToken cancellationToken
}
}
- private static NotSupportedException UnsupportedProviderException() =>
+ private static NotSupportedException UnsupportedProviderException(string providerName) =>
new(
- "The current database provider is not supported. "
- + "To bypass this check, set AllowUnsafeProviderFallback to true in EntityFrameworkCoreJobStorageOptions. "
- + "Note that this may lead to unexpected behavior."
+ $"The current database provider '{providerName}' is not supported. "
+ + "Atomizer.EntityFrameworkCore supports SQL Server, PostgreSQL and MySQL."
);
}
diff --git a/tests/Atomizer.EntityFrameworkCore.Tests/Fixtures/SqliteDatabaseFixture.cs b/tests/Atomizer.EntityFrameworkCore.Tests/Fixtures/SqliteDatabaseFixture.cs
deleted file mode 100644
index 9a1e801..0000000
--- a/tests/Atomizer.EntityFrameworkCore.Tests/Fixtures/SqliteDatabaseFixture.cs
+++ /dev/null
@@ -1,40 +0,0 @@
-using Atomizer.EntityFrameworkCore.Providers;
-using Atomizer.EntityFrameworkCore.Tests.TestSetup.Sqlite;
-using Microsoft.EntityFrameworkCore;
-
-namespace Atomizer.EntityFrameworkCore.Tests.Fixtures;
-
-[CollectionDefinition(nameof(SqliteDatabaseFixture))]
-public class SqliteDatabaseFixture : ICollectionFixture, IAsyncLifetime
-{
- public SqliteDbContext DbContext { get; private set; } = null!;
- private string _databaseName = string.Empty;
-
- public async ValueTask InitializeAsync()
- {
- _databaseName = $"atomizer_db_{Guid.NewGuid():N}.db";
- RelationalProviderCache.ResetInstanceForTests();
-
- DbContext = ConfigureDbContext();
- await DbContext.Database.EnsureCreatedAsync();
- }
-
- private SqliteDbContext ConfigureDbContext()
- {
- var options = new DbContextOptionsBuilder();
- options.UseSqlite($"Data Source={_databaseName}");
-
- return new SqliteDbContext(options.Options);
- }
-
- public SqliteDbContext CreateNewDbContext()
- {
- return ConfigureDbContext();
- }
-
- public async ValueTask DisposeAsync()
- {
- await DbContext.Database.EnsureDeletedAsync();
- await DbContext.DisposeAsync();
- }
-}
diff --git a/tests/Atomizer.EntityFrameworkCore.Tests/Storage/EntityFrameworkCoreStorageProviderTests.cs b/tests/Atomizer.EntityFrameworkCore.Tests/Storage/EntityFrameworkCoreStorageProviderTests.cs
new file mode 100644
index 0000000..fbb985c
--- /dev/null
+++ b/tests/Atomizer.EntityFrameworkCore.Tests/Storage/EntityFrameworkCoreStorageProviderTests.cs
@@ -0,0 +1,37 @@
+using Atomizer.Core;
+using Atomizer.EntityFrameworkCore.Storage;
+using Atomizer.EntityFrameworkCore.Tests.TestSetup;
+using AwesomeAssertions;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.Logging;
+using NSubstitute;
+
+namespace Atomizer.EntityFrameworkCore.Tests.Storage;
+
+public sealed class EntityFrameworkCoreStorageProviderTests
+{
+ [Fact]
+ public void Constructor_WhenProviderIsUnsupported_ShouldFailFast()
+ {
+ var options = new DbContextOptionsBuilder()
+ .UseSqlite("Data Source=:memory:")
+ .Options;
+
+ using var dbContext = new UnsupportedProviderDbContext(options);
+
+ var act = () =>
+ new EntityFrameworkCoreStorage(
+ dbContext,
+ new EntityFrameworkCoreJobStorageOptions(),
+ Substitute.For>>(),
+ Substitute.For()
+ );
+
+ act.Should()
+ .Throw()
+ .WithMessage("*Microsoft.EntityFrameworkCore.Sqlite*SQL Server*PostgreSQL*MySQL*");
+ }
+
+ public sealed class UnsupportedProviderDbContext(DbContextOptions options)
+ : TestDbContext(options);
+}
diff --git a/tests/Atomizer.EntityFrameworkCore.Tests/Storage/EntityFrameworkCoreStorageTests.cs b/tests/Atomizer.EntityFrameworkCore.Tests/Storage/EntityFrameworkCoreStorageTests.cs
index 149ef5c..399d02d 100644
--- a/tests/Atomizer.EntityFrameworkCore.Tests/Storage/EntityFrameworkCoreStorageTests.cs
+++ b/tests/Atomizer.EntityFrameworkCore.Tests/Storage/EntityFrameworkCoreStorageTests.cs
@@ -225,12 +225,6 @@ public async Task GetDueJobsAsync_WhenForUpdate_ShouldNotReturnSameJobs()
await using var dbContext1 = _dbContextFactory();
- if (dbContext1.Database.IsSqlite())
- {
- // SQLite does not support "FOR UPDATE SKIP LOCKED" behavior, so we skip this test for SQLite.
- return;
- }
-
var storage1 = _storageFactory(dbContext1);
await using var dbContext2 = _dbContextFactory();
@@ -461,12 +455,6 @@ public async Task GetDueSchedules_WhenForUpdate_ShouldNotReturnSameSchedules()
await using var dbContext1 = _dbContextFactory();
- if (dbContext1.Database.IsSqlite())
- {
- // SQLite does not support "FOR UPDATE SKIP LOCKED" behavior, so we skip this test for SQLite.
- return;
- }
-
var storage1 = _storageFactory(dbContext1);
await using var dbContext2 = _dbContextFactory();
@@ -814,10 +802,3 @@ public class MySqlStorageTestsExecutor(MySqlDatabaseFixture fixture)
[Collection(nameof(SqlServerDatabaseFixture))]
public class SqlServerStorageTestsExecutor(SqlServerDatabaseFixture fixture)
: EntityFrameworkCoreStorageTests(fixture.CreateNewDbContext);
-
-[Collection(nameof(SqliteDatabaseFixture))]
-public class SqliteStorageTestsExecutor(SqliteDatabaseFixture fixture)
- : EntityFrameworkCoreStorageTests(
- fixture.CreateNewDbContext,
- new EntityFrameworkCoreJobStorageOptions { AllowUnsafeProviderFallback = true }
- );
diff --git a/tests/Atomizer.EntityFrameworkCore.Tests/Storage/Sqlite/SqliteStorageContractTests.cs b/tests/Atomizer.EntityFrameworkCore.Tests/Storage/Sqlite/SqliteStorageContractTests.cs
deleted file mode 100644
index c54944e..0000000
--- a/tests/Atomizer.EntityFrameworkCore.Tests/Storage/Sqlite/SqliteStorageContractTests.cs
+++ /dev/null
@@ -1,49 +0,0 @@
-using Atomizer.Abstractions;
-using Atomizer.Core;
-using Atomizer.EntityFrameworkCore.Storage;
-using Atomizer.EntityFrameworkCore.Tests.Fixtures;
-using Atomizer.EntityFrameworkCore.Tests.TestSetup.Sqlite;
-using Atomizer.Tests.Utilities.StorageContract;
-using Microsoft.Extensions.Logging;
-using NSubstitute;
-
-namespace Atomizer.EntityFrameworkCore.Tests.Storage.Sqlite;
-
-///
-/// Contract tests for backed by SQLite.
-/// SQLite is not a supported production provider; it exercises the LINQ fallback path
-/// (AllowUnsafeProviderFallback = true), not the CTE dialect SQL.
-/// The CTE dialect SQL is verified by the PostgreSQL, SQL Server, and MySQL subclasses.
-///
-///
-/// The fallback path enforces FIFO semantics in-process, but it is not safe for concurrent
-/// multi-node production use because it does not take provider-level row locks.
-///
-[Collection(nameof(SqliteDatabaseFixture))]
-public sealed class SqliteStorageContractTests(SqliteDatabaseFixture fixture) : AtomizerStorageContractTests
-{
- private SqliteDbContext? _dbContext;
-
- protected override IAtomizerStorage CreateStorage(IAtomizerClock clock)
- {
- _dbContext = fixture.CreateNewDbContext();
- return new EntityFrameworkCoreStorage(
- _dbContext,
- new EntityFrameworkCoreJobStorageOptions { AllowUnsafeProviderFallback = true },
- Substitute.For>>(),
- clock
- );
- }
-
- public override async ValueTask DisposeAsync()
- {
- if (_dbContext is not null)
- await _dbContext.DisposeAsync();
-
- // Delete errors before jobs to satisfy the FK constraint, then schedules.
- // Use a bounded cancellation token so teardown does not hang indefinitely.
- using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30));
- await using var cleanupContext = fixture.CreateNewDbContext();
- await StorageTestCleanup.ClearAsync(cleanupContext, cts.Token);
- }
-}
diff --git a/tests/Atomizer.EntityFrameworkCore.Tests/TestSetup/Sqlite/SqliteDbContext.cs b/tests/Atomizer.EntityFrameworkCore.Tests/TestSetup/Sqlite/SqliteDbContext.cs
deleted file mode 100644
index 7e253ee..0000000
--- a/tests/Atomizer.EntityFrameworkCore.Tests/TestSetup/Sqlite/SqliteDbContext.cs
+++ /dev/null
@@ -1,16 +0,0 @@
-using Microsoft.EntityFrameworkCore;
-using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
-
-namespace Atomizer.EntityFrameworkCore.Tests.TestSetup.Sqlite;
-
-public class SqliteDbContext : TestDbContext
-{
- public SqliteDbContext(DbContextOptions options, string? schema = null)
- : base(options, schema) { }
-
- protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder)
- {
- configurationBuilder.Properties().HaveConversion();
- base.ConfigureConventions(configurationBuilder);
- }
-}
diff --git a/tests/Atomizer.Tests/Processing/QueuePumpTests.cs b/tests/Atomizer.Tests/Processing/QueuePumpTests.cs
index 50a0653..bfc2280 100644
--- a/tests/Atomizer.Tests/Processing/QueuePumpTests.cs
+++ b/tests/Atomizer.Tests/Processing/QueuePumpTests.cs
@@ -140,10 +140,7 @@ public async Task StopAsync_WhenWorkerIsLongRunning_ShouldRespectGracePeriod()
// Assert: Should return after about 1 second, not 2
sw.Elapsed.TotalSeconds.Should().BeGreaterThanOrEqualTo(1);
sw.Elapsed.TotalSeconds.Should()
- .BeLessThanOrEqualTo(
- 1.5,
- $"StopAsync should return after about 1 second, elapsed: {sw.Elapsed.TotalSeconds}"
- );
+ .BeLessThan(2, $"StopAsync should return after about 1 second, elapsed: {sw.Elapsed.TotalSeconds}");
logger.Received(1).LogInformation($"Stopping queue '{queueOptions.QueueKey}'...");
logger.Received(1).LogInformation($"Queue '{queueOptions.QueueKey}' stopped");