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

Modeling Pub/Sub system with F# and CQRS (on micro-scale)

Antya Dev
October 25, 2019

Modeling Pub/Sub system with F# and CQRS (on micro-scale)

Almost all APIs that are built in different companies have a lot in common, but when you have to open several projects, they are very different. Query and error handling, interaction with domain objects and infrastructure layers, etc. shouldn't be so different. All this fake difference only complicates the understanding of the components of the project and their interaction. In this talk, I will share how we build APIs that support different semantic models(Request/Response and Pub/Sub) and various protocols exchange (HTTP/WebSockets/AMQP). I will also try to unlock the potential of the CQRS architectural pattern and explain why we apply it to almost all API projects.

Antya Dev

October 25, 2019
Tweet

More Decks by Antya Dev

Other Decks in Programming

Transcript

  1. Modeling Pub/Sub system
    with F#
    and CQRS (on micro scale)
    by @antyadev

    View Slide

  2. The Problem
    Almost all APIs that are built in different companies have a lot in common, but
    when you have to open several projects, they are very different.
    Query and error handling, interaction with domain objects and infrastructure
    layers, etc. shouldn't be so different.
    All this fake difference only complicates the understanding of the components
    of the project and their interaction.

    View Slide

  3. View Slide

  4. Data Feed
    WebSites/Terminals

    View Slide

  5. Data Feed
    F#
    F#
    WebSites/Terminals
    F#

    View Slide

  6. Agenda
    CQRS in 5 mins
    F# and CQRS(on micro scale)
    Advantages and disadvantages

    View Slide

  7. CQRS

    View Slide

  8. View Slide

  9. View Slide

  10. View Slide

  11. View Slide

  12. View Slide

  13. CQRS
    (on micro scale)

    View Slide

  14. Incidents

    View Slide

  15. WebScokets
    DB

    View Slide

  16. Subscribe

    View Slide

  17. View Slide

  18. { incident }

    View Slide

  19. push
    { incident }

    View Slide

  20. Command
    Command

    View Slide

  21. Command Event

    View Slide

  22. Incidents
    Domain

    View Slide

  23. type IncidentType = Goal | Corner | Penalty | RedCard | YellowCard
    type ParticipantRole = Home | Away
    type Incident = {
    Id: IncidentId
    GameId: GameId
    Type: IncidentType
    ParticipantId: ParticipantId
    ParticipantRole: ParticipantRole option
    TimeInGame: DateTime
    GamePart: GamePart option
    }

    View Slide

  24. type Query =
    | Incident of IncidentId
    | IncidentsForGames of GameId[] * IncidentType[] * LimitsParams
    type Command =
    | AddIncident of IncidentArgs
    | RemoveIncident of IncidentId
    | Subscribe of GameId[] * IncidentType[]
    | Unsubscribe of SubscriptionId
    type DomainEvent =
    | IncidentAdded of Incident * subscriptionsToNotify:SubscriptionId[]
    | IncidentRemoved of IncidentId
    | SubscriptionCreated of SubscriptionId
    | SubscriberAdded of SubscriptionId * subscribersCount:uint32
    | SubscriptionRemoved of SubscriptionId
    | SubscriberRemoved of SubscriptionId * subscribersCount:uint32

    View Slide

  25. type ParserError =
    | GameIdFormat of originValues:string[]
    | IncidentIdFormat of originValues:string[]
    | IncidentTypeFormat of originValues:string[]
    type DomainError =
    | IncidentCantBeCreated of gameId:int * typeId:int
    | LimitsOutOfRange of top:int * maxTop:int * skip:int
    | GameNotFound of gameId:int
    | IncidentNotFound of incidentId:int
    | IncidentsForGamesNotFound of gameIds:int[]
    type AppError =
    | Parser of ParserError
    | Domain of DomainError
    static member create(e: ParserError) = e |> Parser
    static member create(e: DomainError) = e |> Domain
    static member createResult(e: DomainError) = e |> Domain |> Error
    static member createResult(e: ParserError) = e |> Parser |> Error

    View Slide

  26. Project development with familiar backbone
    Domain | Commands | Queries | Events
    CommandHandler | QueryHandler
    CQRS
    (on micro scale)

    View Slide

  27. In every project you will see
    the following structure
    CQRS
    (on micro scale)

    View Slide

  28. Domain
    Queries
    Commands
    Events
    Errors
    Command Handler
    Query Handler

    View Slide

  29. Command Handler
    CQRS
    (on micro scale)

    View Slide

  30. Command Handler
    Command -> DomainEvent

    View Slide

  31. Command Handler
    Command -> Result

    View Slide

  32. Command Handler
    Command -> Async>

    View Slide

  33. Command Handler
    Command -> Async>
    TOP LEVEL CONSTRAINT which force
    any new functionality to return result

    View Slide

  34. type Command =
    | AddIncident of IncidentArgs
    | RemoveIncident of IncidentId
    | Subscribe of GameId[] * IncidentType[]
    | Unsubscribe of SubscriptionId
    type DomainEvent =
    | IncidentAdded of Incident * subscriptionsToNotify:SubscriptionId[]
    | IncidentRemoved of IncidentId
    | SubscriptionCreated of SubscriptionId
    | SubscriberAdded of SubscriptionId * subscribersCount:uint32
    | SubscriptionRemoved of SubscriptionId
    | SubscriberRemoved of SubscriptionId * subscribersCount:uint32

    View Slide

  35. let handle (command) =
    match command with
    | AddIncident args -> DomainEvent.IncidentAdded
    | RemoveIncident id -> DomainEvent.IncidentRemoved
    | Subscribe (gamesIds, incTypes) -> DomainEvent.SubscriptionCreated
    | Unsubscribe id -> DomainEvent.SubscriptionRemoved
    type Command =
    | AddIncident of IncidentArgs
    | RemoveIncident of IncidentId
    | Subscribe of GameId[] * IncidentType[]
    | Unsubscribe of SubscriptionId
    type DomainEvent =
    | IncidentAdded of Incident * subscriptionsToNotify:SubscriptionId[]
    | IncidentRemoved of IncidentId
    | SubscriptionCreated of SubscriptionId
    | SubscriberAdded of SubscriptionId * subscribersCount:uint32
    | SubscriptionRemoved of SubscriptionId
    | SubscriberRemoved of SubscriptionId * subscribersCount:uint32

    View Slide

  36. type CommandHandler(storage: IStorage, subscriptions: Subscriptions) =
    let handle (command) = asyncResult {
    match command with
    | AddIncident params ->
    let! game = storage.FindGame(params.GametId) with
    match game with
    | Some game ->
    let! event = IncidentDomain.add(params, game, subscriptions)
    publishEvent(event)
    return event
    | None -> return! GameNotFound(params.GametId) |> AppError.create
    }
    member x.Handle(command) = handle(command)

    View Slide

  37. type CommandHandler(storage: IStorage, subscriptions: Subscriptions) =
    let handle (command) = asyncResult {
    match command with
    | AddIncident params ->
    let! game = storage.FindGame(params.GametId) with
    match game with
    | Some game ->
    let! event = IncidentDomain.add(params, game, subscriptions)
    publishEvent(event)
    return event
    | None -> return! GameNotFound(params.GametId) |> AppError.create
    }
    member x.Handle = handle

    View Slide

  38. type CommandHandler(storage: IStorage, subscriptions: Subscriptions) =
    let handle (command) = asyncResult {
    match command with
    | AddIncident params ->
    match! storage.FindGame(params.GametId) with
    | Some game ->
    let! event = IncidentDomain.add(params, game, subscriptions)
    publishEvent(event)
    return event
    | None -> return! GameNotFound(params.GametId) |> AppError.create
    }
    member x.Handle = handle

    View Slide

  39. type CommandHandler(storage: IStorage, subscriptions: Subscriptions) =
    // handle: Command -> Async>
    let handle (command) = asyncResult {
    match command with
    | AddIncident params ->
    match! storage.FindGame(params.GametId) with // Task>
    | Some game ->
    // Result
    let! event = IncidentDomain.add(params, game, subscriptions)
    publishEvent(event)
    return event
    | None -> return! GameNotFound(params.GametId) |> AppError.create
    }
    member x.Handle = handle

    View Slide

  40. type CommandHandler(storage: IStorage, subscriptions: Subscriptions) =
    // handle: Command -> Async>
    let handle (command) = asyncResult {
    match command with
    | CreateIncident params ->
    match! storage.FindGame(params.GametId) with // Task>
    | Some game ->
    // Result
    let! event = IncidentDomain.add(params, game, subscriptions)
    publishEvent(event)
    return event
    | None -> return! GameNotFound(params.GametId) |> AppError.create
    }
    member x.Handle = handle

    View Slide

  41. type CommandHandler(storage: IStorage, subscriptions: Subscriptions) =
    let eventStream = new Subject()
    let publishEvent (event) = eventStream.OnNext(event)
    // handle: Command -> Async>
    let handle (command) = asyncResult {
    match command with
    | CreateIncident params ->
    match! storage.FindGame(params.GametId) with // Task>
    | Some game ->
    // Result
    let! event = IncidentDomain.add(params, game, subscriptions)
    publishEvent(event)
    return event
    | None -> return! GameNotFound(params.GametId) |> AppError.create
    }
    member x.Handle = handle
    member x.EventStream = eventStream :> IObservable

    View Slide

  42. type CommandHandler(storage: IStorage, subscriptions: Subscriptions) =
    let eventStream = new Subject()
    let publishEvent = eventStream.OnNext
    // handle: Command -> Async>
    let handle (command) = asyncResult {
    match command with
    | CreateIncident params ->
    match! storage.FindGame(params.GametId) with // Task>
    | Some game ->
    // Result
    let! event = IncidentDomain.add(params, game, subscriptions)
    publishEvent(event)
    return event
    | None -> return! GameNotFound(params.GametId) |> AppError.create
    }
    member x.Handle = handle
    member x.EventStream = eventStream :> IObservable

    View Slide

  43. Command Handler
    Handle: Command -> Async
    EventStream: IObservable
    WebScokets

    View Slide

  44. Command Handler
    WebSockets Middleware AMQP Middleware
    connections
    connections
    EventStream: IObservable
    Dictionary
    Dictionary

    View Slide

  45. Command Handler
    IncidentAddedEvent {
    SubscriptionId = 5,6,7
    Incident: { … }
    }
    EventStream: IObservable
    WebSockets Middleware AMQP Middleware
    connections
    connections
    Dictionary
    Dictionary

    View Slide

  46. Decoupled components
    via Pub/Sub messaging (IObservable)
    publishEvent(event)
    CQRS
    (on micro scale)

    View Slide

  47. Query Handler
    CQRS
    (on micro scale)

    View Slide

  48. Query Handler
    Query -> Async>

    View Slide

  49. type QueryHandler(storage: InStorage) =
    let response (incidents: Incident[]) =
    incidents |> Array.map(Incident.toContractType)
    let handle (query) = asyncResult {
    match query with
    | Incident id ->
    match! storage.FindIncident(id) with
    | Some incident -> return response [| incident |]
    | None -> return response(Array.empty)
    }
    member x.Handle = handle

    View Slide

  50. API Response
    CQRS
    (on micro scale)

    View Slide

  51. Your Server as a function
    CQRS
    (on micro scale)

    View Slide

  52. Your Server as a function
    HttpRequest -> HttpResponse

    View Slide

  53. Your Server as a function
    HttpRequest -> Async

    View Slide

  54. Your Server as a function
    HttpRequest -> Query -> QueryHandler -> Async

    View Slide

  55. Your Server as a function
    HttpRequest -> Query
    -> QueryHandler
    -> Async>
    -> Async

    View Slide

  56. HttpRequest -> Query
    -> QueryHandler
    -> Async>
    -> Async
    WebSocketRequest -> Query
    -> QueryHandler
    -> Async>
    -> Async
    AMQPRequest -> Query
    -> QueryHandler
    -> Async>
    -> Async

    View Slide

  57. HttpRequest -> Query
    -> QueryHandler
    -> Async>
    -> Async
    WebSocketRequest -> Query
    -> QueryHandler
    -> Async>
    -> Async
    AMQPRequest -> Query
    -> QueryHandler
    -> Async>
    -> Async

    View Slide

  58. Error
    responses

    View Slide

  59. type ParserError =
    | GameIdFormat of originValues:string[]
    | IncidentIdFormat of originValues:string[]
    | IncidentTypeFormat of originValues:string[]
    type DomainError =
    | IncidentCantBeCreated of gameId:int * typeId:int
    | LimitsOutOfRange of top:int * maxTop:int * skip:int
    | GameNotFound of gameId:int
    | IncidentNotFound of incidentId:int
    | IncidentsForGamesNotFound of gameIds:int[]
    type AppError =
    | Parser of ParserError
    | Domain of DomainError
    static member create(e: ParserError) = e |> Parser
    static member create(e: DomainError) = e |> Domain
    static member createResult(e: DomainError) = e |> Domain |> Error
    static member createResult(e: ParserError) = e |> Parser |> Error

    View Slide

  60. module HTTPErrorResponse =
    let map (error: AppError) = ...
    let fromDomainError (error: DomainError) =
    match error with
    | IncidentCantBeCreated ->
    new HttpResponseMessage(HttpStatusCode.BadReqeust, { ... })
    let fromParserError (error: ParserError) =
    match error with
    | GameIdFormat ids ->
    new HttpResponseMessage(HttpStatusCode.BadReqeust, { ... })
    | IncidentIdFormat ids ->
    new HttpResponseMessage(HttpStatusCode.BadReqeust, { ... })
    match error with
    | Domain e -> fromDomainError(e)
    | Parser e -> fromParserError(e)

    View Slide

  61. Testing

    View Slide

  62. Input -> Output
    Command -> Event

    View Slide

  63. []
    let ``CommandHandler on AddIncident should return IncidentAdded if related game exist`` () =
    let cmdHandler = TestHelper.createCommandHandler()
    let game = TestHelper.createGame(id = 777)
    let incident = TestHelper.createIncident(id = 1, gameId = 777)
    asyncResult {
    let! gameEvent = Command.AddGame(game) |> cmdHandler.Handle
    let! incidentEvent = Command.AddIncident(incident) |> cmdHandler.Handle
    test <@ match incidentEvent with
    | IncidentAdded _ -> true
    | _ -> false @>
    }
    |> TestHelper.runTest

    View Slide

  64. What about
    non-trivial cases
    with no output?

    View Slide

  65. Testing
    Log Events

    View Slide

  66. logger.Warning("Test message")
    logger.Warning("Test message")
    logger.Warning("Test message")
    InMemoryLogger
    .Should()
    .HaveMessage("Test message")
    .Appearing().Times(3)
    .WithLevel(LogEventLevel.Information)

    View Slide

  67. []
    let ``Agent should be able to reconnect automatically and join the cluster`` () = async {
    let coordinatorDep = Dependency.createFor(NodeType.Coordinator)
    let (agentDep, loggerBuffer) = Dependency.createWithInMemoryLogger(NodeType.Agent)
    let server = MqttTests.startMqttServer()
    // we start one agent
    let! agent = Agent.init(agentDep, [| scenario |], agentSettings)
    Agent.startListening(agent) |> Async.Start
    // we stop mqtt server
    MqttTests.stopMqttServer server
    // and wait some time
    do! Async.Sleep(5_000)
    // spin up the mqtt server again to see that agent will reconnect
    let server = MqttTests.startMqttServer()
    let! coordinator = Coordinator.init(coordinatorDep, [| scenario |], coordinatorSettings,
    [| scenarioSettings |], customSettings)
    // waiting on reconnect
    do! Async.Sleep(2_000)
    let! statsResult = Coordinator.run(coordinator)
    let allStats = statsResult |> Result.defaultValue Array.empty

    View Slide

  68. Property based
    testing

    View Slide

  69. Property based testing
    []
    let ``Subscription filter correctness verification`` (allTypes: IncidentType[],
    typesToSubscribe: IncidentType[]) =
    type IncidentType = Goal | Corner | Penalty | RedCard | YellowCard

    View Slide

  70. Property based testing
    allTypes: [Goal; Corner; Penalty],
    typesToSubscribe: [Corner]) =
    type IncidentType = Goal | Corner | Penalty | RedCard | YellowCard

    View Slide

  71. Property based testing
    allTypes: [Goal; Corner; Penalty; Corner;],
    typesToSubscribe: [RedCard; YellowCard]) =
    type IncidentType = Goal | Corner | Penalty | RedCard | YellowCard

    View Slide

  72. []
    let ``Subscription filter corectness verification`` (game: Game,
    allTypes: IncidentType list,
    typesToSubscribe: IncidentType list) =
    let cmdHandler = createCmdHandler()
    let incidents = allTypes
    |> List.map(fun x -> TestHelper.createIncidents(event)
    Command.AddGame(game) |> cmdHandler.Handle |> ignore
    Command.Subscribe([|game.Id|], typesToSubscribe) |> cmdHandler.Handle |> ignore
    let receivedIncidents =
    incidents
    |> List.map(fun x -> Command.AddIncident(x) |> cmdHandler.Handle
    |> retrieveIncidents
    |> List.sum
    let shouldReceive =
    if List.isEmpty(typesToSubscribe) // subscribed on all types
    then incidents.Length
    else
    let machedTypes = Set.intersect (set allTypes) (set typesToSubscribe)
    incidents
    |> List.choose(fun x -> if machedTypes.Contains(enum x.IncidentTypeId) then
    Some 1 else None)
    |> List.sum
    receivedIncidents = shouldReceive

    View Slide

  73. Part III
    Advantages and disadvantages

    View Slide

  74. We stopped
    write frameworks
    with unlimited extensibility
    to cover cases from future

    View Slide

  75. We stopped
    write frameworks
    No abstract factories
    No factory method

    View Slide

  76. We stopped
    write frameworks
    No interfaces per class

    View Slide

  77. We stopped
    write frameworks
    No interfaces like
    ISimpleIdentifiable

    View Slide

  78. We stopped
    write frameworks
    Instead, we build our private
    world where we know everything

    View Slide

  79. Simpler reading
    let publishMsg = Json.serialize
    >> Zip.compress
    >> RabbitMq.createMsg topicName
    >> client.Publish

    View Slide

  80. Simpler reading
    let validate (context: NBomberContext) =
    context.Scenarios
    |> checkEmptyName
    >>= checkDuplicateName
    >>= checkEmptyStepName
    >>= checkDuration
    >>= checkConcurrentCopies
    >>= fun _ -> Ok context
    |> Result.mapError(AppError.create)

    View Slide

  81. Simpler testing

    View Slide

  82. Type Driven
    Development

    View Slide

  83. module CardGame =
    type Suit = Club | Diamond | Spade | Heart
    type Rank = Two | Three | Four | Five | Six | Seven | Eight
    | Nine | Ten | Jack | Queen | King | Ace
    type Card = Suit * Rank
    type Hand = Card list
    type Deck = Card list
    type Player = { Name:string; Hand:Hand}
    type Game = {Deck:Deck; Players: Player list}
    type Deal = Deck -> (Deck*Card)
    type PickupCard = (Hand*Card) -> Hand

    View Slide

  84. Anonymous Types
    let data = {| X = 1; Y = 2 |}
    let data1 = {| data with Y = 3 |} // {| X:Int; Y:Int |}
    let data2 = {| data with Z = 3 |} // {| X:Int; Y:Int; Z:Int |}

    View Slide

  85. type User = { Age: int }
    let user = { Age = 1 }
    let anonymous = {| user with Y=1 |} // {| Age:Int; Y:Int |}
    Anonymous Types

    View Slide

  86. Expressive API
    with
    Explicit Effects

    View Slide

  87. Option - to represent cases when something may exist
    but also may not.
    Result - to represent operations which could
    be success or fail.
    Choice - to model operations which return
    different values depending on the input
    Async - to represent asynchronicity as an effect (in
    addition people quite often use F# Async to model
    impure operations as alternative to IO monad)

    View Slide

  88. Previously working in C# we
    were forced
    to use name conventions

    View Slide

  89. // if method starts with Find then in case of not found
    // it returns null
    public User FindUser(int userId)
    // if method starts with Get then in case of not found
    // it throws Exception
    public User GetUser(int userId)

    View Slide

  90. // if method starts with Try
    // it means that operation can fail and
    // and exception will be not thrown
    public bool TryParse(string json, out Config config)
    // in this case the exception will be thrown
    public Config Parse(string json)

    View Slide

  91. let findUser (userId): Async, AppError>>
    let parse (json): Result

    View Slide

  92. Monad
    Comprehensions

    View Slide

  93. option {
    let! x = getSomething()
    let! y = getSomethingElse()
    let! z = andYetGetSomethingElse()
    // Code will only hit this point if the three
    // operations above return Some value
    return x + y + z
    }

    View Slide

  94. type QueryHandler(storage: IStorage) =
    let handle (query) = asyncResult {
    match query with
    | Incidents (eventIds, incidentTypes, limits) ->
    // vLimits: Result
    let! vLimits = QueryValidation.validateLimits(limits)
    // incidents: Async>
    let! incidents = storage.FindIncidents(eventIds, incidentTypes, vLimits)
    let data = incidents |> Array.map(Incident.toContractType)
    return { Data = data }
    }
    member x.Handle = handle

    View Slide

  95. THANKS
    @antyadev
    [email protected]

    View Slide