diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5f8e54da9d2..3c728407155 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -149,6 +149,41 @@ jobs: path: | **/TestResults/* **/logs/* + test-efcore-sqlserver: + name: Microsoft Entity Framework Core SQL Server provider tests + runs-on: ubuntu-latest + strategy: + matrix: + provider: [ "EFCore-SqlServer" ] + services: + mssql: + image: mcr.microsoft.com/mssql/server:latest + ports: + - 1433:1433 + env: + ACCEPT_EULA: "Y" + MSSQL_PID: "Developer" + # [SuppressMessage("Microsoft.Security", "CS002:SecretInNextLine", Justification="False positive")] + SA_PASSWORD: "yourStrong(!)Password" + steps: + - uses: actions/checkout@v2 + - name: Setup .NET + uses: actions/setup-dotnet@v1 + with: + dotnet-version: | + 3.1.x + 7.0.x + - name: Test + run: dotnet test --filter "Category=${{ matrix.provider }}&(Category=BVT|Category=SlowBVT|Category=Functional)" --blame-hang-timeout 10m --logger "trx" -- -parallel none -noshadow + - name: Archive Test Results + if: always() + uses: actions/upload-artifact@v3 + with: + name: test_output + retention-days: 1 + path: | + **/TestResults/* + **/logs/* test-sqlserver: name: Microsoft SQL Server provider tests runs-on: ubuntu-latest diff --git a/src/EFCore/Orleans.Clustering.EntityFrameworkCore/EFMembershipTable.cs b/src/EFCore/Orleans.Clustering.EntityFrameworkCore/EFMembershipTable.cs index 169b3da380b..04e8f35acc8 100644 --- a/src/EFCore/Orleans.Clustering.EntityFrameworkCore/EFMembershipTable.cs +++ b/src/EFCore/Orleans.Clustering.EntityFrameworkCore/EFMembershipTable.cs @@ -97,7 +97,7 @@ public async Task CleanupDefunctSiloEntries(DateTimeOffset beforeDate) var silos = await ctx.Silos .Where(s => s.ClusterId == this._clusterId && - s.Status == SiloStatus.Dead && + s.Status != SiloStatus.Active && s.IAmAliveTime < beforeDate) .ToArrayAsync() .ConfigureAwait(false); @@ -122,7 +122,7 @@ public async Task ReadRow(SiloAddress key) { var ctx = this._dbContextFactory.CreateDbContext(); - var record = await ctx.Silos.Include(s => s.ClusterId).AsNoTracking() + var record = await ctx.Silos.Include(s => s.Cluster).AsNoTracking() .SingleOrDefaultAsync(s => s.ClusterId == this._clusterId && s.Address == key.Endpoint.Address.ToString() && @@ -137,7 +137,7 @@ public async Task ReadRow(SiloAddress key) var version = new TableVersion( record.Cluster.Version, - BitConverter.ToUInt64(record.ETag).ToString() + BitConverter.ToUInt64(record.Cluster.ETag).ToString() ); var memEntries = new List> {Tuple.Create(ConvertRecord(record), BitConverter.ToUInt64(record.ETag).ToString())}; @@ -213,8 +213,9 @@ public async Task InsertRow(MembershipEntry entry, TableVersion tableVersi await ctx.SaveChangesAsync().ConfigureAwait(false); return true; } - catch (DbUpdateConcurrencyException) + catch (DbUpdateException exc) { + this._logger.LogWarning(exc, "Failure inserting entry for cluster {Cluster}", this._clusterId); return false; } catch (Exception exc) @@ -231,7 +232,6 @@ public async Task UpdateRow(MembershipEntry entry, string etag, TableVersi { var clusterRecord = this.ConvertToRecord(tableVersion); var siloRecord = this.ConvertToRecord(entry); - siloRecord.ClusterId = clusterRecord.Id; siloRecord.ETag = BitConverter.GetBytes(ulong.Parse(etag)); var ctx = this._dbContextFactory.CreateDbContext(); @@ -359,8 +359,14 @@ private SiloRecord ConvertToRecord(in MembershipEntry memEntry) return record; } - private ClusterRecord ConvertToRecord(in TableVersion tableVersion) + private ClusterRecord ConvertToRecord( in TableVersion tableVersion) { - return new() {Id = this._clusterId, Version = tableVersion.Version, Timestamp = DateTimeOffset.UtcNow, ETag = BitConverter.GetBytes(ulong.Parse(tableVersion.VersionEtag))}; + return new() + { + Id = this._clusterId, + Version = tableVersion.Version, + Timestamp = DateTimeOffset.UtcNow, + ETag = BitConverter.GetBytes(ulong.Parse(tableVersion.VersionEtag)) + }; } } \ No newline at end of file diff --git a/src/EFCore/Orleans.Clustering.EntityFrameworkCore/Orleans.Clustering.EntityFrameworkCore.csproj b/src/EFCore/Orleans.Clustering.EntityFrameworkCore/Orleans.Clustering.EntityFrameworkCore.csproj index 9ea83e05918..1a0b07d5a8e 100644 --- a/src/EFCore/Orleans.Clustering.EntityFrameworkCore/Orleans.Clustering.EntityFrameworkCore.csproj +++ b/src/EFCore/Orleans.Clustering.EntityFrameworkCore/Orleans.Clustering.EntityFrameworkCore.csproj @@ -11,6 +11,7 @@ + diff --git a/src/EFCore/Orleans.GrainDirectory.EntityFrameworkCore/EFCoreGrainDirectory.cs b/src/EFCore/Orleans.GrainDirectory.EntityFrameworkCore/EFCoreGrainDirectory.cs index 2610b8b8e09..afc6eab921d 100644 --- a/src/EFCore/Orleans.GrainDirectory.EntityFrameworkCore/EFCoreGrainDirectory.cs +++ b/src/EFCore/Orleans.GrainDirectory.EntityFrameworkCore/EFCoreGrainDirectory.cs @@ -47,14 +47,14 @@ public EFCoreGrainDirectory( c.GrainId == grainIdStr) .ConfigureAwait(false); - var previousRecord = this.FromGrainAddress(previousAddress); + var previousEntry = this.FromGrainAddress(previousAddress); if (record is null) { ctx.Activations.Add(toRegister); await ctx.SaveChangesAsync().ConfigureAwait(false); } - else if (record.ActivationId != previousRecord.ActivationId || record.SiloAddress != previousRecord.SiloAddress) + else if (record.ActivationId != previousEntry.ActivationId || record.SiloAddress != previousEntry.SiloAddress) { return await Lookup(address.GrainId).ConfigureAwait(false); } @@ -65,7 +65,7 @@ public EFCoreGrainDirectory( ctx.Activations.Update(toRegister); await ctx.SaveChangesAsync().ConfigureAwait(false); - return address; + return this.ToGrainAddress(toRegister); } } else @@ -74,11 +74,10 @@ public EFCoreGrainDirectory( await ctx.SaveChangesAsync().ConfigureAwait(false); } } - catch (Exception exc) + catch { - this._logger.LogWarning(exc, "Unable to update Grain Directory"); - WrappedException.CreateAndRethrow(exc); - throw; + // Possible race condition? + return await Lookup(address.GrainId).ConfigureAwait(false); } return await Lookup(address.GrainId).ConfigureAwait(false); diff --git a/src/EFCore/Orleans.Persistence.EntityFrameworkCore/EFGrainStorage.cs b/src/EFCore/Orleans.Persistence.EntityFrameworkCore/EFGrainStorage.cs index 0fb317e2cf8..8c846fdadbc 100644 --- a/src/EFCore/Orleans.Persistence.EntityFrameworkCore/EFGrainStorage.cs +++ b/src/EFCore/Orleans.Persistence.EntityFrameworkCore/EFGrainStorage.cs @@ -15,6 +15,7 @@ namespace Orleans.Persistence.EntityFrameworkCore; internal class EFGrainStorage : IGrainStorage, ILifecycleParticipant where TDbContext : GrainStateDbContext { + private const string ANY_ETAG = "*"; private readonly ILogger _logger; private readonly string _name; private readonly string _serviceId; @@ -25,15 +26,14 @@ public EFGrainStorage( string name, ILoggerFactory loggerFactory, IOptions clusterOptions, + IDbContextFactory dbContextFactory, IServiceProvider serviceProvider) { this._serviceProvider = serviceProvider; this._name = name; this._serviceId = clusterOptions.Value.ServiceId; this._logger = loggerFactory.CreateLogger>(); - - var dbContextFactory = this._serviceProvider.GetService>(); - this._dbContextFactory = dbContextFactory ?? throw new OrleansConfigurationException("There are no GrainStateDbContext registered"); + this._dbContextFactory = dbContextFactory; } public async Task ReadStateAsync(string stateName, GrainId grainId, IGrainState grainState) @@ -91,14 +91,31 @@ public async Task WriteStateAsync(string stateName, GrainId grainId, IGrainSt Data = JsonSerializer.Serialize(grainState.State), }; - if (grainState.RecordExists) + if (string.IsNullOrWhiteSpace(grainState.ETag)) { - record.ETag = BitConverter.GetBytes(ulong.Parse(grainState.ETag)); - ctx.GrainState.Update(record); + ctx.GrainState.Add(record); + } + else if (grainState.ETag == ANY_ETAG) + { + var etag = await ctx.GrainState.AsNoTracking().Where(r => + r.ServiceId == this._serviceId && + r.GrainType == grainType && + r.StateType == stateName && + r.GrainId == id) + .Select(r => r.ETag) + .FirstOrDefaultAsync(); + + if (etag is not null) + { + record.ETag = etag; + } + + ctx.Update(record); } else { - ctx.GrainState.Add(record); + record.ETag = BitConverter.GetBytes(ulong.Parse(grainState.ETag)); + ctx.GrainState.Update(record); } try diff --git a/src/EFCore/Orleans.Persistence.EntityFrameworkCore/Orleans.Persistence.EntityFrameworkCore.csproj b/src/EFCore/Orleans.Persistence.EntityFrameworkCore/Orleans.Persistence.EntityFrameworkCore.csproj index 4a6146db7cc..d835e74b8dc 100644 --- a/src/EFCore/Orleans.Persistence.EntityFrameworkCore/Orleans.Persistence.EntityFrameworkCore.csproj +++ b/src/EFCore/Orleans.Persistence.EntityFrameworkCore/Orleans.Persistence.EntityFrameworkCore.csproj @@ -11,6 +11,7 @@ + diff --git a/src/EFCore/Orleans.Reminders.EntityFrameworkCore/EFReminderTable.cs b/src/EFCore/Orleans.Reminders.EntityFrameworkCore/EFReminderTable.cs index ac48dc490d0..6f94991e45c 100644 --- a/src/EFCore/Orleans.Reminders.EntityFrameworkCore/EFReminderTable.cs +++ b/src/EFCore/Orleans.Reminders.EntityFrameworkCore/EFReminderTable.cs @@ -119,7 +119,30 @@ public async Task UpsertRow(ReminderEntry entry) var ctx = this._dbContextFactory.CreateDbContext(); - ctx.Reminders.Update(record); + if (string.IsNullOrWhiteSpace(entry.ETag)) + { + var foundRecord = await ctx.Reminders + .AsNoTracking() + .SingleOrDefaultAsync(r => + r.ServiceId == this._serviceId && + r.Name == entry.ReminderName && + r.GrainId == entry.GrainId.ToString()) + .ConfigureAwait(false); + + if (foundRecord is not null) + { + record.ETag = foundRecord.ETag; + ctx.Reminders.Update(record); + } + else + { + ctx.Reminders.Add(record); + } + } + else + { + ctx.Reminders.Update(record); + } await ctx.SaveChangesAsync().ConfigureAwait(false); @@ -197,16 +220,22 @@ public async Task TestOnlyClearTable() private ReminderRecord ConvertToRecord(ReminderEntry entry) { - return new ReminderRecord + var record = new ReminderRecord { ServiceId = this._serviceId, GrainHash = entry.GrainId.GetUniformHashCode(), GrainId = entry.GrainId.ToString(), Name = entry.ReminderName, Period = entry.Period, - StartAt = entry.StartAt, - ETag = BitConverter.GetBytes(ulong.Parse(entry.ETag)) + StartAt = entry.StartAt }; + + if (!string.IsNullOrWhiteSpace(entry.ETag)) + { + record.ETag = BitConverter.GetBytes(ulong.Parse(entry.ETag)); + } + + return record; } private ReminderEntry ConvertToEntity(ReminderRecord record) diff --git a/src/Orleans.Core/Properties/AssemblyInfo.cs b/src/Orleans.Core/Properties/AssemblyInfo.cs index 2e63df2f009..91d754f96e4 100644 --- a/src/Orleans.Core/Properties/AssemblyInfo.cs +++ b/src/Orleans.Core/Properties/AssemblyInfo.cs @@ -16,6 +16,7 @@ [assembly: InternalsVisibleTo("Tester")] [assembly: InternalsVisibleTo("Tester.AzureUtils")] [assembly: InternalsVisibleTo("Tester.Cosmos")] +[assembly: InternalsVisibleTo("Tester.EFCore")] [assembly: InternalsVisibleTo("Tester.AdoNet")] [assembly: InternalsVisibleTo("Tester.Redis")] [assembly: InternalsVisibleTo("Tester.ZooKeeperUtils")] diff --git a/test/Extensions/Tester.EFCore/CollectionFixture.cs b/test/Extensions/Tester.EFCore/CollectionFixture.cs index 0434121f3de..7f00e5a1f45 100644 --- a/test/Extensions/Tester.EFCore/CollectionFixture.cs +++ b/test/Extensions/Tester.EFCore/CollectionFixture.cs @@ -1,5 +1,4 @@ using TestExtensions; -using Xunit; namespace Tester.EFCore; diff --git a/test/Extensions/Tester.EFCore/EFCoreFixture.cs b/test/Extensions/Tester.EFCore/EFCoreFixture.cs new file mode 100644 index 00000000000..c49bd76183d --- /dev/null +++ b/test/Extensions/Tester.EFCore/EFCoreFixture.cs @@ -0,0 +1,76 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Orleans.Persistence; +using Orleans.Persistence.EntityFrameworkCore.SqlServer.Data; +using Orleans.Reminders; +using Orleans.Reminders.EntityFrameworkCore.SqlServer.Data; +using Orleans.TestingHost; +using TestExtensions; + +namespace Tester.EFCore; + +public class EFCoreFixture : BaseTestClusterFixture where TDbContext : DbContext +{ + protected override void CheckPreconditionsOrThrow() => EFCoreTestUtils.CheckSqlServer(); + + protected override void ConfigureTestCluster(TestClusterBuilder builder) + { + builder.Options.InitialSilosCount = 4; + builder.AddSiloBuilderConfigurator(); + } + + private class SiloConfigurator : ISiloConfigurator + { + public void Configure(ISiloBuilder hostBuilder) + { + var ctxTypeName = typeof(TDbContext).Name; + var cs = $"Server=localhost,1433;Database=OrleansTests.{ctxTypeName};User Id=sa;Password=yourStrong(!)Password;TrustServerCertificate=True"; + + hostBuilder.Services.AddPooledDbContextFactory(optionsBuilder => + { + optionsBuilder.UseSqlServer(cs, opt => + { + opt.MigrationsHistoryTable("__EFMigrationsHistory"); + opt.MigrationsAssembly(typeof(TDbContext).Assembly.FullName); + }); + }); + + switch (ctxTypeName) + { + case nameof(SqlServerGrainStateDbContext): + hostBuilder + .AddEntityFrameworkCoreSqlServerGrainStorage("GrainStorageForTest"); + break; + case nameof(SqlServerReminderDbContext): + hostBuilder + .UseEntityFrameworkCoreSqlServerReminderService(); + break; + } + + hostBuilder + .AddMemoryGrainStorage("MemoryStore"); + + var sp = new ServiceCollection() + .AddPooledDbContextFactory(optionsBuilder => + { + optionsBuilder.UseSqlServer(cs, opt => + { + opt.MigrationsHistoryTable("__EFMigrationsHistory"); + opt.MigrationsAssembly(typeof(TDbContext).Assembly.FullName); + }); + }).BuildServiceProvider(); + + var factory = sp.GetRequiredService>(); + + var ctx = factory.CreateDbContext(); + if (ctx.Database.GetPendingMigrations().Any()) + { + try + { + ctx.Database.Migrate(); + } + catch { } + } + } + } +} \ No newline at end of file diff --git a/test/Extensions/Tester.EFCore/EFCoreSqlServerGrainDirectoryTests.cs b/test/Extensions/Tester.EFCore/EFCoreSqlServerGrainDirectoryTests.cs new file mode 100644 index 00000000000..b80bdae2a87 --- /dev/null +++ b/test/Extensions/Tester.EFCore/EFCoreSqlServerGrainDirectoryTests.cs @@ -0,0 +1,110 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Orleans.Configuration; +using Orleans.GrainDirectory.EntityFrameworkCore.SqlServer.Data; +using Orleans.Runtime; +using Orleans.TestingHost.Utils; +using Tester.Directories; +using Xunit.Abstractions; + +namespace Tester.EFCore; + +[TestCategory("Reminders"), TestCategory("EFCore"), TestCategory("EFCore-SqlServer")] +public class EFCoreSqlServerGrainDirectoryTests : GrainDirectoryTests> +{ + public EFCoreSqlServerGrainDirectoryTests(ITestOutputHelper testOutputHelper) : base(testOutputHelper) + { + } + + protected override EFCoreGrainDirectory GetGrainDirectory() + { + EFCoreTestUtils.CheckSqlServer(); + + var clusterOptions = new ClusterOptions + { + ClusterId = Guid.NewGuid().ToString("N"), + ServiceId = Guid.NewGuid().ToString("N"), + }; + + var loggerFactory = TestingUtils.CreateDefaultLoggerFactory("EFCoreSqlServerGrainDirectoryTests.log"); + + var cs = "Server=localhost,1433;Database=OrleansTests.GrainDirectory;User Id=sa;Password=yourStrong(!)Password;TrustServerCertificate=True"; + var sp = new ServiceCollection() + .AddPooledDbContextFactory(optionsBuilder => + { + optionsBuilder.UseSqlServer(cs, opt => + { + opt.MigrationsHistoryTable("__EFMigrationsHistory"); + opt.MigrationsAssembly(typeof(SqlServerGrainDirectoryDbContext).Assembly.FullName); + }); + }).BuildServiceProvider(); + + var factory = sp.GetRequiredService>(); + + var ctx = factory.CreateDbContext(); + if (ctx.Database.GetPendingMigrations().Any()) + { + try + { + ctx.Database.Migrate(); + } + catch { } + } + + var directory = new EFCoreGrainDirectory(loggerFactory, factory, Options.Create(clusterOptions)); + + return directory; + } + + [SkippableFact] + public async Task UnregisterMany() + { + const int N = 25; + const int R = 4; + + // Create and insert N entries + var addresses = new List(); + for (var i = 0; i < N; i++) + { + var addr = new GrainAddress + { + ActivationId = ActivationId.NewId(), + GrainId = GrainId.Parse("user/someraondomuser_" + Guid.NewGuid().ToString("N")), + SiloAddress = SiloAddress.FromParsableString("10.0.23.12:1000@5678"), + MembershipVersion = new MembershipVersion(51) + }; + addresses.Add(addr); + await this.grainDirectory.Register(addr, previousAddress: null); + } + + // Modify the Rth entry locally, to simulate another activation tentative by another silo + var ra = addresses[R]; + var oldActivation = ra.ActivationId; + addresses[R] = new() + { + GrainId = ra.GrainId, + SiloAddress = ra.SiloAddress, + MembershipVersion = ra.MembershipVersion, + ActivationId = ActivationId.NewId() + }; + + // Batch unregister + await this.grainDirectory.UnregisterMany(addresses); + + // Now we should only find the old Rth entry + for (int i = 0; i < N; i++) + { + if (i == R) + { + var addr = await this.grainDirectory.Lookup(addresses[i].GrainId); + Assert.NotNull(addr); + Assert.Equal(oldActivation, addr.ActivationId); + } + else + { + Assert.Null(await this.grainDirectory.Lookup(addresses[i].GrainId)); + } + } + } +} \ No newline at end of file diff --git a/test/Extensions/Tester.EFCore/EFCoreSqlServerMembershipTableTests.cs b/test/Extensions/Tester.EFCore/EFCoreSqlServerMembershipTableTests.cs new file mode 100644 index 00000000000..f1f619cdba1 --- /dev/null +++ b/test/Extensions/Tester.EFCore/EFCoreSqlServerMembershipTableTests.cs @@ -0,0 +1,128 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Orleans.Messaging; +using Orleans.Clustering.EntityFrameworkCore.SqlServer.Data; +using TestExtensions; +using UnitTests; +using UnitTests.MembershipTests; + +namespace Tester.EFCore; + +[TestCategory("Membership"), TestCategory("EFCore"), TestCategory("EFCore-SqlServer")] +public class EFCoreSqlServerMembershipTableTests : MembershipTableTestsBase +{ + public EFCoreSqlServerMembershipTableTests(ConnectionStringFixture fixture, TestEnvironmentFixture environment) : base(fixture, environment, CreateFilters()) + { + EFCoreTestUtils.CheckSqlServer(); + } + + private static LoggerFilterOptions CreateFilters() + { + var filters = new LoggerFilterOptions(); + filters.AddFilter(nameof(EFCoreSqlServerMembershipTableTests), LogLevel.Trace); + return filters; + } + + protected override IMembershipTable CreateMembershipTable(ILogger logger) + { + return new EFMembershipTable(this.loggerFactory, this._clusterOptions, this.GetFactory()); + } + + protected override Task GetConnectionString() + { + var cs = "Server=localhost,1433;Database=OrleansTests.Membership;User Id=sa;Password=yourStrong(!)Password;TrustServerCertificate=True"; + return Task.FromResult(cs); + } + + protected override IGatewayListProvider CreateGatewayListProvider(ILogger logger) + { + return new EFGatewayListProvider(this.loggerFactory, this._clusterOptions, this._gatewayOptions, this.GetFactory()); + } + + private IDbContextFactory GetFactory() + { + var sp = new ServiceCollection() + .AddPooledDbContextFactory(optionsBuilder => + { + optionsBuilder.UseSqlServer(this.connectionString, opt => + { + opt.MigrationsHistoryTable("__EFMigrationsHistory"); + opt.MigrationsAssembly(typeof(SqlServerClusterDbContext).Assembly.FullName); + }); + }).BuildServiceProvider(); + + var factory = sp.GetRequiredService>(); + + var ctx = factory.CreateDbContext(); + if (ctx.Database.GetPendingMigrations().Any()) + { + try + { + ctx.Database.Migrate(); + } + catch { } + } + + return factory; + } + + [SkippableFact] + public void MembershipTable_SqlServer_Init() + { + } + + [SkippableFact] + public async Task MembershipTable_SqlServer_GetGateways() + { + await MembershipTable_GetGateways(); + } + + [SkippableFact] + public async Task MembershipTable_SqlServer_ReadAll_EmptyTable() + { + await MembershipTable_ReadAll_EmptyTable(); + } + + [SkippableFact] + public async Task MembershipTable_SqlServer_InsertRow() + { + await MembershipTable_InsertRow(); + } + + [SkippableFact] + public async Task MembershipTable_SqlServer_ReadRow_Insert_Read() + { + await MembershipTable_ReadRow_Insert_Read(); + } + + [SkippableFact] + public async Task MembershipTable_SqlServer_ReadAll_Insert_ReadAll() + { + await MembershipTable_ReadAll_Insert_ReadAll(); + } + + [SkippableFact] + public async Task MembershipTable_SqlServer_UpdateRow() + { + await MembershipTable_UpdateRow(); + } + + [SkippableFact] + public async Task MembershipTable_SqlServer_UpdateRowInParallel() + { + await MembershipTable_UpdateRowInParallel(); + } + + [SkippableFact] + public async Task MembershipTable_SqlServer_UpdateIAmAlive() + { + await MembershipTable_UpdateIAmAlive(); + } + + [SkippableFact] + public async Task MembershipTableSqlServerSql_CleanupDefunctSiloEntries() + { + await MembershipTable_CleanupDefunctSiloEntries(); + } +} \ No newline at end of file diff --git a/test/Extensions/Tester.EFCore/EFCoreTestUtils.cs b/test/Extensions/Tester.EFCore/EFCoreTestUtils.cs new file mode 100644 index 00000000000..4361aef8464 --- /dev/null +++ b/test/Extensions/Tester.EFCore/EFCoreTestUtils.cs @@ -0,0 +1,29 @@ +using System.Net.Sockets; + +namespace Tester.EFCore; + +public static class EFCoreTestUtils +{ + public static void CheckSqlServer() => IsPortOpen("localhost", 1433); + + private static bool IsPortOpen(string host, int port) + { + using var client = new TcpClient(); + try + { + var result = client.BeginConnect(host, port, null, null); + var success = result.AsyncWaitHandle.WaitOne(TimeSpan.FromMilliseconds(300)); + if (!success) + { + throw new TimeoutException("Connection timed out."); + } + + client.EndConnect(result); + return true; + } + catch + { + throw new SkipException(); + } + } +} \ No newline at end of file diff --git a/test/Extensions/Tester.EFCore/PersistenceGrainTests_EFCoreSqlServerGrainStorage.cs b/test/Extensions/Tester.EFCore/PersistenceGrainTests_EFCoreSqlServerGrainStorage.cs new file mode 100644 index 00000000000..d5047f74e7f --- /dev/null +++ b/test/Extensions/Tester.EFCore/PersistenceGrainTests_EFCoreSqlServerGrainStorage.cs @@ -0,0 +1,46 @@ +using Orleans.Persistence.EntityFrameworkCore.SqlServer.Data; +using TestExtensions; +using TestExtensions.Runners; +using Xunit.Abstractions; + +namespace Tester.EFCore; + +[TestCategory("Persistence"), TestCategory("EFCore"), TestCategory("EFCore-SqlServer")] +public class PersistenceGrainTests_EFCoreSqlServerGrainStorage : OrleansTestingBase, IClassFixture> +{ + private readonly GrainPersistenceTestsRunner _runner; + + public PersistenceGrainTests_EFCoreSqlServerGrainStorage( + ITestOutputHelper output, EFCoreFixture fixture, string grainNamespace = "UnitTests.Grains") + { + fixture.EnsurePreconditionsMet(); + this._runner = new GrainPersistenceTestsRunner(output, fixture, grainNamespace); + } + + [SkippableFact, TestCategory("Functional")] + public async Task Grain_EFCoreSqlServerGrainStorage_Delete() => await _runner.Grain_GrainStorage_Delete(); + + [SkippableFact, TestCategory("Functional")] + public async Task Grain_EFCoreSqlServerGrainStorage_Read() => await _runner.Grain_GrainStorage_Read(); + + [SkippableFact, TestCategory("Functional")] + public async Task Grain_GuidKey_EFCoreSqlServerGrainStorage_Read_Write() => await _runner.Grain_GuidKey_GrainStorage_Read_Write(); + + [SkippableFact, TestCategory("Functional")] + public async Task Grain_LongKey_EFCoreSqlServerGrainStorage_Read_Write() => await _runner.Grain_LongKey_GrainStorage_Read_Write(); + + [SkippableFact, TestCategory("Functional")] + public async Task Grain_LongKeyExtended_EFCoreSqlServerGrainStorage_Read_Write() => await _runner.Grain_LongKeyExtended_GrainStorage_Read_Write(); + + [SkippableFact, TestCategory("Functional")] + public async Task Grain_GuidKeyExtended_EFCoreSqlServerGrainStorage_Read_Write() => await _runner.Grain_GuidKeyExtended_GrainStorage_Read_Write(); + + [SkippableFact, TestCategory("Functional")] + public async Task Grain_Generic_EFCoreSqlServerGrainStorage_Read_Write() => await _runner.Grain_Generic_GrainStorage_Read_Write(); + + [SkippableFact, TestCategory("Functional")] + public async Task Grain_Generic_EFCoreSqlServerGrainStorage_DiffTypes() => await _runner.Grain_Generic_GrainStorage_DiffTypes(); + + [SkippableFact, TestCategory("Functional")] + public async Task Grain_EFCoreSqlServerGrainStorage_SiloRestart() => await _runner.Grain_GrainStorage_SiloRestart(); +} \ No newline at end of file diff --git a/test/Extensions/Tester.EFCore/PersistenceProviderTests_EFCoreSqlServer.cs b/test/Extensions/Tester.EFCore/PersistenceProviderTests_EFCoreSqlServer.cs new file mode 100644 index 00000000000..6d46e7be437 --- /dev/null +++ b/test/Extensions/Tester.EFCore/PersistenceProviderTests_EFCoreSqlServer.cs @@ -0,0 +1,274 @@ +using System.Diagnostics; +using System.Globalization; +using System.Text.Json.Serialization; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Orleans.Configuration; +using Orleans.Persistence.EntityFrameworkCore.SqlServer.Data; +using Orleans.Providers; +using Orleans.Runtime; +using Orleans.Storage; +using TestExtensions; +using UnitTests.Persistence; +using Xunit.Abstractions; + +namespace Tester.EFCore; + +[Collection(TestEnvironmentFixture.DefaultCollection)] +[TestCategory("Persistence"), TestCategory("EFCore"), TestCategory("EFCore-SqlServer")] +public class PersistenceProviderTests_EFCoreSqlServer +{ + private readonly IProviderRuntime _providerRuntime; + private readonly ITestOutputHelper _output; + private readonly TestEnvironmentFixture _fixture; + private readonly string _clusterId; + private readonly string _serviceId; + + public PersistenceProviderTests_EFCoreSqlServer( + ITestOutputHelper output, + TestEnvironmentFixture fixture) + { + EFCoreTestUtils.CheckSqlServer(); + + this._output = output; + this._fixture = fixture; + this._providerRuntime = new ClientProviderRuntime( + this._fixture.InternalGrainFactory, + this._fixture.Services, + this._fixture.Services.GetRequiredService()); + this._clusterId = Guid.NewGuid().ToString("N"); + this._serviceId = Guid.NewGuid().ToString("N"); + } + + private async Task> InitializeStorage() + { + var clusterOptions = Options.Create(new ClusterOptions {ClusterId = _clusterId, ServiceId = _serviceId}); + var loggerFactory = this._providerRuntime.ServiceProvider.GetRequiredService(); + var lifecycle = ActivatorUtilities.CreateInstance(this._providerRuntime.ServiceProvider); + + var cs = "Server=localhost,1433;Database=OrleansTests.Generic;User Id=sa;Password=yourStrong(!)Password;TrustServerCertificate=True"; + + var sp = new ServiceCollection() + .AddPooledDbContextFactory(optionsBuilder => + { + optionsBuilder.UseSqlServer(cs, opt => + { + opt.MigrationsHistoryTable("__EFMigrationsHistory"); + opt.MigrationsAssembly(typeof(SqlServerGrainStateDbContext).Assembly.FullName); + }); + }).BuildServiceProvider(); + + var factory = sp.GetRequiredService>(); + + var ctx = factory.CreateDbContext(); + if ((await ctx.Database.GetPendingMigrationsAsync()).Any()) + { + try + { + await ctx.Database.MigrateAsync(); + } + catch { } + } + + var store = new EFGrainStorage("TestStorage", + loggerFactory, + clusterOptions, + factory, + this._providerRuntime.ServiceProvider); + + store.Participate(lifecycle); + + await lifecycle.OnStart(); + + return store; + } + + [SkippableFact, TestCategory("Functional")] + public async Task PersistenceProvider_Read() + { + const string testName = nameof(PersistenceProvider_Read); + + var store = await InitializeStorage(); + await Test_PersistenceProvider_Read(testName, store, null, grainId: GrainId.Create("TestGrain", Guid.NewGuid().ToString())); + } + + [SkippableTheory, TestCategory("Functional")] + [InlineData(null)] + [InlineData(15 * 64 * 1024 - 256)] + [InlineData(15 * 32 * 1024 - 256)] + public async Task PersistenceProvider_WriteRead(int? stringLength) + { + var testName = string.Format("{0}({1} = {2})", + nameof(PersistenceProvider_WriteRead), + nameof(stringLength), stringLength == null ? "default" : stringLength.ToString()); + + var grainState = TestStoreGrainState.NewRandomState(stringLength); + + var store = await InitializeStorage(); + + await Test_PersistenceProvider_WriteRead(testName, store, grainState, GrainId.Create("TestGrain", Guid.NewGuid().ToString())); + } + + [SkippableTheory, TestCategory("Functional")] + [InlineData(null)] + [InlineData(15 * 64 * 1024 - 256)] + [InlineData(15 * 32 * 1024 - 256)] + public async Task PersistenceProvider_WriteClearRead(int? stringLength) + { + var testName = string.Format("{0}({1} = {2})", + nameof(PersistenceProvider_WriteClearRead), + nameof(stringLength), stringLength == null ? "default" : stringLength.ToString()); + + var grainState = TestStoreGrainState.NewRandomState(stringLength); + + var store = await InitializeStorage(); + + await Test_PersistenceProvider_WriteClearRead(testName, store, grainState); + } + + [SkippableTheory, TestCategory("Functional")] + [InlineData(null)] + [InlineData(15 * 32 * 1024 - 256)] + public async Task PersistenceProvider_ChangeReadFormat(int? stringLength) + { + var testName = string.Format("{0}({1} = {2})", + nameof(PersistenceProvider_ChangeReadFormat), + nameof(stringLength), stringLength == null ? "default" : stringLength.ToString()); + + var grainState = TestStoreGrainState.NewRandomState(stringLength); + var grainId = GrainId.Create("TestGrain", Guid.NewGuid().ToString()); + + var store = await InitializeStorage(); + + grainState = await Test_PersistenceProvider_WriteRead(testName, store, grainState, grainId); + + store = await InitializeStorage(); + + await Test_PersistenceProvider_Read(testName, store, grainState, grainId); + } + + [SkippableTheory, TestCategory("Functional")] + [InlineData(null)] + [InlineData(15 * 32 * 1024 - 256)] + public async Task PersistenceProvider_ChangeWriteFormat(int? stringLength) + { + var testName = string.Format("{0}({1}={2})", + nameof(PersistenceProvider_ChangeWriteFormat), + nameof(stringLength), stringLength == null ? "default" : stringLength.ToString()); + + var grainState = TestStoreGrainState.NewRandomState(stringLength); + + var grainId = GrainId.Create("TestGrain", Guid.NewGuid().ToString()); + + var store = await InitializeStorage(); + + await Test_PersistenceProvider_WriteRead(testName, store, grainState, grainId); + + grainState = TestStoreGrainState.NewRandomState(stringLength); + grainState.ETag = "*"; + + store = await InitializeStorage(); + + await Test_PersistenceProvider_WriteRead(testName, store, grainState, grainId); + } + + private async Task Test_PersistenceProvider_Read(string grainTypeName, IGrainStorage store, GrainState grainState, GrainId grainId) + { + grainState ??= new GrainState(new TestStoreGrainState()); + + var storedGrainState = new GrainState(new TestStoreGrainState()); + + var sw = new Stopwatch(); + sw.Start(); + + await store.ReadStateAsync(grainTypeName, grainId, storedGrainState); + + var readTime = sw.Elapsed; + this._output.WriteLine("{0} - Read time = {1}", store.GetType().FullName, readTime); + + var storedState = storedGrainState.State; + Assert.Equal(grainState.State.A, storedState.A); + Assert.Equal(grainState.State.B, storedState.B); + Assert.Equal(grainState.State.C, storedState.C); + } + + private async Task> Test_PersistenceProvider_WriteRead(string grainTypeName, + IGrainStorage store, GrainState grainState, GrainId grainId) + { + grainState ??= TestStoreGrainState.NewRandomState(); + + var sw = new Stopwatch(); + sw.Start(); + + await store.WriteStateAsync(grainTypeName, grainId, grainState); + + var writeTime = sw.Elapsed; + sw.Restart(); + + var storedGrainState = new GrainState {State = new TestStoreGrainState()}; + await store.ReadStateAsync(grainTypeName, grainId, storedGrainState); + var readTime = sw.Elapsed; + this._output.WriteLine("{0} - Write time = {1} Read time = {2}", store.GetType().FullName, writeTime, readTime); + Assert.Equal(grainState.State.A, storedGrainState.State.A); + Assert.Equal(grainState.State.B, storedGrainState.State.B); + Assert.Equal(grainState.State.C, storedGrainState.State.C); + + return storedGrainState; + } + + private async Task Test_PersistenceProvider_WriteClearRead(string grainTypeName, + IGrainStorage store, GrainState grainState = null, GrainId grainId = default) + { + grainId = this._fixture.InternalGrainFactory.GetGrain(grainId.IsDefault ? LegacyGrainId.NewId().ToGrainId() : grainId).GetGrainId(); + + grainState ??= TestStoreGrainState.NewRandomState(); + + var sw = new Stopwatch(); + sw.Start(); + + await store.WriteStateAsync(grainTypeName, grainId, grainState); + + var writeTime = sw.Elapsed; + sw.Restart(); + + await store.ClearStateAsync(grainTypeName, grainId, grainState); + + var storedGrainState = new GrainState {State = new TestStoreGrainState()}; + await store.ReadStateAsync(grainTypeName, grainId, storedGrainState); + var readTime = sw.Elapsed; + this._output.WriteLine("{0} - Write time = {1} Read time = {2}", store.GetType().FullName, writeTime, readTime); + Assert.NotNull(storedGrainState.State); + Assert.Equal(default, storedGrainState.State.A); + Assert.Equal(default, storedGrainState.State.B); + Assert.Equal(default, storedGrainState.State.C); + } + + public class TestStoreGrainStateWithCustomJsonProperties + { + [JsonPropertyName("s")] public string String { get; set; } + + internal static GrainState NewRandomState(int? aPropertyLength = null) => + new() + { + State = new TestStoreGrainStateWithCustomJsonProperties + { + String = aPropertyLength == null + ? Random.Shared.Next().ToString(CultureInfo.InvariantCulture) + : GenerateRandomDigitString(aPropertyLength.Value) + } + }; + + private static string GenerateRandomDigitString(int stringLength) + { + var characters = new char[stringLength]; + for (var i = 0; i < stringLength; ++i) + { + characters[i] = (char)Random.Shared.Next('0', '9' + 1); + } + + return new string(characters); + } + } +} \ No newline at end of file diff --git a/test/Extensions/Tester.EFCore/ReminderTests_EFCoreSqlServer.cs b/test/Extensions/Tester.EFCore/ReminderTests_EFCoreSqlServer.cs new file mode 100644 index 00000000000..7c089de6ae8 --- /dev/null +++ b/test/Extensions/Tester.EFCore/ReminderTests_EFCoreSqlServer.cs @@ -0,0 +1,298 @@ +using Microsoft.Extensions.Logging; +using Orleans.Runtime; +using Orleans.Internal; +using Orleans.Reminders.EntityFrameworkCore.SqlServer.Data; +using UnitTests.TimerTests; +using UnitTests.GrainInterfaces; + +namespace Tester.EFCore; + +[TestCategory("Reminders"), TestCategory("EFCore"), TestCategory("EFCore-SqlServer")] +public class ReminderTests_EFCoreSqlServer : ReminderTests_Base, IClassFixture> +{ + public ReminderTests_EFCoreSqlServer(EFCoreFixture fixture) : base(fixture) + { + } + + // Basic tests + [SkippableFact, TestCategory("Functional")] + public async Task Rem_EFCoreSqlServer_Basic_StopByRef() + { + await Test_Reminders_Basic_StopByRef(); + } + + [SkippableFact, TestCategory("Functional")] + public async Task Rem_EFCoreSqlServer_Basic_ListOps() + { + await Test_Reminders_Basic_ListOps(); + } + + // Single join tests ... multi grain, multi reminders + + [SkippableFact, TestCategory("Functional")] + public async Task Rem_EFCoreSqlServer_1J_MultiGrainMultiReminders() + { + await Test_Reminders_1J_MultiGrainMultiReminders(); + } + + [SkippableFact, TestCategory("Functional")] + public async Task Rem_EFCoreSqlServer_ReminderNotFound() + { + await Test_Reminders_ReminderNotFound(); + } + + [SkippableFact, TestCategory("Functional")] + public async Task Rem_EFCoreSqlServer_Basic() + { + // start up a test grain and get the period that it's programmed to use. + var grain = GrainFactory.GetGrain(Guid.NewGuid()); + var period = await grain.GetReminderPeriod(DR); + // start up the 'DR' reminder and wait for two ticks to pass. + await grain.StartReminder(DR); + Thread.Sleep(period.Multiply(2) + LEEWAY); // giving some leeway + // retrieve the value of the counter-- it should match the sequence number which is the number of periods + // we've waited. + var last = await grain.GetCounter(DR); + Assert.Equal(2, last); + // stop the timer and wait for a whole period. + await grain.StopReminder(DR); + Thread.Sleep(period.Multiply(1) + LEEWAY); // giving some leeway + // the counter should not have changed. + var curr = await grain.GetCounter(DR); + Assert.Equal(last, curr); + } + + [SkippableFact, TestCategory("Functional")] + public async Task Rem_EFCoreSqlServer_Basic_Restart() + { + var grain = GrainFactory.GetGrain(Guid.NewGuid()); + var period = await grain.GetReminderPeriod(DR); + await grain.StartReminder(DR); + Thread.Sleep(period.Multiply(2) + LEEWAY); // giving some leeway + var last = await grain.GetCounter(DR); + Assert.Equal(2, last); + + await grain.StopReminder(DR); + var sleepFor = period.Multiply(1) + LEEWAY; + Thread.Sleep(sleepFor); // giving some leeway + var curr = await grain.GetCounter(DR); + Assert.Equal(last, curr); + AssertIsInRange(curr, last, last + 1, grain, DR, sleepFor); + + // start the same reminder again + await grain.StartReminder(DR); + sleepFor = period.Multiply(2) + LEEWAY; + Thread.Sleep(sleepFor); // giving some leeway + curr = await grain.GetCounter(DR); + AssertIsInRange(curr, 2, 3, grain, DR, sleepFor); + await grain.StopReminder(DR); // cleanup + } + + [SkippableFact, TestCategory("Functional")] + public async Task Rem_EFCoreSqlServer_MultipleReminders() + { + var grain = GrainFactory.GetGrain(Guid.NewGuid()); + await PerGrainMultiReminderTest(grain); + } + + [SkippableFact, TestCategory("Functional")] + public async Task Rem_EFCoreSqlServer_2J_MultiGrainMultiReminders() + { + var g1 = GrainFactory.GetGrain(Guid.NewGuid()); + var g2 = GrainFactory.GetGrain(Guid.NewGuid()); + var g3 = GrainFactory.GetGrain(Guid.NewGuid()); + var g4 = GrainFactory.GetGrain(Guid.NewGuid()); + var g5 = GrainFactory.GetGrain(Guid.NewGuid()); + + var period = await g1.GetReminderPeriod(DR); + + Task[] tasks = {Task.Run(() => PerGrainMultiReminderTestChurn(g1)), Task.Run(() => PerGrainMultiReminderTestChurn(g2)), Task.Run(() => PerGrainMultiReminderTestChurn(g3)), Task.Run(() => PerGrainMultiReminderTestChurn(g4)), Task.Run(() => PerGrainMultiReminderTestChurn(g5)),}; + + await Task.Delay(period.Multiply(5)); + + // start two extra silos ... although it will take it a while before they stabilize + log.LogInformation("Starting 2 extra silos"); + + await HostedCluster.StartAdditionalSilosAsync(2, true); + await HostedCluster.WaitForLivenessToStabilizeAsync(); + + //Block until all tasks complete. + await Task.WhenAll(tasks).WithTimeout(ENDWAIT); + } + + [SkippableFact, TestCategory("Functional")] + public async Task Rem_EFCoreSqlServer_MultiGrainMultiReminders() + { + var g1 = GrainFactory.GetGrain(Guid.NewGuid()); + var g2 = GrainFactory.GetGrain(Guid.NewGuid()); + var g3 = GrainFactory.GetGrain(Guid.NewGuid()); + var g4 = GrainFactory.GetGrain(Guid.NewGuid()); + var g5 = GrainFactory.GetGrain(Guid.NewGuid()); + + Task[] tasks = {Task.Run(() => PerGrainMultiReminderTest(g1)), Task.Run(() => PerGrainMultiReminderTest(g2)), Task.Run(() => PerGrainMultiReminderTest(g3)), Task.Run(() => PerGrainMultiReminderTest(g4)), Task.Run(() => PerGrainMultiReminderTest(g5)),}; + + //Block until all tasks complete. + await Task.WhenAll(tasks).WithTimeout(ENDWAIT); + } + + [SkippableFact, TestCategory("Functional")] + public async Task Rem_EFCoreSqlServer_1F_Basic() + { + var g1 = GrainFactory.GetGrain(Guid.NewGuid()); + + var period = await g1.GetReminderPeriod(DR); + + var test = Task.Run(async () => + { + await PerGrainFailureTest(g1); + return true; + }); + + Thread.Sleep(period.Multiply(failAfter)); + // stop the secondary silo + log.LogInformation("Stopping secondary silo"); + await HostedCluster.StopSiloAsync(HostedCluster.SecondarySilos.First()); + + await test; // Block until test completes. + } + + [SkippableFact, TestCategory("Functional")] + public async Task Rem_EFCoreSqlServer_2F_MultiGrain() + { + var silos = await HostedCluster.StartAdditionalSilosAsync(2, true); + + var g1 = GrainFactory.GetGrain(Guid.NewGuid()); + var g2 = GrainFactory.GetGrain(Guid.NewGuid()); + var g3 = GrainFactory.GetGrain(Guid.NewGuid()); + var g4 = GrainFactory.GetGrain(Guid.NewGuid()); + var g5 = GrainFactory.GetGrain(Guid.NewGuid()); + + var period = await g1.GetReminderPeriod(DR); + + Task[] tasks = {Task.Run(() => PerGrainFailureTest(g1)), Task.Run(() => PerGrainFailureTest(g2)), Task.Run(() => PerGrainFailureTest(g3)), Task.Run(() => PerGrainFailureTest(g4)), Task.Run(() => PerGrainFailureTest(g5)),}; + + Thread.Sleep(period.Multiply(failAfter)); + + // stop a couple of silos + log.LogInformation("Stopping 2 silos"); + var i = Random.Shared.Next(silos.Count); + await HostedCluster.StopSiloAsync(silos[i]); + silos.RemoveAt(i); + await HostedCluster.StopSiloAsync(silos[Random.Shared.Next(silos.Count)]); + + await Task.WhenAll(tasks).WithTimeout(ENDWAIT); // Block until all tasks complete. + } + + [SkippableFact, TestCategory("Functional")] + public async Task Rem_EFCoreSqlServer_1F1J_MultiGrain() + { + var silos = await HostedCluster.StartAdditionalSilosAsync(1); + await HostedCluster.WaitForLivenessToStabilizeAsync(); + + var g1 = GrainFactory.GetGrain(Guid.NewGuid()); + var g2 = GrainFactory.GetGrain(Guid.NewGuid()); + var g3 = GrainFactory.GetGrain(Guid.NewGuid()); + var g4 = GrainFactory.GetGrain(Guid.NewGuid()); + var g5 = GrainFactory.GetGrain(Guid.NewGuid()); + + var period = await g1.GetReminderPeriod(DR); + + Task[] tasks = {Task.Run(() => PerGrainFailureTest(g1)), Task.Run(() => PerGrainFailureTest(g2)), Task.Run(() => PerGrainFailureTest(g3)), Task.Run(() => PerGrainFailureTest(g4)), Task.Run(() => PerGrainFailureTest(g5)),}; + + Thread.Sleep(period.Multiply(failAfter)); + + var siloToKill = silos[Random.Shared.Next(silos.Count)]; + // stop a silo and join a new one in parallel + log.LogInformation("Stopping a silo and joining a silo"); + Task t1 = Task.Factory.StartNew(async () => await HostedCluster.StopSiloAsync(siloToKill)); + var t2 = HostedCluster.StartAdditionalSilosAsync(1, true).ContinueWith(t => + { + t.GetAwaiter().GetResult(); + }); + await Task.WhenAll(new[] {t1, t2}).WithTimeout(ENDWAIT); + + await Task.WhenAll(tasks).WithTimeout(ENDWAIT); // Block until all tasks complete. + log.LogInformation("\n\n\nReminderTest_1F1J_MultiGrain passed OK.\n\n\n"); + } + + [SkippableFact, TestCategory("Functional")] + public async Task Rem_EFCoreSqlServer_RegisterSameReminderTwice() + { + var grain = GrainFactory.GetGrain(Guid.NewGuid()); + var promise1 = grain.StartReminder(DR); + var promise2 = grain.StartReminder(DR); + Task[] tasks = {promise1, promise2}; + await Task.WhenAll(tasks).WithTimeout(TimeSpan.FromSeconds(15)); + //Assert.NotEqual(promise1.Result, promise2.Result); + // TODO: write tests where period of a reminder is changed + } + + [SkippableFact, TestCategory("Functional")] + public async Task Rem_EFCoreSqlServer_GT_Basic() + { + var g1 = GrainFactory.GetGrain(Guid.NewGuid()); + var g2 = GrainFactory.GetGrain(Guid.NewGuid()); + var period = await g1.GetReminderPeriod(DR); // using same period + + await g1.StartReminder(DR); + Thread.Sleep(period.Multiply(2) + LEEWAY); // giving some leeway + await g2.StartReminder(DR); + Thread.Sleep(period.Multiply(2) + LEEWAY); // giving some leeway + var last1 = await g1.GetCounter(DR); + Assert.Equal(4, last1); + var last2 = await g2.GetCounter(DR); + Assert.Equal(2, last2); // CopyGrain fault + + await g1.StopReminder(DR); + Thread.Sleep(period.Multiply(2) + LEEWAY); // giving some leeway + await g2.StopReminder(DR); + var curr1 = await g1.GetCounter(DR); + Assert.Equal(last1, curr1); + var curr2 = await g2.GetCounter(DR); + Assert.Equal(4, curr2); // CopyGrain fault + } + + [SkippableFact(Skip = "https://github.com/dotnet/orleans/issues/4319"), TestCategory("Functional")] + public async Task Rem_EFCoreSqlServer_GT_1F1J_MultiGrain() + { + var silos = await HostedCluster.StartAdditionalSilosAsync(1); + await HostedCluster.WaitForLivenessToStabilizeAsync(); + + var g1 = GrainFactory.GetGrain(Guid.NewGuid()); + var g2 = GrainFactory.GetGrain(Guid.NewGuid()); + var g3 = GrainFactory.GetGrain(Guid.NewGuid()); + var g4 = GrainFactory.GetGrain(Guid.NewGuid()); + + var period = await g1.GetReminderPeriod(DR); + + Task[] tasks = {Task.Run(() => PerGrainFailureTest(g1)), Task.Run(() => PerGrainFailureTest(g2)), Task.Run(() => PerCopyGrainFailureTest(g3)), Task.Run(() => PerCopyGrainFailureTest(g4)),}; + + Thread.Sleep(period.Multiply(failAfter)); + + var siloToKill = silos[Random.Shared.Next(silos.Count)]; + // stop a silo and join a new one in parallel + log.LogInformation("Stopping a silo and joining a silo"); + var t1 = Task.Run(async () => await HostedCluster.StopSiloAsync(siloToKill)); + Task t2 = Task.Run(async () => await HostedCluster.StartAdditionalSilosAsync(1)); + await Task.WhenAll(new[] {t1, t2}).WithTimeout(ENDWAIT); + + await Task.WhenAll(tasks).WithTimeout(ENDWAIT); // Block until all tasks complete. + } + + [SkippableFact, TestCategory("Functional")] + public async Task Rem_EFCoreSqlServer_Wrong_LowerThanAllowedPeriod() + { + var grain = GrainFactory.GetGrain(Guid.NewGuid()); + await Assert.ThrowsAsync(() => + grain.StartReminder(DR, TimeSpan.FromMilliseconds(3000), true)); + } + + [SkippableFact, TestCategory("Functional")] + public async Task Rem_EFCoreSqlServer_Wrong_Grain() + { + var grain = GrainFactory.GetGrain(0); + + await Assert.ThrowsAsync(() => + grain.StartReminder(DR)); + } +} \ No newline at end of file diff --git a/test/Extensions/Tester.EFCore/ReminderTests_EFCoreSqlServer_Standalone.cs b/test/Extensions/Tester.EFCore/ReminderTests_EFCoreSqlServer_Standalone.cs new file mode 100644 index 00000000000..667c37630f3 --- /dev/null +++ b/test/Extensions/Tester.EFCore/ReminderTests_EFCoreSqlServer_Standalone.cs @@ -0,0 +1,170 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Orleans.Configuration; +using Orleans.Internal; +using Orleans.Reminders.EntityFrameworkCore.SqlServer.Data; +using Orleans.Runtime; +using Orleans.TestingHost.Utils; +using TestExtensions; +using Xunit.Abstractions; + +namespace Tester.EFCore; + +[Collection(TestEnvironmentFixture.DefaultCollection)] +[TestCategory("Reminders"), TestCategory("EFCore"), TestCategory("EFCore-SqlServer")] +public class ReminderTests_EFCoreSqlServer_Standalone +{ + private readonly ITestOutputHelper _output; + private readonly TestEnvironmentFixture _fixture; + private readonly string _serviceId; + private readonly ILogger _log; + private readonly ILoggerFactory _loggerFactory; + + public ReminderTests_EFCoreSqlServer_Standalone(ITestOutputHelper output, TestEnvironmentFixture fixture) + { + EFCoreTestUtils.CheckSqlServer(); + + _output = output; + _fixture = fixture; + _loggerFactory = TestingUtils.CreateDefaultLoggerFactory($"{GetType().Name}.log"); + _log = _loggerFactory.CreateLogger(); + + _serviceId = Guid.NewGuid().ToString(); + + TestUtils.ConfigureClientThreadPoolSettingsForStorageTests(1000); + } + + [SkippableFact, TestCategory("Reminders"), TestCategory("Performance")] + public async Task Reminders_AzureTable_InsertRate() + { + IReminderTable table = this.GetReminderTable("TMSLocalTesting"); + await table.Init(); + + await TestTableInsertRate(table, 10); + await TestTableInsertRate(table, 500); + } + + [SkippableFact, TestCategory("Reminders")] + public async Task Reminders_AzureTable_InsertNewRowAndReadBack() + { + var clusterId = NewClusterId(); + IReminderTable table = this.GetReminderTable(clusterId); + await table.Init(); + + ReminderEntry[] rows = (await GetAllRows(table)).ToArray(); + Assert.Empty(rows); // "The reminder table (sid={0}, did={1}) was not empty.", ServiceId, clusterId); + + ReminderEntry expected = NewReminderEntry(); + await table.UpsertRow(expected); + rows = (await GetAllRows(table)).ToArray(); + + Assert.Single(rows); // "The reminder table (sid={0}, did={1}) did not contain the correct number of rows (1).", ServiceId, clusterId); + ReminderEntry actual = rows[0]; + Assert.Equal(expected.GrainId, actual.GrainId); // "The newly inserted reminder table (sid={0}, did={1}) row did not contain the expected grain reference.", ServiceId, clusterId); + Assert.Equal(expected.ReminderName, actual.ReminderName); // "The newly inserted reminder table (sid={0}, did={1}) row did not have the expected reminder name.", ServiceId, clusterId); + Assert.Equal(expected.Period, actual.Period); // "The newly inserted reminder table (sid={0}, did={1}) row did not have the expected period.", ServiceId, clusterId); + // the following assertion fails but i don't know why yet-- the timestamps appear identical in the error message. it's not really a priority to hunt down the reason, however, because i have high confidence it is working well enough for the moment. + /*Assert.Equal(expected.StartAt, actual.StartAt); // "The newly inserted reminder table (sid={0}, did={1}) row did not contain the correct start time.", ServiceId, clusterId);*/ + Assert.False(string.IsNullOrWhiteSpace(actual.ETag), $"The newly inserted reminder table (sid={_serviceId}, did={clusterId}) row contains an invalid etag."); + } + + private async Task TestTableInsertRate(IReminderTable reminderTable, double numOfInserts) + { + DateTime startedAt = DateTime.UtcNow; + + try + { + List> promises = new List>(); + for (int i = 0; i < numOfInserts; i++) + { + //"177BF46E-D06D-44C0-943B-C12F26DF5373" + string s = string.Format("177BF46E-D06D-44C0-943B-C12F26D{0:d5}", i); + + var e = new ReminderEntry + { + //GrainId = LegacyGrainId.GetGrainId(new Guid(s)), + GrainId = _fixture.InternalGrainFactory.GetGrain(LegacyGrainId.NewId()).GetGrainId(), + ReminderName = "MY_REMINDER_" + i, + Period = TimeSpan.FromSeconds(5), + StartAt = DateTime.UtcNow + }; + + int capture = i; + Task promise = Task.Run(async () => + { + await reminderTable.UpsertRow(e); + _output.WriteLine("Done " + capture); + return true; + }); + promises.Add(promise); + _log.LogInformation("Started {Capture}", capture); + } + _log.LogInformation("Started all, now waiting..."); + await Task.WhenAll(promises).WithTimeout(TimeSpan.FromSeconds(500)); + } + catch (Exception exc) + { + _log.LogInformation(exc, "Exception caught"); + } + TimeSpan dur = DateTime.UtcNow - startedAt; + _log.LogInformation( + "Inserted {InsertCount} rows in {Duration}, i.e., {Rate} upserts/sec", + numOfInserts, + dur, + (numOfInserts / dur.TotalSeconds).ToString("f2")); + } + + private ReminderEntry NewReminderEntry() + { + Guid guid = Guid.NewGuid(); + return new ReminderEntry + { + GrainId = _fixture.InternalGrainFactory.GetGrain(LegacyGrainId.NewId()).GetGrainId(), + ReminderName = string.Format("TestReminder.{0}", guid), + Period = TimeSpan.FromSeconds(5), + StartAt = DateTime.UtcNow + }; + } + + private EFReminderTable GetReminderTable(string clusterId) + { + var cs = "Server=localhost,1433;Database=OrleansTests.Reminders;User Id=sa;Password=yourStrong(!)Password;TrustServerCertificate=True"; + var sp = new ServiceCollection() + .AddPooledDbContextFactory(optionsBuilder => + { + optionsBuilder.UseSqlServer(cs, opt => + { + opt.MigrationsHistoryTable("__EFMigrationsHistory"); + opt.MigrationsAssembly(typeof(SqlServerReminderDbContext).Assembly.FullName); + }); + }).BuildServiceProvider(); + + var factory = sp.GetRequiredService>(); + + var ctx = factory.CreateDbContext(); + if (ctx.Database.GetPendingMigrations().Any()) + { + try + { + ctx.Database.Migrate(); + } + catch { } + } + + var clusterOptions = Options.Create(new ClusterOptions { ClusterId = clusterId, ServiceId = _serviceId }); + return new EFReminderTable(this._loggerFactory, clusterOptions, factory); + } + + private string NewClusterId() + { + return string.Format("ReminderTest.{0}", Guid.NewGuid()); + } + + private async Task> GetAllRows(IReminderTable table) + { + ReminderTableData data = await table.ReadRows(0, 0xffffffff); + return data.Reminders; + } +} \ No newline at end of file diff --git a/test/Extensions/Tester.EFCore/Tester.EFCore.csproj b/test/Extensions/Tester.EFCore/Tester.EFCore.csproj index 54db5327556..37781f3a5ec 100644 --- a/test/Extensions/Tester.EFCore/Tester.EFCore.csproj +++ b/test/Extensions/Tester.EFCore/Tester.EFCore.csproj @@ -12,6 +12,10 @@ + + + + diff --git a/test/Extensions/Tester.EFCore/Usings.cs b/test/Extensions/Tester.EFCore/Usings.cs new file mode 100644 index 00000000000..472f817a287 --- /dev/null +++ b/test/Extensions/Tester.EFCore/Usings.cs @@ -0,0 +1,5 @@ +global using Xunit; +global using Orleans.Clustering.EntityFrameworkCore; +global using Orleans.Persistence.EntityFrameworkCore; +global using Orleans.Reminders.EntityFrameworkCore; +global using Orleans.GrainDirectory.EntityFrameworkCore; \ No newline at end of file diff --git a/test/TestInfrastructure/TestExtensions/Properties/AssemblyInfo.cs b/test/TestInfrastructure/TestExtensions/Properties/AssemblyInfo.cs index d344dc5fe5b..e6a32cfd5a8 100644 --- a/test/TestInfrastructure/TestExtensions/Properties/AssemblyInfo.cs +++ b/test/TestInfrastructure/TestExtensions/Properties/AssemblyInfo.cs @@ -4,6 +4,7 @@ [assembly: InternalsVisibleTo("NonSilo.Tests")] [assembly: InternalsVisibleTo("Tester.AzureUtils")] [assembly: InternalsVisibleTo("Tester.Cosmos")] +[assembly: InternalsVisibleTo("Tester.EFCore")] [assembly: InternalsVisibleTo("Tester.AdoNet")] [assembly: InternalsVisibleTo("Tester.Redis")] [assembly: InternalsVisibleTo("AWSUtils.Tests")] diff --git a/test/TesterInternal/Properties/AssemblyInfo.cs b/test/TesterInternal/Properties/AssemblyInfo.cs index e7e5ada4dfb..dfa3f952de2 100644 --- a/test/TesterInternal/Properties/AssemblyInfo.cs +++ b/test/TesterInternal/Properties/AssemblyInfo.cs @@ -5,6 +5,7 @@ [assembly: InternalsVisibleTo("Tester.AzureUtils")] [assembly: InternalsVisibleTo("Tester.Cosmos")] +[assembly: InternalsVisibleTo("Tester.EFCore")] [assembly: InternalsVisibleTo("Tester.AdoNet")] [assembly: InternalsVisibleTo("Tester.Redis")] [assembly: InternalsVisibleTo("AWSUtils.Tests")]