diff --git a/Test/GrainInterfaces/IConstrainedGrain.cs b/Test/GrainInterfaces/IConstrainedGrain.cs deleted file mode 100644 index de5a6fc..0000000 --- a/Test/GrainInterfaces/IConstrainedGrain.cs +++ /dev/null @@ -1,9 +0,0 @@ -using System.Threading.Tasks; - -namespace Orleans.Providers.MongoDB.Test.GrainInterfaces -{ - public interface IConstrainedGrain : IGrainWithIntegerKey - { - Task SetName(string name); - } -} diff --git a/Test/Grains/ConstrainedGrain.cs b/Test/Grains/ConstrainedGrain.cs deleted file mode 100644 index 4a4a8be..0000000 --- a/Test/Grains/ConstrainedGrain.cs +++ /dev/null @@ -1,31 +0,0 @@ -using System; -using System.Threading.Tasks; -using Orleans.Providers.MongoDB.Test.GrainInterfaces; -using Orleans.Runtime; - -namespace Orleans.Providers.MongoDB.Test.Grains -{ - internal class ConstrainedGrain : Grain, IConstrainedGrain - { - private readonly IPersistentState state; - - public ConstrainedGrain([PersistentState(nameof(ConstrainedGrain), "MongoDBStore")] IPersistentState state) - { - this.state = state; - } - - public async Task SetName(string name) - { - state.State.Name = name; - - try - { - await state.WriteStateAsync(); - } - catch (Exception ex) - { - throw new ProviderStateException(ex.Message); - } - } - } -} \ No newline at end of file diff --git a/Test/Grains/ConstrainedGrainState.cs b/Test/Grains/ConstrainedGrainState.cs deleted file mode 100644 index b5c023a..0000000 --- a/Test/Grains/ConstrainedGrainState.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Orleans.Providers.MongoDB.Test.Grains -{ - public class ConstrainedGrainState - { - public string Name { get; set; } - } -} diff --git a/Test/Grains/StreamConsumerGrain.cs b/Test/Grains/StreamConsumerGrain.cs index 3920e8b..5a5e8ba 100644 --- a/Test/Grains/StreamConsumerGrain.cs +++ b/Test/Grains/StreamConsumerGrain.cs @@ -1,5 +1,6 @@ using System; using System.Threading.Tasks; +using Microsoft.Extensions.Logging; using Orleans.Providers.MongoDB.Test.GrainInterfaces; using Orleans.Streams; @@ -7,8 +8,14 @@ namespace Orleans.Providers.MongoDB.Test.Grains { public sealed class StreamConsumerGrain : Grain, IStreamConsumerGrain { + private readonly ILogger logger; private int consumedItems = 0; + public StreamConsumerGrain(ILogger logger) + { + this.logger = logger; + } + public async Task Activate() { var streamProvider = this.GetStreamProvider("OrleansTestStream"); @@ -16,6 +23,7 @@ public async Task Activate() await streamOfNumbers.SubscribeAsync((message, token) => { + this.logger.LogInformation("Grain {GrainId} consumed: {Message}", this.GetPrimaryKeyString(), message); consumedItems++; return Task.CompletedTask; diff --git a/Test/Grains/StreamProducerGrain.cs b/Test/Grains/StreamProducerGrain.cs index aa108e4..7ce691f 100644 --- a/Test/Grains/StreamProducerGrain.cs +++ b/Test/Grains/StreamProducerGrain.cs @@ -12,13 +12,10 @@ public async Task ProduceEvents() var streamProvider = this.GetStreamProvider("OrleansTestStream"); var streamOfNumbers = streamProvider.GetStream("MyNamespace", Guid.Empty); - var i = 0; - - while (true) + for (var i = 0; i < 5; ++i) { - await streamOfNumbers.OnNextAsync(++i); - - await Task.Delay(100); + await streamOfNumbers.OnNextAsync(i); + await Task.Delay(500); } } } diff --git a/Test/Host/Program.cs b/Test/Host/Program.cs index 604f372..bae6213 100644 --- a/Test/Host/Program.cs +++ b/Test/Host/Program.cs @@ -10,14 +10,12 @@ using MongoDB.Bson.Serialization; using MongoDB.Bson.Serialization.Conventions; using MongoDB.Bson.Serialization.Serializers; -using MongoDB.Driver; using Newtonsoft.Json; using Orleans.Configuration; using Orleans.Hosting; using Orleans.Providers.MongoDB.Configuration; using Orleans.Providers.MongoDB.StorageProviders.Serializers; using Orleans.Providers.MongoDB.Test.GrainInterfaces; -using Orleans.Providers.MongoDB.Test.Grains; using Orleans.Runtime; namespace Orleans.Providers.MongoDB.Test.Host @@ -89,8 +87,6 @@ public static async Task Main(string[] args) await host.StartAsync(); - await CreateConstraints(host.Services.GetRequiredService()); - var clientHost = new HostBuilder() .UseOrleansClient((ctx, clientBuilder) => clientBuilder .UseMongoDBClient(mongoRunner.ConnectionString) @@ -125,45 +121,26 @@ public static async Task Main(string[] args) await TestState(client); await TestStateWithCollections(client); - await TestStateConstraints(client); - Console.ReadKey(); await host.StopAsync(); } - private static async Task CreateConstraints(IMongoClient mongoClient) - { - var collection = mongoClient.GetDatabase("OrleansTestApp") - .GetCollection("GrainsConstrainedGrain"); - - var indexBuilder = Builders.IndexKeys.Ascending(f => f.Name); - - var options = new CreateIndexOptions - { - Unique = true - }; - - var indexModel = new CreateIndexModel(indexBuilder, options); - - await collection.Indexes.CreateOneAsync(indexModel); - } - private static async Task TestStreams(IClusterClient client) { var streamProducer = client.GetGrain(0); - var streamConsumer1 = client.GetGrain(0); - var streamConsumer2 = client.GetGrain(0); + var streamConsumer1 = client.GetGrain(1); + var streamConsumer2 = client.GetGrain(2); _ = streamProducer.ProduceEvents(); await streamConsumer1.Activate(); await streamConsumer2.Activate(); - await Task.Delay(1000); + await Task.Delay(3000); var consumed1 = await streamConsumer1.GetConsumedItems(); - var consumed2 = await streamConsumer1.GetConsumedItems(); + var consumed2 = await streamConsumer2.GetConsumedItems(); Console.WriteLine("Consumed Events: {0}/{1}", consumed1, consumed2); } @@ -228,41 +205,6 @@ private static async Task TestState(IClusterClient client) Console.WriteLine(employeeId); } - - private static async Task TestStateConstraints(IClusterClient client) - { - var database = client.ServiceProvider.GetRequiredService() - .GetDatabase("OrleansTestApp"); - - var guid = Guid.NewGuid().ToString(); - - var grain0 = client.GetGrain(0); - await grain0.SetName(guid); - - var filter = Builders.Filter.Eq("_id", "constrained/0"); - var update = Builders.Update.Set("_etag", Guid.NewGuid().ToString()); - - await database.GetCollection("GrainsConstrainedGrain").UpdateOneAsync(filter, update); - - try - { - await grain0.SetName(Guid.NewGuid().ToString()); - } - catch (ProviderStateException ex) - { - Console.WriteLine($"Exception thrown due to invalid e-tag: {ex.Message}"); - } - - var grain1 = client.GetGrain(1); - try - { - await grain1.SetName(guid); - } - catch (ProviderStateException ex) - { - Console.WriteLine($"Exception thrown due to unique constrain violation: {ex.Message}"); - } - } private static void ApplyBsonConfiguration() { diff --git a/UnitTest/Serializers/SerializationTests.cs b/UnitTest/Serializers/SerializationTests.cs index 8627081..00b8387 100644 --- a/UnitTest/Serializers/SerializationTests.cs +++ b/UnitTest/Serializers/SerializationTests.cs @@ -40,6 +40,35 @@ public void CorrectSerializerIsUsed() Assert.IsType(optionsMonitor.Get("BinaryProvider").GrainStateSerializer); } + [Fact] + public void CanCustomizeJsonSerializerSettings() + { + var host = new HostBuilder() + .UseOrleans((ctx, siloBuilder) => + { + siloBuilder + .UseLocalhostClustering() + .AddMongoDBGrainStorage( + "JsonProvider", + options => options.Configure((options, sp) => + { + options.GrainStateSerializer = new JsonGrainStateSerializer( + Options.Create(new JsonGrainStateSerializerOptions + { + ConfigureJsonSerializerSettings = settings => + { + settings.Formatting = Newtonsoft.Json.Formatting.Indented; + } + }), + sp); + })); + }) + .Build(); + + var optionsMonitor = host.Services.GetRequiredService>(); + Assert.IsType(optionsMonitor.Get("JsonProvider").GrainStateSerializer); + } + [Fact] public async void BsonSerializerForPubSubStore_Throws() { diff --git a/UnitTest/Storage/StorageTests.cs b/UnitTest/Storage/StorageTests.cs new file mode 100644 index 0000000..58c1e16 --- /dev/null +++ b/UnitTest/Storage/StorageTests.cs @@ -0,0 +1,67 @@ +using System; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using MongoDB.Bson; +using MongoDB.Driver; +using Orleans.Hosting; +using Orleans.Providers.MongoDB.StorageProviders.Serializers; +using Orleans.Providers.MongoDB.UnitTest.Fixtures; +using Orleans.Runtime; +using TestExtensions; +using Xunit; +using static Orleans.Providers.MongoDB.UnitTest.Storage.TestGrains.StorageTests; + +namespace Orleans.Providers.MongoDB.UnitTest.Storage +{ + [Collection(TestEnvironmentFixture.DefaultCollection)] + public partial class StorageTests + { + [Fact] + public async Task ThrowErrorWhenUpdatingCollectionWithAUniqueIndex() + { + var host = new HostBuilder() + .UseOrleans((ctx, siloBuilder) => + { + siloBuilder.Services + .AddSingletonNamedService(ProviderConstants.DEFAULT_PUBSUB_PROVIDER_NAME); + + siloBuilder + .AddMemoryStreams("OrleansTestStream") + .UseLocalhostClustering() + .UseMongoDBClient(MongoDatabaseFixture.DatabaseConnectionString) + .AddMongoDBGrainStorageAsDefault(options => options.DatabaseName = "OrleansTestApp") + .AddMongoDBGrainStorage(ProviderConstants.DEFAULT_PUBSUB_PROVIDER_NAME, options => options.DatabaseName = "OrleansTestApp"); + }) + .Build(); + + await host.StartAsync(); + + var client = host.Services.GetRequiredService(); + var mongoClient = client.ServiceProvider.GetRequiredService(); + var database = mongoClient.GetDatabase("OrleansTestApp"); + var collection = database.GetCollection("GrainsConstrainedGrain"); + + await collection.Indexes.CreateOneAsync( + new CreateIndexModel( + Builders.IndexKeys.Ascending(f => f.Name), + new CreateIndexOptions { Unique = true })); + + var guid = Guid.NewGuid().ToString(); + + var grain0 = client.GetGrain(0); + await grain0.SetName(guid); + + await database.GetCollection("GrainsConstrainedGrain").UpdateOneAsync( + Builders.Filter.Eq("_id", "constrained/0"), + Builders.Update.Set("_etag", Guid.NewGuid().ToString())); + + var exception0 = await Assert.ThrowsAsync(async () => await grain0.SetName(Guid.NewGuid().ToString())); + Assert.Equal("A write operation resulted in an error. WriteError: { Category : \"DuplicateKey\", Code : 11000, Message : \"E11000 duplicate key error collection: OrleansTestApp.GrainsConstrainedGrain index: _id_ dup key: { _id: \"constrained/0\" }\" }.", exception0.Message); + + var grain1 = client.GetGrain(1); + var exception1 = await Assert.ThrowsAsync(async () => await grain1.SetName(guid)); + Assert.Contains("A write operation resulted in an error. WriteError: { Category : \"DuplicateKey\", Code : 11000, Message : \"E11000 duplicate key error collection: OrleansTestApp.GrainsConstrainedGrain index: Name_1 dup key: { Name: null }\" }.", exception1.Message); + } + } +} diff --git a/UnitTest/Storage/TestGrains/ConstrainedGrain.cs b/UnitTest/Storage/TestGrains/ConstrainedGrain.cs new file mode 100644 index 0000000..8a22a20 --- /dev/null +++ b/UnitTest/Storage/TestGrains/ConstrainedGrain.cs @@ -0,0 +1,33 @@ +using System; +using System.Threading.Tasks; +using Orleans.Runtime; + +namespace Orleans.Providers.MongoDB.UnitTest.Storage.TestGrains +{ + public partial class StorageTests + { + public class ConstrainedGrain : Grain, IConstrainedGrain + { + private readonly IPersistentState state; + + public ConstrainedGrain([PersistentState(nameof(ConstrainedGrain))] IPersistentState state) + { + this.state = state; + } + + public async Task SetName(string name) + { + state.State.Name = name; + + try + { + await state.WriteStateAsync(); + } + catch (Exception ex) + { + throw new ProviderStateException(ex.Message); + } + } + } + } +} diff --git a/UnitTest/Storage/TestGrains/ConstrainedGrainState.cs b/UnitTest/Storage/TestGrains/ConstrainedGrainState.cs new file mode 100644 index 0000000..145668d --- /dev/null +++ b/UnitTest/Storage/TestGrains/ConstrainedGrainState.cs @@ -0,0 +1,10 @@ +namespace Orleans.Providers.MongoDB.UnitTest.Storage.TestGrains +{ + public partial class StorageTests + { + public class ConstrainedGrainState + { + public string Name { get; set; } + } + } +} diff --git a/UnitTest/Storage/TestGrains/IConstrainedGrain.cs b/UnitTest/Storage/TestGrains/IConstrainedGrain.cs new file mode 100644 index 0000000..6978d44 --- /dev/null +++ b/UnitTest/Storage/TestGrains/IConstrainedGrain.cs @@ -0,0 +1,12 @@ +using System.Threading.Tasks; + +namespace Orleans.Providers.MongoDB.UnitTest.Storage.TestGrains +{ + public partial class StorageTests + { + public interface IConstrainedGrain : IGrainWithIntegerKey + { + Task SetName(string name); + } + } +}