Slide 1

Slide 1 text

Viačeslav Pozdniakov @poznia github.com/vipo FP Design Patterns in Micro-Service IPC

Slide 2

Slide 2 text

Wix Engineering Locations Ukraine Israel Lithuania Vilnius Kyiv Dnipro Tel-Aviv Be’er Sheva Haifa

Slide 3

Slide 3 text

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)

Slide 4

Slide 4 text

P'ship Assoc. Prof. @Vilnius University Assistant Lecturer DBMS OOP C# Algorithms And Data Structures Computer Science Functional Programming

Slide 5

Slide 5 text

BEFORE WE EVEN START ~300 developers

Slide 6

Slide 6 text

AGENDA Synchronous Imperative Past Asynchronous Functional Present Improvements for the Future Takeaways

Slide 7

Slide 7 text

Synchronous Imperative Past 01

Slide 8

Slide 8 text

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

Slide 9

Slide 9 text

Microservices communication Request contains: 1. Entity (defined by Interface Definition Language) 2. Context (usually just documented): ▪ Headers ▪ Cookies ▪ Kafka message headers ▪ ... BACKGROUND

Slide 10

Slide 10 text

Wix request context ▪ User identity of initial requests ▪ What kind of user ▪ User interface language ▪ Experiment conduction data (A/B testing) ▪ … BACKGROUND

Slide 11

Slide 11 text

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

Slide 12

Slide 12 text

Why implicit context propagation ▪ No need for recompilation redeployments ▪ No need for redeployments ▪ No boilerplate value passing ▪ ... BACKGROUND

Slide 13

Slide 13 text

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

Slide 14

Slide 14 text

Old style service implementation class ServiceNameImpl extends ServiceName { override def echo(request: Message): Message = { val response = rpcClient.boo() kafka.produce(response) request } } BACKGROUND

Slide 15

Slide 15 text

No content

Slide 16

Slide 16 text

Protocol Buffers IDL syntax = "proto3"; service ServiceName { rpc Echo (Message) returns (Message); }; message Message { string field = 1; repeated string fields = 2; } gRPC

Slide 17

Slide 17 text

ScalaPB protoc gRPC .proto .scala ScalaPB protoc compiler

Slide 18

Slide 18 text

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

Slide 19

Slide 19 text

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

Slide 20

Slide 20 text

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

Slide 21

Slide 21 text

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

Slide 22

Slide 22 text

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

Slide 23

Slide 23 text

What are the options? ● Suffer ● FP elitists: “Use Reader monad!” ● Martin Odersky: Implicit Function Types (dotty) or brand new Witnesses gRPC

Slide 24

Slide 24 text

No content

Slide 25

Slide 25 text

Asynchronous Functional Present 02

Slide 26

Slide 26 text

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

Slide 27

Slide 27 text

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

Slide 28

Slide 28 text

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

Slide 29

Slide 29 text

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

Slide 30

Slide 30 text

But wait… we want Response Context READER A B C D E42=? E42=? E42=1 E42=2 I don’t care about E42

Slide 31

Slide 31 text

No content

Slide 32

Slide 32 text

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

Slide 33

Slide 33 text

No content

Slide 34

Slide 34 text

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

Slide 35

Slide 35 text

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

Slide 36

Slide 36 text

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

Slide 37

Slide 37 text

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

Slide 38

Slide 38 text

Improvements for the Future 03

Slide 39

Slide 39 text

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

Slide 40

Slide 40 text

Tagless Final approach in Scala All nice and dandy, but: ● Difficult ● Probably you won’t ever change underlying interpreter ● Still not generic enough TAGLESS

Slide 41

Slide 41 text

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

Slide 42

Slide 42 text

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

Slide 43

Slide 43 text

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

Slide 44

Slide 44 text

No content

Slide 45

Slide 45 text

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

Slide 46

Slide 46 text

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

Slide 47

Slide 47 text

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

Slide 48

Slide 48 text

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

Slide 49

Slide 49 text

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

Slide 50

Slide 50 text

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

Slide 51

Slide 51 text

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

Slide 52

Slide 52 text

Takeaways 04

Slide 53

Slide 53 text

Limiting but liberating Algebraic structures TAKEAWAYS

Slide 54

Slide 54 text

When in doubt, use... A Function, Not a Method TAKEAWAYS

Slide 55

Slide 55 text

What is a Monad? It is a design pattern TAKEAWAYS

Slide 56

Slide 56 text

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

Slide 57

Slide 57 text

Thank You @poznia github.com/vipo

Slide 58

Slide 58 text

Q&A @poznia github.com/vipo