question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Hopac + ZeroMQ Question

See original GitHub issue

Hi,

first off, sorry for filing an issue here, but I wasn’t sure what is the best way of asking a question related to Hopac, so here it is. I suspect that I am overlooking something important related to Hopac internals, but it could be a different issue altogether. I tried to keep in mind that Hopac jobs should not block on, e.g., IO for long times to keep the machine well-greased (see the 100ms timeouts on polling). I am not sure how to debug Hopac internals, so it would be interesting to me learn how to hunt down those kinds of issues.

The Setup

So, I am experimenting with the following setup, in which there is a ZeroMQ IServer component accepting connections and routing them to back-end workers, which, in turn, queue up request on a Mailbox to be processed downstream. Additionally, there is a simple client which issues requests, and a root level handler which simply reverses the request payload and returns it.

The Issue

The issue I’m seeing is that if I start the client independently of the server, the client works as expected (i.e. it enters the loop, takes requests, times out as expected and generally works). Yet, if I start the server before the client, the client never executes its inner server loop and blocks infinitely on a request. It looks as if this is a Hopac issue, because I have another, similar setup with Hopac involving a slightly different client/server setup (using a REP/REQ pair) that works as expected. I can also post that if needed.

Reproduce

To reproduce, the followng snipped can be used:

let ctx = new ZContext()

let mutable respond = true

let handler (request: Request) (handler: ResponseHandler) =
  if respond then
    let chars = request.ToCharArray()
    Array.Reverse chars
    let response = String chars
    handler response

let server = Server.create 4 ctx handler // start a server with 4 workers
let client = Client.create ctx           // start a client instance

client.Request "ohai" // blocks indefinitely

To demonstrate why I believe something on the Hopac side is not working correctly this snipped should normally print hello after one second, but doesn’t. Additionally, when broken up into separate processes, both client and server work as expected.

job {
  do! timeOut (TimeSpan.FromSeconds 1.0)
  printfn "hello"
} |> Hopac.start

Expected Behavior

The client should start its inner loop and issue requests.

Code

open Hopac
open Hopac.Infixes
open ZeroMQ
open System
open System.Threading
open System.Diagnostics

//   ____                _              _
//  / ___|___  _ __  ___| |_ __ _ _ __ | |_ ___
// | |   / _ \| '_ \/ __| __/ _` | '_ \| __/ __|
// | |__| (_) | | | \__ \ || (_| | | | | |_\__ \
//  \____\___/|_| |_|___/\__\__,_|_| |_|\__|___/

module Constants =
  let backend = "inproc://backend"
  let frontend = "tcp://127.0.0.1:5555"

//  _   _ _   _ _
// | | | | |_(_) |___
// | | | | __| | / __|
// | |_| | |_| | \__ \
//  \___/ \__|_|_|___/

[<AutoOpen>]
module Utils =

  let timedOut (timer: Stopwatch) =
    if timer.ElapsedMilliseconds > 1000L then
      timer.Stop()
      true
    else false

  let dispose (t: 't when 't :> IDisposable) =
    try  (t :> IDisposable).Dispose()
    with | _ -> ()

type Request = string
type Response = string
type ResponseHandler = string -> unit

//   ____ _ _            _
//  / ___| (_) ___ _ __ | |_
// | |   | | |/ _ \ '_ \| __|
// | |___| | |  __/ | | | |_
//  \____|_|_|\___|_| |_|\__|

type IClient =
  inherit IDisposable
  abstract Request: Request -> Response

