diff --git a/Common/Config/AppConfig.cs b/Common/Config/AppConfig.cs index 9178b0a..f13336f 100644 --- a/Common/Config/AppConfig.cs +++ b/Common/Config/AppConfig.cs @@ -2,6 +2,8 @@ public sealed class AppConfig { + public bool OrleansTransactions { get; set; } + public bool SellerViewPostgres { get; set; } public bool ShipmentUpdatePostgres { get; set; } @@ -14,7 +16,7 @@ public sealed class AppConfig public string RedisSecondaryConnectionString { get; set; } - public bool OrleansTransactions { get; set; } + public bool TrackCartHistory { get; set; } public bool OrleansStorage { get; set; } @@ -47,6 +49,7 @@ public override string ToString() " \nUseSwagger: " + UseSwagger + " \nRedisReplication: " + RedisReplication + " \nRedisPrimaryConnectionString: " + RedisPrimaryConnectionString + - " \nRedisSecondaryConnectionString: " + RedisSecondaryConnectionString; + " \nRedisSecondaryConnectionString: " + RedisSecondaryConnectionString + + " \nTrackCartHistory: " + TrackCartHistory; } } diff --git a/Common/Entities/Cart.cs b/Common/Entities/Cart.cs index d1968f6..96ec21e 100644 --- a/Common/Entities/Cart.cs +++ b/Common/Entities/Cart.cs @@ -6,19 +6,18 @@ namespace Common.Entities public sealed class Cart { // no inter identified within an actor. so it requires an id - public int customerId { get; set; } = 0; + public int customerId; public CartStatus status { get; set; } = CartStatus.OPEN; public List items { get; set; } = new List(); - public int instanceId { get; set; } - - // to return - public List divergencies { get; set; } - public Cart() {} + public Cart(int customerId) { + this.customerId = customerId; + } + public override string ToString() { return new StringBuilder().Append("customerId : ").Append(customerId).Append("status").Append(status.ToString()).ToString(); diff --git a/Orleans/Grains/CartActor.cs b/Orleans/Grains/CartActor.cs index a7daf83..9993912 100644 --- a/Orleans/Grains/CartActor.cs +++ b/Orleans/Grains/CartActor.cs @@ -12,11 +12,15 @@ namespace OrleansApp.Grains; public class CartActor : Grain, ICartActor { + private delegate IOrderActor GetOrderActorDelegate(int customerId); protected readonly IPersistentState cart; - protected readonly AppConfig config; + protected readonly bool orleansStorage; + private readonly bool trackHistory; protected int customerId; - protected readonly ILogger logger; private readonly GetOrderActorDelegate callback; + protected readonly ILogger logger; + + private readonly Dictionary> history; public CartActor([PersistentState( stateName: "cart", @@ -25,8 +29,10 @@ public CartActor([PersistentState( ILogger _logger) { this.cart = state; - this.config = options; - this.callback = config.OrleansTransactions ? GetTransactionalOrderActor : GetOrderActor; + this.callback = options.OrleansTransactions ? GetTransactionalOrderActor : GetOrderActor; + this.orleansStorage = options.OrleansStorage; + this.trackHistory = options.TrackCartHistory; + if(this.trackHistory) history = new Dictionary>(); this.logger = _logger; } @@ -34,8 +40,7 @@ public override Task OnActivateAsync(CancellationToken token) { this.customerId = (int) this.GetPrimaryKeyLong(); if(this.cart.State is null) { - this.cart.State = new Cart(); - this.cart.State.customerId = this.customerId; + this.cart.State = new Cart(this.customerId); } return Task.CompletedTask; } @@ -59,8 +64,9 @@ public virtual async Task AddItem(CartItem item) this.cart.State.items.Add(item); - if(config.OrleansStorage) + if(this.orleansStorage){ await this.cart.WriteStateAsync(); + } } // customer decided to checkout @@ -71,6 +77,11 @@ public virtual async Task NotifyCheckout(CustomerCheckout customerCheckout) var checkout = new ReserveStock(DateTime.UtcNow, customerCheckout, this.cart.State.items, customerCheckout.instanceId); this.cart.State.status = CartStatus.CHECKOUT_SENT; try { + if (this.trackHistory) + { + // store cart items internally + this.history.Add(customerCheckout.instanceId, new(this.cart.State.items)); + } await orderActor.Checkout(checkout); await this.Seal(); } catch(Exception e) { @@ -80,7 +91,20 @@ public virtual async Task NotifyCheckout(CustomerCheckout customerCheckout) } } - private delegate IOrderActor GetOrderActorDelegate(int customerId); + public async Task Seal() + { + this.cart.State.status = CartStatus.OPEN; + this.cart.State.items.Clear(); + if(this.orleansStorage) + await this.cart.WriteStateAsync(); + } + + public Task> GetHistory(string tid) + { + if(this.history.ContainsKey(tid)) + return Task.FromResult(this.history[tid]); + return Task.FromResult(new List()); + } private IOrderActor GetOrderActor(int customerId) { @@ -92,12 +116,4 @@ private ITransactionalOrderActor GetTransactionalOrderActor(int customerId) return this.GrainFactory.GetGrain(customerId); } - public async Task Seal() - { - this.cart.State.status = CartStatus.OPEN; - this.cart.State.items.Clear(); - if(this.config.OrleansStorage) - await this.cart.WriteStateAsync(); - } - } \ No newline at end of file diff --git a/Orleans/Grains/Replication/CausalCartActor.cs b/Orleans/Grains/Replication/CausalCartActor.cs index 45e6ee0..9a579d5 100644 --- a/Orleans/Grains/Replication/CausalCartActor.cs +++ b/Orleans/Grains/Replication/CausalCartActor.cs @@ -48,6 +48,7 @@ public override async Task NotifyCheckout(CustomerCheckout customerCheckout) // process new prices as discount if (item.UnitPrice < productReplica.Price) { + item.UnitPrice = productReplica.Price; item.Voucher += productReplica.Price - item.UnitPrice; } } diff --git a/Orleans/Grains/Replication/EventualCartActor.cs b/Orleans/Grains/Replication/EventualCartActor.cs index 9a9d88e..2322034 100644 --- a/Orleans/Grains/Replication/EventualCartActor.cs +++ b/Orleans/Grains/Replication/EventualCartActor.cs @@ -55,7 +55,13 @@ public async Task StopConsuming() private Task UpdateProductAsync(Product product, StreamSequenceToken token) { - this.cachedProducts.Add((product.seller_id, product.product_id), product); + if(this.cachedProducts.ContainsKey((product.seller_id, product.product_id))){ + this.cachedProducts[(product.seller_id, product.product_id)] = product; + } else + { + this.cachedProducts.Add((product.seller_id, product.product_id), product); + } + return Task.CompletedTask; } @@ -75,6 +81,7 @@ public override async Task NotifyCheckout(CustomerCheckout customerCheckout) { Product product = this.cachedProducts[ID]; if( item.Version.SequenceEqual(product.version) && item.UnitPrice < product.price ){ + item.UnitPrice = product.price; item.Voucher += product.price - item.UnitPrice; } } diff --git a/Orleans/Interfaces/ICartActor.cs b/Orleans/Interfaces/ICartActor.cs index a9db0be..550db9e 100644 --- a/Orleans/Interfaces/ICartActor.cs +++ b/Orleans/Interfaces/ICartActor.cs @@ -2,18 +2,18 @@ using Common.Requests; using Orleans.Concurrency; -namespace OrleansApp.Interfaces +namespace OrleansApp.Interfaces; + +public interface ICartActor : IGrainWithIntegerKey { + public Task AddItem(CartItem item); - public interface ICartActor : IGrainWithIntegerKey - { - public Task AddItem(CartItem item); + public Task NotifyCheckout(CustomerCheckout basketCheckout); - public Task NotifyCheckout(CustomerCheckout basketCheckout); + [ReadOnly] + public Task GetCart(); - [ReadOnly] - public Task GetCart(); + public Task Seal(); - public Task Seal(); - } -} \ No newline at end of file + public Task> GetHistory(string tid); +} diff --git a/README.md b/README.md index 9ed3a5e..ed6a5f0 100644 --- a/README.md +++ b/README.md @@ -37,13 +37,13 @@ Further details about the benchmark can be found in the benchmark driver [reposi The Orleans virtual actor programming model prescribes a single thread per actor. Since we have one event per function call, to minimize latency, we map each entity to a logical actor, e.g., order, payment, and shipment. -* A cart actor per customer. ID is customer_id -* A customer actor per customer. ID is customer_id +* A cart actor per customer. ID is `customer_id` +* A customer actor per customer. ID is `customer_id` * A product actor per product. ID is composite `[seller_id,product_id]` -* A seller actor per seller. ID is seller_id +* A seller actor per seller. ID is `seller_id` * A stock actor per stock item. ID is composite `[seller_id,product_id]` -* An order actor per customer. ID is customer_id -* A payment actor per customer. ID is customer_id +* An order actor per customer. ID is `customer_id` +* A payment actor per customer. ID is `customer_id` * A shipment actor per partition of customers. Hash to define which shipment actor an order is forwarded to is defined by the hash of `[customer_id]`. Number of partitions is predefined (see [Configuration](#config)). Actors that log historical records: diff --git a/Silo/Controllers/CartController.cs b/Silo/Controllers/CartController.cs index 476e3fb..8df2431 100644 --- a/Silo/Controllers/CartController.cs +++ b/Silo/Controllers/CartController.cs @@ -59,7 +59,8 @@ public async Task NotifyCheckout([FromServices] IGrainFactory grai { await cartGrain.NotifyCheckout(customerCheckout); return Ok(); - } catch(Exception e) + } + catch(Exception e) { return StatusCode((int)HttpStatusCode.InternalServerError, e.Message); } @@ -82,5 +83,23 @@ public async Task Seal([FromServices] IGrainFactory grains, int cu return StatusCode((int)HttpStatusCode.InternalServerError, e.Message); } } + + [Route("/cart/{customerId}/history/{tid}")] + [HttpGet] + [ProducesResponseType((int)HttpStatusCode.Accepted)] + [ProducesResponseType((int)HttpStatusCode.InternalServerError)] + public async Task>>> GetHistory([FromServices] IGrainFactory grains, int customerId, string tid) + { + var cartGrain = this.callback(grains, customerId); + try + { + return Ok(await cartGrain.GetHistory(tid)); + } + catch (Exception e) + { + return StatusCode((int)HttpStatusCode.InternalServerError, e.Message); + } + } + } diff --git a/Silo/Program.cs b/Silo/Program.cs index b06de84..bbc4b32 100644 --- a/Silo/Program.cs +++ b/Silo/Program.cs @@ -28,6 +28,8 @@ var redisPrimaryConnectionString = configSection.GetValue("RedisPrimaryConnectionString"); var redisSecondaryConnectionString = configSection.GetValue("RedisSecondaryConnectionString"); +var trackCartHistory = configSection.GetValue("TrackCartHistory"); + AppConfig appConfig = new() { OrleansTransactions = orleansTransactions, @@ -44,6 +46,7 @@ NumShipmentActors = numShipmentActors, UseDashboard = useDash, UseSwagger = useSwagger, + TrackCartHistory = trackCartHistory }; // Orleans testing has no support for IOptions apparently... @@ -206,7 +209,8 @@ " \n Stream Replication: " + appConfig.StreamReplication + " \n RedisReplication: " + appConfig.RedisReplication + " \n RedisPrimaryConnectionString: "+ appConfig.RedisPrimaryConnectionString + - " \n RedisSecondaryConnectionString: "+ appConfig.RedisSecondaryConnectionString + " \n RedisSecondaryConnectionString: "+ appConfig.RedisSecondaryConnectionString + + " \n TrackCartHistory: "+appConfig.TrackCartHistory ); Console.WriteLine(" The Orleans server started. Press any key to terminate... "); Console.WriteLine("\n *************************************************************************"); diff --git a/Silo/appsettings.Development.json b/Silo/appsettings.Development.json index e9c096d..a099960 100644 --- a/Silo/appsettings.Development.json +++ b/Silo/appsettings.Development.json @@ -22,7 +22,8 @@ "StreamReplication": false, "RedisReplication": false, "RedisPrimaryConnectionString": "localhost:6379", - "RedisSecondaryConnectionString": "localhost:6380" + "RedisSecondaryConnectionString": "localhost:6380", + "TrackCartHistory": true } } diff --git a/Silo/appsettings.Production.json b/Silo/appsettings.Production.json index 714ddb6..dd6a20e 100644 --- a/Silo/appsettings.Production.json +++ b/Silo/appsettings.Production.json @@ -22,7 +22,8 @@ "StreamReplication": false, "RedisReplication": false, "RedisPrimaryConnectionString": "localhost:6379", - "RedisSecondaryConnectionString": "localhost:6380" + "RedisSecondaryConnectionString": "localhost:6380", + "TrackCartHistory": true } } diff --git a/Test/Infra/BaseTest.cs b/Test/Infra/BaseTest.cs index dc7264c..dc84c76 100644 --- a/Test/Infra/BaseTest.cs +++ b/Test/Infra/BaseTest.cs @@ -52,7 +52,8 @@ protected CustomerCheckout BuildCustomerCheckout(int customerId) CardExpiration = "1224", CardSecurityNumber = "001", CardBrand = "VISA", - Installments = 1 + Installments = 1, + instanceId = customerId.ToString() }; return customerCheckout; } diff --git a/Test/Infra/ConfigHelper.cs b/Test/Infra/ConfigHelper.cs index bb23d9c..88ac1b2 100644 --- a/Test/Infra/ConfigHelper.cs +++ b/Test/Infra/ConfigHelper.cs @@ -16,6 +16,7 @@ public class ConfigHelper NumShipmentActors = 1, UseDashboard = false, UseSwagger = false, + TrackCartHistory = true }; public static AppConfig NonTransactionalDefaultAppConfig = new() @@ -30,6 +31,7 @@ public class ConfigHelper NumShipmentActors = 1, UseDashboard = false, UseSwagger = false, + TrackCartHistory = false }; public const string PostgresConnectionString = "Host=localhost;Port=5432;Database=postgres;Username=postgres;Password=password;Pooling=true;Minimum Pool Size=0;Maximum Pool Size=10000"; diff --git a/Test/Replication/ReplicationTest.cs b/Test/Replication/ReplicationTest.cs index ebf6b84..94a4ec7 100644 --- a/Test/Replication/ReplicationTest.cs +++ b/Test/Replication/ReplicationTest.cs @@ -1,6 +1,8 @@ using Common.Entities; using Common.Requests; using Orleans.Interfaces.Replication; +using OrleansApp.Grains; +using OrleansApp.Interfaces; using OrleansApp.Transactional; using Test.Infra; using Test.Infra.Transactional; @@ -15,22 +17,25 @@ public ReplicationTest(TransactionalClusterFixture fixture) : base(fixture.Clust [Fact] public async Task TestPriceUpdate() { - var productActor = _cluster.GrainFactory.GetGrain(1,1.ToString()); + int productId = 100; + int customerId = 100; + + var productActor = _cluster.GrainFactory.GetGrain(1, productId.ToString()); await productActor.SetProduct( new Product() { seller_id = 1, - product_id = 1, + product_id = productId, price = 1, freight_value = 1, active = true, version = 1.ToString(), }); - var cartActor = _cluster.GrainFactory.GetGrain(1); + var cartActor = _cluster.GrainFactory.GetGrain(customerId); CartItem cartItem = new CartItem() { SellerId = 1, - ProductId = 1, + ProductId = productId, ProductName = "", UnitPrice = 0, Quantity = 1, @@ -49,10 +54,31 @@ public async Task TestPriceUpdate() // await replication await Task.Delay(1000); - var cartPrice = (await cartActor.GetReplicaItem(1, 1)).price; + var cartPrice = (await cartActor.GetReplicaItem(1, productId)).price; Assert.True(newPrice == priceUpdate.price); Assert.True(newPrice == cartPrice); + + await cartActor.Seal(); + } + + [Fact] + public async Task TestTrackHistory() + { + int maxCustomers = 10; + await InitData(maxCustomers, 2); + + for(int i = 1; i <= maxCustomers; i++){ + await BuildAndSendCheckout(i); + } + + for(int i = 1; i <= maxCustomers; i++){ + var cartActor = _cluster.GrainFactory.GetGrain(i); + var carts = await cartActor.GetHistory(i.ToString()); + Assert.NotEmpty(carts); + await cartActor.Seal(); + } + } }