From 8860d8bfddf9adda8f7c81e1ef8195bfb1634e9d Mon Sep 17 00:00:00 2001 From: Simon Zeltser Date: Thu, 21 Jun 2018 11:09:12 -0700 Subject: [PATCH 1/4] added redis integration and command line arguments handling to cart service. Didn't test in the cloud yet --- .gitignore | 3 + .vscode/launch.json | 28 +++++++ .vscode/tasks.json | 15 ++++ src/cartservice/cartstore/LocalCartStore.cs | 41 ++++++++++ src/cartservice/cartstore/RedisCartStore.cs | 74 +++++++++++++++++++ src/cartservice/interfaces/ICartStore.cs | 12 +++ .../run_redis_emulator_windows.bat | 11 +++ 7 files changed, 184 insertions(+) create mode 100644 .gitignore create mode 100644 .vscode/launch.json create mode 100644 .vscode/tasks.json create mode 100644 src/cartservice/cartstore/LocalCartStore.cs create mode 100644 src/cartservice/cartstore/RedisCartStore.cs create mode 100644 src/cartservice/interfaces/ICartStore.cs create mode 100644 src/cartservice/run_redis_emulator_windows.bat diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..cf3cbd3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +src/cartservice/bin/* +src/cartservice/obj/* +.vs/*.* \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..6cba3ec --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,28 @@ +{ + // Use IntelliSense to find out which attributes exist for C# debugging + // Use hover for the description of the existing attributes + // For further information visit https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md + "version": "0.2.0", + "configurations": [ + { + "name": ".NET Core Launch (console)", + "type": "coreclr", + "request": "launch", + "preLaunchTask": "build", + // If you have changed target frameworks, make sure to update the program path. + "program": "${workspaceFolder}/src/cartservice/bin/Debug/netcoreapp2.0/cartservice.dll", + "args": [], + "cwd": "${workspaceFolder}/src/cartservice", + // For more information about the 'console' field, see https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md#console-terminal-window + "console": "internalConsole", + "stopAtEntry": false, + "internalConsoleOptions": "openOnSessionStart" + }, + { + "name": ".NET Core Attach", + "type": "coreclr", + "request": "attach", + "processId": "${command:pickProcess}" + } + ,] +} \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000..ac157b4 --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,15 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "build", + "command": "dotnet", + "type": "process", + "args": [ + "build", + "${workspaceFolder}/src/cartservice/cartservice.csproj" + ], + "problemMatcher": "$msCompile" + } + ] +} \ No newline at end of file diff --git a/src/cartservice/cartstore/LocalCartStore.cs b/src/cartservice/cartstore/LocalCartStore.cs new file mode 100644 index 0000000..66c8d40 --- /dev/null +++ b/src/cartservice/cartstore/LocalCartStore.cs @@ -0,0 +1,41 @@ +using System.Collections.Concurrent; +using cartservice.interfaces; + +namespace cartservice.cartstore +{ + internal class LocalCartStore + { + // Maps between user and their cart + private ConcurrentDictionary userCartItems = new ConcurrentDictionary(); + + public void AddItem(string userId, string productId, int quantity) + { + Cart cart; + if (!userCartItems.TryGetValue(userId, out cart)) + { + cart = new Cart(userId); + } + else + { + cart = userCartItems[userId]; + } + cart.AddItem(productId, quantity); + } + + public void EmptyCart(string userId) + { + Cart cart; + if (userCartItems.TryGetValue(userId, out cart)) + { + cart.EmptyCart(); + } + } + + public Cart GetCart(string userId) + { + Cart cart = null; + userCartItems.TryGetValue(userId, out cart); + return cart; + } + } +} \ No newline at end of file diff --git a/src/cartservice/cartstore/RedisCartStore.cs b/src/cartservice/cartstore/RedisCartStore.cs new file mode 100644 index 0000000..8745a19 --- /dev/null +++ b/src/cartservice/cartstore/RedisCartStore.cs @@ -0,0 +1,74 @@ +using System; +using System.IO; +using System.Threading.Tasks; +using cartservice.interfaces; +using Google.Protobuf; +using Hipstershop; +using StackExchange.Redis; + +namespace cartservice.cartstore +{ + public class RedisCartStore : ICartStore + { + private const string CART_FIELD_NAME = "cart"; + + private readonly ConnectionMultiplexer redis; + + private readonly byte[] emptyCartBytes; + + public RedisCartStore(string redisAddress) + { + // Serialize empty cart into byte array. + var cart = new Hipstershop.Cart(); + emptyCartBytes = cart.ToByteArray(); + + string connectionString = $"{redisAddress},ssl=false,allowAdmin=true"; + Console.WriteLine("Connecting to Redis: " + connectionString); + redis = ConnectionMultiplexer.Connect(connectionString); + } + + public async Task AddItemAsync(string userId, string productId, int quantity) + { + var db = redis.GetDatabase(); + + // Access the cart from the cache + var value = await db.HashGetAsync(userId, CART_FIELD_NAME); + + Hipstershop.Cart cart; + if (value.IsNull) + { + cart = new Hipstershop.Cart(); + } + else + { + cart = Hipstershop.Cart.Parser.ParseFrom(value); + } + + cart.Items.Add(new Hipstershop.CartItem { ProductId = productId, Quantity = quantity }); + } + + public async Task EmptyCartAsync(string userId) + { + var db = redis.GetDatabase(); + + // Update the cache with empty cart for given user + await db.HashSetAsync(userId, new[] { new HashEntry(CART_FIELD_NAME, emptyCartBytes) }); + } + + public async Task GetCartAsync(string userId) + { + var db = redis.GetDatabase(); + + // Access the cart from the cache + var value = await db.HashGetAsync(userId, CART_FIELD_NAME); + + Hipstershop.Cart cart = null; + if (!value.IsNull) + { + cart = Hipstershop.Cart.Parser.ParseFrom(value); + } + + return cart; + } + } +} \ No newline at end of file diff --git a/src/cartservice/interfaces/ICartStore.cs b/src/cartservice/interfaces/ICartStore.cs new file mode 100644 index 0000000..f4fa1e5 --- /dev/null +++ b/src/cartservice/interfaces/ICartStore.cs @@ -0,0 +1,12 @@ +using System.Threading.Tasks; + +namespace cartservice.interfaces +{ + internal interface ICartStore + { + Task AddItemAsync(string userId, string productId, int quantity); + Task EmptyCartAsync(string userId); + + Task GetCartAsync(string userId); + } +} \ No newline at end of file diff --git a/src/cartservice/run_redis_emulator_windows.bat b/src/cartservice/run_redis_emulator_windows.bat new file mode 100644 index 0000000..8b1d9c0 --- /dev/null +++ b/src/cartservice/run_redis_emulator_windows.bat @@ -0,0 +1,11 @@ +@echo off +rem install redis on windows using choco +rem choco install redis-64 + +rem run redis +redis-server --daemonize yes + +rem testing locally +rem redis-cli +rem SET foo bar +rem GET foo \ No newline at end of file From 0ce195bb80156b2652bc28df3d971da166bd0635 Mon Sep 17 00:00:00 2001 From: Simon Zeltser Date: Thu, 21 Jun 2018 11:10:13 -0700 Subject: [PATCH 2/4] command line arguments handling --- src/cartservice/CartServiceImpl.cs | 59 ++++++------------------------ src/cartservice/Program.cs | 44 +++++++++++++++++++--- src/cartservice/cartservice.csproj | 3 ++ 3 files changed, 53 insertions(+), 53 deletions(-) diff --git a/src/cartservice/CartServiceImpl.cs b/src/cartservice/CartServiceImpl.cs index e1cbb49..c6c4887 100644 --- a/src/cartservice/CartServiceImpl.cs +++ b/src/cartservice/CartServiceImpl.cs @@ -2,6 +2,7 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using cartservice.interfaces; using Grpc.Core; using Hipstershop; using static Hipstershop.CartService; @@ -11,65 +12,29 @@ namespace cartservice // Cart wrapper to deal with grpc communication internal class CartServiceImpl : CartServiceBase { - private CartStore cartStore; + private ICartStore cartStore; + private readonly static Empty Empty = new Empty(); - public CartServiceImpl(CartStore cartStore) + public CartServiceImpl(ICartStore cartStore) { this.cartStore = cartStore; } - public override Task AddItem(AddItemRequest request, Grpc.Core.ServerCallContext context) + public async override Task AddItem(AddItemRequest request, Grpc.Core.ServerCallContext context) { - cartStore.AddItem(request.UserId, request.Item.ProductId, request.Item.Quantity); - return Task.FromResult(new Empty()); + await cartStore.AddItemAsync(request.UserId, request.Item.ProductId, request.Item.Quantity); + return Empty; } - public override Task EmptyCart(EmptyCartRequest request, ServerCallContext context) + public async override Task EmptyCart(EmptyCartRequest request, ServerCallContext context) { - cartStore.EmptyCart(request.UserId); - return Task.FromResult(new Empty()); + await cartStore.EmptyCartAsync(request.UserId); + return Empty; } - public override Task GetCart(GetCartRequest request, ServerCallContext context) + public async override Task GetCart(GetCartRequest request, ServerCallContext context) { - var cart = cartStore.GetCart(request.UserId); - return Task.FromResult(cart.ToHipsterCart()); - } - } - - internal class CartStore - { - // Maps between user and their cart - private ConcurrentDictionary userCartItems = new ConcurrentDictionary(); - - public void AddItem(string userId, string productId, int quantity) - { - Cart cart; - if (!userCartItems.TryGetValue(userId, out cart)) - { - cart = new Cart(userId); - } - else - { - cart = userCartItems[userId]; - } - cart.AddItem(productId, quantity); - } - - public void EmptyCart(string userId) - { - Cart cart; - if (userCartItems.TryGetValue(userId, out cart)) - { - cart.EmptyCart(); - } - } - - public Cart GetCart(string userId) - { - Cart cart = null; - userCartItems.TryGetValue(userId, out cart); - return cart; + return await cartStore.GetCartAsync(request.UserId); } } diff --git a/src/cartservice/Program.cs b/src/cartservice/Program.cs index 8d4e396..1902144 100644 --- a/src/cartservice/Program.cs +++ b/src/cartservice/Program.cs @@ -1,29 +1,41 @@ using System; +using System.IO; +using cartservice.cartstore; using CommandLine; using Grpc.Core; +using Microsoft.Extensions.Configuration; namespace cartservice { class Program { + const string CART_SERVICE_ADDRESS = "CART_SERVICE_ADDR"; + const string REDIS_ADDRESS = "REDIS_ADDR"; + [Verb("start", HelpText = "Starts the server listening on provided port")] class ServerOptions { + [Option('h', "hostname", HelpText = "The ip on which the server is running. If not provided, CART_SERVICE_ADDR environment variable value will be used. If not defined, localhost is used")] + public string Host { get; set; } [Option('p', "port", HelpText = "The port on for running the server", Required = true)] public int Port { get; set; } + + [Option('r', "redis", HelpText = "The ip of redis cache")] + public string Redis { get; set; } + } - static object StartServer(string host, int port) + static object StartServer(string host, int port, string redisAddress) { - var store = new CartStore(); + var store = new RedisCartStore(redisAddress); Server server = new Server { Services = { Hipstershop.CartService.BindService(new CartServiceImpl(store)) }, Ports = { new ServerPort(host, port, ServerCredentials.Insecure) } }; - Console.WriteLine("Cart server is listening on port " + port); + Console.WriteLine($"Cart server is listening at {host}:{port}"); Console.WriteLine("Press any key to stop the server..."); server.Start(); @@ -33,6 +45,7 @@ namespace cartservice return null; } + static void Main(string[] args) { if (args.Length == 0) @@ -45,15 +58,34 @@ namespace cartservice { case "start": Parser.Default.ParseArguments(args).MapResult( - (ServerOptions options) => StartServer("localhost", options.Port), + (ServerOptions options) => + { + string host = options.Host; + if (string.IsNullOrEmpty(host)) + { + Console.WriteLine($"Reading host address from {CART_SERVICE_ADDRESS} environment variable..."); + host = Environment.GetEnvironmentVariable(CART_SERVICE_ADDRESS); + if (string.IsNullOrEmpty(host)) + { + Console.WriteLine("Setting the host to 127.0.0.1"); + host = "127.0.0.1"; + } + } + + string redis = options.Redis; + if (string.IsNullOrEmpty(redis)) + { + Console.WriteLine("Reading redis cache address from environment variable"); + redis = Environment.GetEnvironmentVariable(REDIS_ADDRESS); + } + return StartServer(host, options.Port, redis); + }, errs => 1); break; default: Console.WriteLine("Invalid command"); break; } - - Console.WriteLine("Hello World!"); } } } diff --git a/src/cartservice/cartservice.csproj b/src/cartservice/cartservice.csproj index fddba5c..6298870 100644 --- a/src/cartservice/cartservice.csproj +++ b/src/cartservice/cartservice.csproj @@ -11,6 +11,9 @@ + + + From d7d9165da6dceb9544c44708e81ceb7a76041e55 Mon Sep 17 00:00:00 2001 From: Simon Zeltser Date: Thu, 21 Jun 2018 11:19:48 -0700 Subject: [PATCH 3/4] added gitignore file for .net proj --- src/cartservice/.gitignore | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 src/cartservice/.gitignore diff --git a/src/cartservice/.gitignore b/src/cartservice/.gitignore new file mode 100644 index 0000000..f983350 --- /dev/null +++ b/src/cartservice/.gitignore @@ -0,0 +1,3 @@ +/bin/* +/obj/* +.vs/*.* From b91d97f673b23022c92a36c998d51e40dccc0f62 Mon Sep 17 00:00:00 2001 From: Simon Zeltser Date: Thu, 21 Jun 2018 11:22:23 -0700 Subject: [PATCH 4/4] remove .gitignore changes for cart service - they should be in cart service folder --- .gitignore | 3 --- 1 file changed, 3 deletions(-) delete mode 100644 .gitignore diff --git a/.gitignore b/.gitignore deleted file mode 100644 index cf3cbd3..0000000 --- a/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -src/cartservice/bin/* -src/cartservice/obj/* -.vs/*.* \ No newline at end of file