module Client =

  type private ClientState(ctx: ZContext) =
    let id = Guid.NewGuid()

    let req:Ch<string * IVar<string>> = Ch()
    let poll = ZPollItem.CreateReceiver()

    [<DefaultValue>]
    val mutable Socket: ZSocket

    member state.Id
      with get () = id.ToByteArray()

    member state.Req
      with get () = req

    member state.Poll
      with get () = poll

    member state.Start() =
      let socket = new ZSocket(ctx, ZSocketType.DEALER) // DEALER
      socket.Identity <- id.ToByteArray()
      socket.Linger <- TimeSpan.FromMilliseconds(1.0)
      socket.Connect(Constants.frontend)
      state.Socket <- socket

    member state.Restart() =
      dispose state
      state.Start()

    interface IDisposable with
      member self.Dispose() =
        dispose self.Socket

  let private loop (state: ClientState) = job {
      printfn "[client] waiting for request"

      let! (payload, ivar) = Ch.take state.Req

      let mutable error = ZError.None
      let t = Nullable(TimeSpan.FromMilliseconds 1.0)

      use msg = new ZMessage()
      msg.Add(new ZFrame(state.Id))
      msg.Add(new ZFrame(payload))

      printfn "[client] sending request"
      let result = state.Socket.Send(msg, &error)

      if result then
        let mutable incoming = Unchecked.defaultof<ZMessage>

        let timer = new Stopwatch()
        timer.Start()

        printfn "[client] waiting for reply"
        while not (state.Socket.PollIn(state.Poll, &incoming, &error, t)) && not (timedOut timer) do
          do! timeOut (TimeSpan.FromMilliseconds 1.0)

        if timedOut timer then
          printfn "[client] timeout"
          do! IVar.fill ivar "error"
        else
          let response = incoming.[0].ReadString()
          printfn "[client] got a response"
          do! IVar.fill ivar response
      else
        printfn "[client] sending unsuccessful"
        do! IVar.fill ivar "error"

      return state
    }

  let create (ctx: ZContext) =
    let state = new ClientState(ctx)
    state.Start()

    printfn "[client] starting server loop"

    loop
    |> Job.iterateServer state
    |> Hopac.start

    printfn "[client] returning"

    { new IClient with
        member client.Request (request: Request) =
          job {
            let ivar = IVar()

            printfn "[client]] passing request to loop"
            do! Ch.give state.Req (request, ivar)

            printfn "[client]] waiting for response from loop"
            let! result = IVar.read ivar

            return result
          }
          |> Hopac.run

        member client.Dispose () =
          dispose state }

//  ____
// / ___|  ___ _ ____   _____ _ __
// \___ \ / _ \ '__\ \ / / _ \ '__|
//  ___) |  __/ |   \ V /  __/ |
// |____/ \___|_|    \_/ \___|_|

type IServer =
  inherit IDisposable

