Upgrade to Pro — share decks privately, control downloads, hide ads and more …

FP Design Patterns in Micro-Service Inter-Process Communication

FP Design Patterns in Micro-Service Inter-Process Communication

Viaceslav Pozdniakov

February 21, 2019
Tweet

More Decks by Viaceslav Pozdniakov

Other Decks in Technology

Transcript

  1. Wix Numbers ▪ ~450 jvm (~Scala) (micro-)services / ~1400 instances

    ▪ ~440 Node.js (micro-)services / ~1040 instances ▪ ~250 back-end developers (our users) ▪ ~400 front-end developers (some of them are also our users)
  2. P'ship Assoc. Prof. @Vilnius University Assistant Lecturer DBMS OOP C#

    Algorithms And Data Structures Computer Science Functional Programming
  3. Microservices Microservices are a software development technique — a variant

    of the service-oriented architecture (SOA) architectural style that structures an application as a collection of loosely coupled services. https://en.wikipedia.org/wiki/Microservices BACKGROUND
  4. Microservices communication Request contains: 1. Entity (defined by Interface Definition

    Language) 2. Context (usually just documented): ▪ Headers ▪ Cookies ▪ Kafka message headers ▪ ... BACKGROUND
  5. Wix request context ▪ User identity of initial requests ▪

    What kind of user ▪ User interface language ▪ Experiment conduction data (A/B testing) ▪ … BACKGROUND
  6. Implicit context If service A calls service B and service

    B calls service C, service C must receive all context data sent by service A even though this data is not explicitly used by service B and all calls do not bring any parameters. BACKGROUND A B C uid=42 uid=42 uid=42 I don’t care about uid
  7. Why implicit context propagation ▪ No need for recompilation redeployments

    ▪ No need for redeployments ▪ No boilerplate value passing ▪ ... BACKGROUND
  8. Scala as Interface Definition Language trait ServiceName { def echo(request:

    Message): Message } //client val aClient = rpcClientFactory.forTrait[ServiceName](url) aClient.echo(Message()) // supports context propagation BACKGROUND
  9. Old style service implementation class ServiceNameImpl extends ServiceName { override

    def echo(request: Message): Message = { val response = rpcClient.boo() kafka.produce(response) request } } BACKGROUND
  10. Protocol Buffers IDL syntax = "proto3"; service ServiceName { rpc

    Echo (Message) returns (Message); }; message Message { string field = 1; repeated string fields = 2; } gRPC
  11. Protocol Buffers IDL + ScalaPB import scala.concurrent.ExecutionContext.Implicits.global class ServiceNameImpl extends

    ServiceNameGrpc.ServiceName { override def echo(request: Message): Future[Message] = Future(request) } gRPC
  12. Request Context Strike import scala.concurrent.ExecutionContext.Implicits.global class ServiceNameImpl extends ServiceNameGrpc.ServiceName {

    override def echo(request: Message): Future[Message] = { rpcClient.boo() // works as expected Future { rpcClient.boo() // initial request context is lost request } } gRPC
  13. Request Context Strike import scala.concurrent.ExecutionContext.Implicits.global class ServiceNameImpl extends ServiceNameGrpc.ServiceName {

    override def echo(request: Message): Future[Message] = { rpcClient.boo() // works as expected Future { rpcClient.boo() // initial request context is lost request } } gRPC
  14. Request Context Strike import scala.concurrent.ExecutionContext.Implicits.global class ServiceNameImpl extends ServiceNameGrpc.ServiceName {

    override def echo(request: Message): Future[Message] = { rpcClient.boo() // works as expected Future { rpcClient.boo() // initial request context is lost request } } gRPC
  15. Our stack relied on ThreadLocal 1. java.lang.ThreadLocal[T] is a global

    variable per thread 2. ~7 years old code base 3. Own and specialized json-rpc and kafka clients,.. read ThreadLocal 4. Designed for sequential request processing gRPC
  16. What are the options? • Suffer • FP elitists: “Use

    Reader monad!” • Martin Odersky: Implicit Function Types (dotty) or brand new Witnesses gRPC
  17. Reader monad for mortals class ServiceNameImpl(rpcClient: SomeRpcClient, kafka: KafkaClient) extends

    ServiceNameWithCallScope.ServiceName { override def echo(request: Message)(implicit cs: CallScope): Future[Message] = for { response <- rpcClient.foo(request) _ <- kafka.produce(response) } yield request } READER
  18. Reader monad for mortals class ServiceNameImpl(rpcClient: SomeRpcClient, kafka: KafkaClient) extends

    ServiceNameWithCallScope.ServiceName { override def echo(request: Message)(implicit cs: CallScope): Future[Message] = for { response <- rpcClient.foo(request)(cs) _ <- kafka.produce(response)(cs) } yield request } READER
  19. Implicit CallScope (our Reader Monad) • Contains request context, request

    deadline,.. • Immutable representation of execution thread • A builder for other CallScopes with updated values (to run a new Reader) READER
  20. Look ma, no globals class ServiceNameImpl(rpcClient: SomeRpcClient, kafka: KafkaClient) extends

    ServiceNameWithCallScope.ServiceName { override def echo(request: Message)(implicit cs: CallScope) = { val f1 = Future(rpcClient.boo()(cs.withValue(Uid(42))) val f2 = Future(rpcClient.boo()(cs.withValue(Uid(43))) for { _ <- f1 _ <- f2 } yield request } } READER
  21. But wait… we want Response Context READER A B C

    D E42=? E42=? E42=1 E42=2 I don’t care about E42
  22. Imaginary Writer class ServiceNameImpl(rpcClient: SomeRpcClient, kafka: KafkaClient) extends ServiceNameWithCallScope.ServiceName {

    override def echo(request: Message)(implicit cs: CallScope) = { for { (rc1, _) <- Writer(rpcClient.boo()).run(...) (rc2, _) <- Writer(rpcClient.boo()).run(...) } yield request } } WRITER
  23. A Writer Monad for async world Reader Monad + Transactional

    Variable • Existing syntactic sugar of reader monad • JVM atomic reference as a Transactional variable (or something better, i.e. Ref) • Merge happens inside a monad! WRITER
  24. Merge is a fold over a lazy list • Semigroup

    is enough, but monoid is safier • Must be idempotent (we are asynchronous) • The list can be replaced with a set WRITER
  25. Response Context WRITER A B C D [ E42=1 E42=2

    ] E42=mf(1,2) E42=1 E42=2 I still don’t care about E42 I need E42 value
  26. Simplified CallScope Api Reader: • def withValue[T](value: T): CallScope •

    def value[T]: Option[T] Writer: • def put[T](value : T): Unit • def get[T](implicit mf : MergeFunction[T]) : Option[T] WRITER
  27. Monads for immortals import scala.language.higherKinds class ServiceNameTagless[F[_] : Async :

    HasCallScope]( rpcClient: SomeRpcClientF[F], kafka: KafkaClientF[F]) extends ServiceNameF.ServiceName[F] { override def echo(request: Message): F[Message] = for { response <- rpcClient.foo(request) _ <- kafka.produce(response) } yield request } TAGLESS
  28. Tagless Final approach in Scala All nice and dandy, but:

    • Difficult • Probably you won’t ever change underlying interpreter • Still not generic enough TAGLESS
  29. Subtyping class ServiceNameImpl(rpcClient: SomeRpcClient, kafka: KafkaClient) extends ServiceNameWithCallScope.ServiceName { override

    def echo(request: Message)(implicit cs: CallScope) = doSmthUseful() override def echo2(request: Message)(implicit cs: CallScope) = doSmthUseful2() } SUBTYPING
  30. Subtyping is bad class ServiceNameImpl(rpcClient: SomeRpcClient, kafka: KafkaClient) extends ServiceNameWithCallScope.ServiceName

    { override def echo(request: Message)(implicit cs: CallScope) = trace(doSmthUseful()) override def echo2(request: Message)(implicit cs: CallScope) = trace(doSmthUseful2()) } SUBTYPING
  31. Subtyping is evil class ServiceNameImpl(rpcClient: SomeRpcClient, kafka: KafkaClient) extends ServiceNameWithCallScope.ServiceName

    { override def echo(request: Message)(implicit cs: CallScope) = for { _ <- authorize() r <- trace(doSmthUseful()) } yield r override def echo2(request: Message)(implicit cs: CallScope) = for { _ <- authorize() r <- trace(doSmthUseful2()) } yield r } SUBTYPING
  32. A more complicated API package vipo; import "common.proto"; service Calculator

    { rpc add (Pair) returns (SingleValue); rpc sub (Pair) returns (SingleValue); rpc mul (Pair) returns (SingleValue); rpc div (Pair) returns (SingleValue); rpc neg (SingleValue) returns (SingleValue); } FUNCTION
  33. Even more complicated API syntax = "proto3"; package vipo; import

    "common.proto"; service Streaming { rpc RequestStream (SingleValue) returns (stream SingleValue); rpc ConsumeStream (stream SingleValue) returns (SingleValue); rpc BiStream (stream SingleValue) returns (stream SingleValue); } FUNCTION
  34. Service is a function type CalculatorFunction[U[_], S[_]] = CalculatorRequest[U, S]

    => CalculatorResponse[U, S] type StreamingFunction[U[_], S[_]] = StreamingRequest[U, S] => StreamingResponse[U, S] FUNCTION Generated ADT for method & request Generated ADT for response
  35. Monix val calculator: CalculatorFunction[Task, Observable] = { case r@Add(p) =>

    r(Task.evalAsync(SingleValue(p.a + p.b))) case r@Sub(p) => r(Task.evalAsync(SingleValue(p.a - p.b))) case r@Mul(p) => r(Task.evalAsync(SingleValue(p.a * p.b))) case r@Div(p) => r(Task.evalAsync(SingleValue(p.a / p.b))) case r@Neg(p) => r(Task.evalAsync(SingleValue(-p.a))) } val streaming: StreamingFunction[Task, Observable] = { case r@RequestStream(p) => r(fromIterable(Iterator.fill(p.a.toInt)(SingleValue(42)).toIterable)) case r@ConsumeStream(s) => r(s.map(_.a.toInt).foldLeftL(0)(_+_).map(SingleValue(_))) case r@BiStream(s) => r(s) } FUNCTION
  36. Zio val calculator: CalculatorFunction[GrpcIO, GrpcStream] = { case r@Add(p) =>

    r(IO.succeed(SingleValue(p.a + p.b))) case r@Sub(p) => r(IO.succeed(SingleValue(p.a - p.b))) case r@Mul(p) => r(IO.succeed(SingleValue(p.a * p.b))) case r@Div(p) => r(IO.succeed(SingleValue(p.a / p.b))) case r@Neg(p) => r(IO.succeed(SingleValue(-p.a))) } val streaming: StreamingFunction[GrpcIO, GrpcStream] = { case r@RequestStream(p) => r(fromIterable(Iterator.fill(p.a.toInt)(SingleValue(42)).toIterable)) case r@ConsumeStream(s) => r(s.map(_.a.toInt).foldLeft(0)(_+_).map(SingleValue(_))) case r@BiStream(s) => r(s) } type GrpcIO[T] = IO[StatusRuntimeException, T] type GrpcStream[T] = Stream[StatusRuntimeException, T] FUNCTION
  37. Vanilla Scala val calculator: CalculatorFunction[Future, NoStreaming] = { case r@Add(p)

    => r(Future.successful(SingleValue(p.a + p.b))) case r@Sub(p) => r(Future.successful(SingleValue(p.a - p.b))) case r@Mul(p) => r(Future.successful(SingleValue(p.a * p.b))) case r@Div(p) => r(Future.successful(SingleValue(p.a / p.b))) case r@Neg(p) => r(Future.successful(SingleValue(-p.a))) } trait NoStreaming[T] FUNCTION
  38. grpc4s • We are testing this idea, probably will be

    (re-)used at Wix • Common generated code for any backend: Monix and Vanilla at the moment, Zio is under development (HELP NEEDED!) • Will work with any IO, Stream implementation, since it is just a function • Composable! • Sources: https://github.com/vipo/grpc4s FUNCTION
  39. References • Martin Odersky: How To Abstract Over Context https://www.youtube.com/watch?v=uiorT754IwA

    • Monad Transformer State - Michael Snoyman https://www.youtube.com/watch?v=KZIN9f9rI34 • Grpc service as a function https://github.com/vipo/grpc4s TAKEAWAYS