Merge branch 'master' of sso://user/ahmetb/microservices-demo

This commit is contained in:
Jonathan Lui 2018-06-21 13:03:40 -07:00
commit 91663aa541
18 changed files with 2750 additions and 55 deletions

3
.gitignore vendored
View file

@ -1,3 +1,6 @@
bin/
pkg/
.DS_Store .DS_Store
*.pyc
*.swp *.swp
*~ *~

28
.vscode/launch.json vendored Normal file
View file

@ -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}"
}
,]
}

15
.vscode/tasks.json vendored Normal file
View file

@ -0,0 +1,15 @@
{
"version": "2.0.0",
"tasks": [
{
"label": "build",
"command": "dotnet",
"type": "process",
"args": [
"build",
"${workspaceFolder}/src/cartservice/cartservice.csproj"
],
"problemMatcher": "$msCompile"
}
]
}

3
src/cartservice/.gitignore vendored Normal file
View file

@ -0,0 +1,3 @@
/bin/*
/obj/*
.vs/*.*

View file

@ -2,6 +2,7 @@ using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using cartservice.interfaces;
using Grpc.Core; using Grpc.Core;
using Hipstershop; using Hipstershop;
using static Hipstershop.CartService; using static Hipstershop.CartService;
@ -11,65 +12,29 @@ namespace cartservice
// Cart wrapper to deal with grpc communication // Cart wrapper to deal with grpc communication
internal class CartServiceImpl : CartServiceBase 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; this.cartStore = cartStore;
} }
public override Task<Empty> AddItem(AddItemRequest request, Grpc.Core.ServerCallContext context) public async override Task<Empty> AddItem(AddItemRequest request, Grpc.Core.ServerCallContext context)
{ {
cartStore.AddItem(request.UserId, request.Item.ProductId, request.Item.Quantity); await cartStore.AddItemAsync(request.UserId, request.Item.ProductId, request.Item.Quantity);
return Task.FromResult(new Empty()); return Empty;
} }
public override Task<Empty> EmptyCart(EmptyCartRequest request, ServerCallContext context) public async override Task<Empty> EmptyCart(EmptyCartRequest request, ServerCallContext context)
{ {
cartStore.EmptyCart(request.UserId); await cartStore.EmptyCartAsync(request.UserId);
return Task.FromResult(new Empty()); return Empty;
} }
public override Task<Hipstershop.Cart> GetCart(GetCartRequest request, ServerCallContext context) public async override Task<Hipstershop.Cart> GetCart(GetCartRequest request, ServerCallContext context)
{ {
var cart = cartStore.GetCart(request.UserId); return await cartStore.GetCartAsync(request.UserId);
return Task.FromResult(cart.ToHipsterCart());
}
}
internal class CartStore
{
// Maps between user and their cart
private ConcurrentDictionary<string, Cart> userCartItems = new ConcurrentDictionary<string, Cart>();
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;
} }
} }

View file

@ -1,29 +1,41 @@
using System; using System;
using System.IO;
using cartservice.cartstore;
using CommandLine; using CommandLine;
using Grpc.Core; using Grpc.Core;
using Microsoft.Extensions.Configuration;
namespace cartservice namespace cartservice
{ {
class Program 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")] [Verb("start", HelpText = "Starts the server listening on provided port")]
class ServerOptions 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)] [Option('p', "port", HelpText = "The port on for running the server", Required = true)]
public int Port { get; set; } 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 Server server = new Server
{ {
Services = { Hipstershop.CartService.BindService(new CartServiceImpl(store)) }, Services = { Hipstershop.CartService.BindService(new CartServiceImpl(store)) },
Ports = { new ServerPort(host, port, ServerCredentials.Insecure) } 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..."); Console.WriteLine("Press any key to stop the server...");
server.Start(); server.Start();
@ -33,6 +45,7 @@ namespace cartservice
return null; return null;
} }
static void Main(string[] args) static void Main(string[] args)
{ {
if (args.Length == 0) if (args.Length == 0)
@ -45,15 +58,34 @@ namespace cartservice
{ {
case "start": case "start":
Parser.Default.ParseArguments<ServerOptions>(args).MapResult( Parser.Default.ParseArguments<ServerOptions>(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); errs => 1);
break; break;
default: default:
Console.WriteLine("Invalid command"); Console.WriteLine("Invalid command");
break; break;
} }
Console.WriteLine("Hello World!");
} }
} }
} }

View file

@ -11,6 +11,9 @@
<PackageReference Include="Google.Protobuf.Tools" Version="3.5.1" /> <PackageReference Include="Google.Protobuf.Tools" Version="3.5.1" />
<PackageReference Include="grpc" Version="1.12.0" /> <PackageReference Include="grpc" Version="1.12.0" />
<PackageReference Include="grpc.tools" Version="1.12.0" /> <PackageReference Include="grpc.tools" Version="1.12.0" />
<PackageReference Include="Microsoft.Extensions.Configuration" Version="2.1.1" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="2.1.1" />
<PackageReference Include="StackExchange.Redis" Version="1.2.6" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View file

@ -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<string, Cart> userCartItems = new ConcurrentDictionary<string, Cart>();
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;
}
}
}

View file

@ -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<Hipstershop.Cart> 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;
}
}
}

View file

@ -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<Hipstershop.Cart> GetCartAsync(string userId);
}
}

View file

@ -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

View file

@ -0,0 +1,13 @@
FROM golang:1.10-alpine as builder
RUN apk add --no-cache ca-certificates git
WORKDIR /src/microservices-demo/catalogservice
COPY . .
RUN go get -d ./...
RUN go build -o /catalogservice .
FROM alpine as release
RUN apk add --no-cache \
ca-certificates
COPY --from=builder /catalogservice /catalogservice
EXPOSE 5000
ENTRYPOINT /catalogservice

View file

@ -0,0 +1,6 @@
#!/bin/bash -e
PATH=$PATH:$GOPATH/bin
protodir=../../pb
protoc --go_out=plugins=grpc:genproto -I $protodir $protodir/demo.proto

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,93 @@
package main
import (
"context"
"fmt"
"log"
"net"
"os"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
pb "./genproto"
"google.golang.org/grpc"
)
const (
listenPort = "5000"
)
type checkoutService struct {
productCatalogSvcAddr string
cartSvcAddr string
currencySvcAddr string
shippingSvcAddr string
emailSvcAddr string
paymentSvcAddr string
}
func main() {
port := listenPort
if os.Getenv("PORT") != "" {
port = os.Getenv("PORT")
}
svc := new(checkoutService)
mustMapEnv(&svc.shippingSvcAddr, "SHIPPING_SERVICE_ADDR")
// mustMapEnv(&svc.productCatalogSvcAddr, "PRODUCT_CATALOG_SERVICE_ADDR")
// mustMapEnv(&svc.cartSvcAddr, "CART_SERVICE_ADDR")
// mustMapEnv(&svc.currencySvcAddr, "CURRENCY_SERVICE_ADDR")
// mustMapEnv(&svc.emailSvcAddr, "EMAIL_SERVICE_ADDR")
// mustMapEnv(&svc.paymentSvcAddr, "PAYMENT_SERVICE_ADDR")
log.Printf("service config: %+v", svc)
lis, err := net.Listen("tcp", fmt.Sprintf(":%s", port))
if err != nil {
log.Fatal(err)
}
srv := grpc.NewServer()
pb.RegisterCheckoutServiceServer(srv, svc)
log.Printf("starting to listen on tcp: %q", lis.Addr().String())
log.Fatal(srv.Serve(lis))
}
func mustMapEnv(target *string, envKey string) {
v := os.Getenv(envKey)
if v == "" {
panic(fmt.Sprintf("environment variable %q not set", envKey))
}
*target = v
}
func (cs *checkoutService) CreateOrder(ctx context.Context, req *pb.CreateOrderRequest) (*pb.CreateOrderResponse, error) {
log.Printf("[CreateOrder] user_id=%q user_currency=%q", req.UserId, req.UserCurrency)
resp := new(pb.CreateOrderResponse)
conn, err := grpc.Dial(cs.shippingSvcAddr, grpc.WithInsecure())
if err != nil {
return nil, status.Errorf(codes.Unavailable, "could not connect shippping service: %+v", err)
}
defer conn.Close()
shippingQuote, err := pb.NewShippingServiceClient(conn).
GetQuote(ctx, &pb.GetQuoteRequest{
Address: req.Address,
Items: nil}) // TODO(ahmetb): query CartService for items
if err != nil {
return nil, status.Errorf(codes.Unavailable, "failed to get shipping quote: %+v", err)
}
resp.ShippingCost = &pb.Money{
Amount: shippingQuote.GetCostUsd(),
CurrencyCode: "USD", // TOD(ahmetb) convert to req.UserCurrency
}
// TODO(ahmetb) calculate resp.OrderItem with req.UserCurrency
return resp, nil
}
func (cs *checkoutService) PlaceOrder(ctx context.Context, req *pb.PlaceOrderRequest) (*pb.PlaceOrderResponse, error) {
log.Printf("[PlaceOrder] user_id=%q user_currency=%q", req.UserId, req.UserCurrency)
resp := new(pb.PlaceOrderResponse)
return resp, nil
}

View file

@ -5,7 +5,7 @@ RUN apk add --no-cache \
WORKDIR /src/microservices-demo/productcatalogservice WORKDIR /src/microservices-demo/productcatalogservice
COPY . . COPY . .
RUN go get -d ./... RUN go get -d ./...
RUN go build -v -o /productcatalogservice . RUN go build -o /productcatalogservice .
FROM alpine as release FROM alpine as release
RUN apk add --no-cache \ RUN apk add --no-cache \

View file

@ -1,3 +1,6 @@
#!/bin/bash
set -e
# script to compile python protos # script to compile python protos
# #
# requires gRPC tools: # requires gRPC tools:

View file

@ -5,7 +5,7 @@ RUN apk add --no-cache \
WORKDIR /src/microservices-demo/shippingservice WORKDIR /src/microservices-demo/shippingservice
COPY . . COPY . .
RUN go get -d ./... RUN go get -d ./...
RUN go build -v -o /shippingservice . RUN go build -o /shippingservice .
FROM alpine as release FROM alpine as release
COPY --from=builder /shippingservice /shippingservice COPY --from=builder /shippingservice /shippingservice