module Server =

  type IWorker =
    inherit IDisposable

  type Workers = IWorker array
  type RequestPipeline = Mailbox<Request * (Response -> unit)>

  type private ServerState =
    { Frontend: ZSocket
      Backend: ZSocket
      Workers: Workers
      Mailbox: RequestPipeline }

    interface IDisposable with
      member self.Dispose() =
        Array.iter dispose self.Workers
        dispose self.Frontend
        dispose self.Backend

  module private Worker =
    type private WorkerState(ctx: ZContext, mb: RequestPipeline) =
      let timeout = Nullable(TimeSpan.FromMilliseconds 100.0)
      let poll = ZPollItem.CreateReceiver()

      [<DefaultValue>]
      val mutable Socket: ZSocket

      member worker.Poll
        with get () = poll

      member worker.Timeout
        with get () = timeout

      member worker.Mailbox
        with get () = mb

      member worker.Start() =
        worker.Socket <- new ZSocket(ctx, ZSocketType.DEALER)
        worker.Socket.Connect(Constants.backend)

      interface IDisposable with
        member self.Dispose() =
          dispose self.Socket

    let private handle (var: IVar<Response>) (response: Response) =
      response |> IVar.fill var |> Hopac.queue

    let private loop (state: WorkerState) = job {
      let mutable error = ZError.None
      let mutable incoming = Unchecked.defaultof<ZMessage>

      if state.Socket.PollIn(state.Poll, &incoming, &error, state.Timeout) then
        let ident = incoming.[1].Read()
        let str = incoming.[2].ReadString()

        let var = IVar()
        do! Mailbox.send state.Mailbox (str, handle var)

        let timer = Stopwatch()
        timer.Start()

        while not (IVar.Now.isFull var) && not (timedOut timer) do
         do! timeOut (TimeSpan.FromMilliseconds 1.0)

        if timedOut timer then
          printfn "[worker] timeout on backend"
        else
          let! response = IVar.read var

          use reply = new ZMessage()
          reply.Add(new ZFrame(ident))
          reply.Add(new ZFrame(response))

          if state.Socket.Send(reply, &error) then
            printfn "[worker] replied successfully"
          else
            printfn "[worker] error sending response %O" error
      else
        if error <> ZError.EAGAIN then
          printfn "[worker] error during poll %O" error

      return state
    }

    let private onRequest (cb: Request -> ResponseHandler -> unit) (mb: RequestPipeline): Job<RequestPipeline> =
      Mailbox.take mb >>= fun (request, handler) ->
        cb request handler
        Job.result mb

    let create (mb: RequestPipeline) (ctx: ZContext) (cb: Request -> ResponseHandler -> unit) =
      printfn "[worker] starting"

      let state = new WorkerState(ctx, mb)
      state.Start()

      onRequest cb
      |> Job.iterateServer mb
      |> Hopac.start

      loop
      |> Job.iterateServer state
      |> Hopac.start

      { new IWorker with
          member server.Dispose() =
            dispose state }


  let create (n: int) (ctx: ZContext) (handler: Request -> ResponseHandler -> unit)  =
    let mailbox = RequestPipeline()

    printfn "[server] creating frontend"
    let frontend = new ZSocket(ctx, ZSocketType.ROUTER)
    frontend.Bind(Constants.frontend)

    printfn "[server] creating backend"
    let backend = new ZSocket(ctx, ZSocketType.DEALER)
    backend.Bind(Constants.backend)

    printfn "[server] creating worker"
    let workers = new ResizeArray<IWorker>()
    for n in 0 .. n - 1 do
      let worker = Worker.create mailbox ctx handler
      workers.Add worker

    let thread = new Thread(fun () ->
      printfn "[server] setting up proxy"
      // proxy messages
      let mutable err = ZError.None
      if not (ZContext.Proxy(frontend, backend, &err)) then
        printfn "[server] error during proxy setup %O" err
        ZException(err) |> raise
      printfn "[server] proxy not started: %O" err)
    thread.Start()

    printfn "[server] startup done"
    let state =
      { Frontend = frontend
        Backend = backend
        Workers = workers.ToArray()
        Mailbox = mailbox }

    { new IServer with
        member self.Dispose() =
          dispose state }

Thank you for any hints!

Karsten

Issue Analytics

  • State:closed
  • Created 6 years ago
  • Comments:7 (5 by maintainers)

github_iconTop GitHub Comments

2reactions
hafcommented, Dec 22, 2017

Here’s a sample that can push 4000+ data points a second. https://github.com/logary/logary/blob/master/src/services/Logary.Services.Rutta/Rutta.fs on a beefy machine (40 cores) at total=2% CPU usage)

0reactions
hafcommented, Dec 22, 2017

Closing because I think this has been answered.

Read more comments on GitHub >

github_iconTop Results From Across the Web

Chapter 2 - Sockets and Patterns
A common question that newcomers to ZeroMQ ask (it's one I've asked myself) is, “how do I write an XYZ server in ZeroMQ?”...
Read more >
Chapter 3 - Advanced Request-Reply Patterns
It solves the main problem with simple round robin routing (as PUSH and DEALER offer) which is that round robin becomes inefficient if...
Read more >
Chapter 4 - Reliable Request-Reply Patterns
This chapter looks at the general question of reliability and builds a set of reliable messaging patterns on top of ZeroMQ's core request-reply...
Read more >
Chapter 5 - Advanced Pub-Sub Patterns
This solves the problem of how a new subscriber catches up when it joins the network. The theory is that publishers get notified...
Read more >
1. Basics | ØMQ - The Guide
How to explain ZeroMQ? Some of us start by saying all the wonderful things it does. It's sockets on steroids. It's like mailboxes...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found