thread-supervisor-0.1.0.1: A simplified implementation of Erlang/OTP like supervisor over thread

Copyright(c) Naoto Shimazaki 2018-2020
LicenseMIT (see the file LICENSE)
Maintainerhttps://github.com/nshimaza
Stabilityexperimental
Safe HaskellNone
LanguageHaskell2010

Control.Concurrent.Supervisor

Contents

Description

A simplified implementation of Erlang/OTP like supervisor over thread and underlying behaviors.

Synopsis

Actor and Message queue

Actor is restartable IO action with inbound message queue. Actor is designed to allow other threads sending messages to an actor keep using the same write-end of the queue before and after restart of the actor. Actor consists of message queue and its handler. Inbox is a message queue designed for actor's message inbox. It is thread-safe, bounded or unbounded, and selectively readable queue.

To protect read-end of the queue, it has different type for read-end and write-end. Message handler of actor can access to both end but only write-end is accessible from outside of message handler. To realize this, constructor of Inbox is not exposed. The only way to create a new Inbox object is creating a new actor using newActor function.

newActor :: (Inbox message -> IO result) -> IO (Actor message result)

This package provides type synonym for message handler as below.

type ActorHandler message result = (Inbox message -> IO result)

newActor receives an user supplied message handler, creates a new Inbox value, then returns write-end of actor's message queue and IO action of the actor's body wrapped by Actor. Actor is defined as following.

data Actor message result = Actor
    { actorQueue  :: ActorQ message -- ^ Write end of message queue of 'Actor'
    , actorAction :: IO result      -- ^ IO action to execute 'Actor'
    }

The 'ActorQ message' in the Actor is the write-end of created Inbox. While user supplied message handler receives Inbox, which is read-end of created queue, caller of newActor gets write-end only.

Message queue

Inbox is specifically designed queue for implementing actor. All behaviors available in this package depend on it. It provides following capabilities.

  • Thread-safe read/pull/receive and write/push/send.
  • Blocking and non-blocking read operation.
  • Selective read operation.
  • Current queue length.
  • Bounded queue.

The type Inbox is intended to be used only for pulling side as inbox of actor. Single Inbox object is only readable from single actor. In order to avoid from other actors, no Inbox constructor is exposed but instead you can get it only via newActor or newBoundedActor.

Read an oldest message from Inbox

To read a message at the head of message queue, apply receive to Inbox. If one or more message is available, receive returns oldest one. If no message is available, receive blocks until at least one message arrives. A skeleton of actor message handler will look like this.

myActorHandler :: Inbox YourMessageType -> IO ()
myActorHandler inbox = do
    newMessage <- receive inbox
    doSomethingWith newMessage
    myActorHandler inbox

Send a message to an actor

To send a message to an actor, call send with write-end of the actor's inbox and the message.

    send :: ActorQ message -> message -> IO ()

ActorQ is write-end of actor's message queue. ActorQ is actually just a wrapper of Inbox. Its role is hiding read-end API of Inbox. From outside of actor, only write-end is exposed via ActorQ. From inside of actor, both read-end and write-end are available. You can read from given inbox directly. You need to wrap the inbox by Actor when you write to the inbox.

Send a message from an actor to itself

When you need to send a message to your inbox, do this.

    send (ActorQ yourInbox) message

You can convert Inbox (read-end) to ActorQ (write-end) by wrapping Inbox by ActorQ so that you can send a message from an actor to itself.

myActorHandler :: Inbox YourMessageType -> IO ()
myActorHandler inbox = do
    newMessage <- receive inbox
    doSomethingWith newMessage

    send (ActorQ inbox) messageToMyself -- Send a message to itself.

    myActorHandler inbox

data Inbox a Source #

Message queue abstraction.

newtype ActorQ a Source #

Write end of Inbox exposed to outside of actor.

Constructors

ActorQ (Inbox a) 

send Source #

Arguments

:: ActorQ a

Write-end of target actor's message queue.

-> a

Message to be sent.

-> IO () 

Send a message to given ActorQ. Block while the queue is full.

trySend Source #

Arguments

:: ActorQ a

Write-end of target actor's message queue.

-> a

Message to be sent.

-> IO (Maybe ()) 

Try to send a message to given ActorQ. Return Nothing if the queue is already full.

length :: ActorQ a -> IO Word Source #

Number of elements currently held by the ActorQ.

receive :: Inbox a -> IO a Source #

Receive first message in Inbox. Block until message available.

tryReceive :: Inbox a -> IO (Maybe a) Source #

Try to receive first message in Inbox. It returns Nothing if there is no message available.

receiveSelect Source #

Arguments

:: (a -> Bool)

Predicate to pick a interesting message.

-> Inbox a

Message queue where interesting message searched for.

-> IO a 

Perform selective receive from given Inbox.

receiveSelect searches given queue for first interesting message predicated by user supplied function. It applies the predicate to the queue, returns the first element that satisfy the predicate, and remove the element from the Inbox.

It blocks until interesting message arrived if no interesting message was found in the queue.

Caution

Use this function with care. It does NOT discard any message unsatisfying predicate. It keeps them in the queue for future receive and the function itself blocks until interesting message arrived. That causes your queue filled up by non-interesting messages. There is no escape hatch.

Consider using tryReceiveSelect instead.

Caveat

Current implementation has performance caveat. It has O(n) performance characteristic where n is number of messages existing before your interested message appears. It is because this function performs liner scan from the top of the queue every time it is called. It doesn't cache pair of predicates and results you have given before.

Use this function in limited situation only.

tryReceiveSelect Source #

Arguments

:: (a -> Bool)

Predicate to pick a interesting message.

-> Inbox a

Message queue where interesting message searched for.

-> IO (Maybe a) 

Try to perform selective receive from given Inbox.

tryReceiveSelect searches given queue for first interesting message predicated by user supplied function. It applies the predicate to the queue, returns the first element that satisfy the predicate, and remove the element from the Inbox.

It return Nothing if there is no interesting message found in the queue.

Caveat

Current implementation has performance caveat. It has O(n) performance characteristic where n is number of messages existing before your interested message appears. It is because this function performs liner scan from the top of the queue every time it is called. It doesn't cache pair of predicates and results you have given before.

Use this function in limited situation only.

Actor

Actor is IO action emulating Erlang's actor. It has a dedicated Inbox and processes incoming messages until reaching end state.

Actor is restartable without replacing message queue. When actor's IO action crashed and restarted, the new execution of the IO action continue referring the same message queue. Thus, threads sending messages to the actor can continue using the same write-end of the queue.

newActor and newBoundedActor create an actor with new Inbox. It is the only exposed way to create a new Inbox. This limitation is intended. It prevents any code other than message handler of the actor from reading the inbox.

From perspective of outside of actor, user supplies an IO action with type ActorHandler to newActor or newBoundedActor then user gets IO action of created actor and write-end of message queue of the actor, which is ActorQ type value.

From perspective of inside of actor, in other word, from perspective of user supplied message handler, it has a message queue both read and write side available.

Shared Inbox

You can run created actor multiple time simultaneously with different thread each. In such case, each actor instances share single Inbox. This would be useful to distribute task stream to multiple worker actor instances, however, keep in mind there is no way to control which message is routed to what actor.

type ActorHandler message result = Inbox message -> IO result Source #

Type synonym of user supplied message handler working inside actor.

data Actor message result Source #

Actor representation.

Constructors

Actor 

Fields

newActor Source #

Arguments

:: ActorHandler message result

IO action handling received messages.

-> IO (Actor message result) 

Create a new actor.

Users have to supply a message handler function with ActorHandler type. ActorHandler accepts a Inbox and returns anything.

newActor creates a new Inbox, apply user supplied message handler to the queue, returns reference to write-end of the queue and IO action of the actor. Because newActor only returns ActorQ, caller of newActor can only send messages to created actor. Caller cannot receive message from the queue.

Inbox, or read-end of the queue, is passed to user supplied message handler so the handler can receive message to the actor. If the handler need to send a message to itself, wrap the message queue by ActorQ constructor then use send over created ActorQ. Here is an example.

send (ActorQ yourInbox) message

newBoundedActor Source #

Arguments

:: InboxLength

Maximum length of inbox message queue.

-> ActorHandler message result

IO action handling received messages.

-> IO (Actor message result) 

Create a new actor with bounded inbox queue.

Supervisable IO action

Monitored action

This package provides facility for supervising IO actions. With types and functions described in this section, you can run IO action with its own thread and receive notification on its termination at another thread with reason of termination. Functions in this section provides guaranteed supervision of your thread.

It looks something similar to bracket. What distinguishes from bracket is guaranteed work through entire lifetime of thread.

Use bracket when you need guaranteed cleanup of resources acquired within the same thread. It works as you expect. However, installing callback for thread supervision using bracket (or finally or even low level catch) within a thread has NO guarantee. There is a little window where asynchronous exception is thrown after the thread is started but callback is not yet installed. We will discuss this later in this section.

Notification is delivered via user supplied callback. Helper functions described in this section install your callback to your IO action. Then the callback will be called on termination of the IO action.

Important: Callback is called in terminated thread

Callback is called in terminated thread. You have to use inter-thread communication in order to notify to another thread.

User supplied callback receives ExitReason and ThreadId so that user can determine witch thread was terminated and why it was terminated. In order to receive those parameters, user supplied callback must have type signature Monitor, which is following.

ExitReason -> ThreadId -> IO ()

Function watch installs your callback to your plain IO action then returns monitored action.

Callback can be nested. Use nestWatch to install another callback to already monitored action.

Helper functions return IO action with signature MonitoredAction instead of plain IO (). From here to the end of this section it will be a little technical deep dive for describing why it has such signature.

The signature of MonitoredAction is this.

(IO () -> IO ()) -> IO ()

It requires an extra function argument. It is because MonitoredAction will be invoked with forkIOWithUnmask.

In order to ensure callback on termination works in any timing, the callback must be installed under asynchronous exception masked. At the same time, in order to allow killing the tread from another thread, body of IO action must be executed under asynchronous exception unmasked. In order to satisfy both conditions, the IO action and callback must be called using forkIOWithUnmask. Typically it looks like following.

mask_ $ forkIOWithUnmask $ \unmask -> unmask action `finally` callback

The extra function parameter in the signature of MonitoredAction is used for accepting the unmask function which is passed by forkIOWithUnmask. Functions defined in this section help installing callback and converting type to fit to forkIOWithUnmask.

type MonitoredAction = (IO () -> IO ()) -> IO () Source #

MonitoredAction is type synonym of function with callback on termination installed. Its type signature fits to argument for forkIOWithUnmask.

data ExitReason Source #

ExitReason indicates reason of thread termination.

Constructors

Normal

Thread was normally finished.

UncaughtException SomeException

A synchronous exception was thrown and it was not caught. This indicates some unhandled error happened inside of the thread handler.

Killed

An asynchronous exception was thrown. This also happen when the thread was killed by supervisor.

type Monitor Source #

Arguments

 = ExitReason

Reason of thread termination.

-> ThreadId

ID of terminated thread.

-> IO () 

Monitor is user supplied callback function which is called when monitored thread is terminated.

watch :: Monitor -> IO () -> MonitoredAction Source #

Install Monitor callback function to simple IO action.

nestWatch :: Monitor -> MonitoredAction -> MonitoredAction Source #

Install another Monitor callback function to MonitoredAction.

noWatch :: IO () -> MonitoredAction Source #

Convert simple IO action to MonitoredAction without installing Monitor.

Child Specification

ChildSpec is casting mold of child thread IO action which supervisor spawns and manages. It is passed to supervisor, then supervisor let it run with its own thread, monitor it, and restart it if needed. ChildSpec provides additional attributes to MonitoredAction for controlling restart on thread termination. That is Restart. Restart represents restart type concept came from Erlang/OTP. The value of Restart defines how restart operation by supervisor is triggered on termination of the thread. ChildSpec with Permanent restart type triggers restart operation regardless its reason of termination. It triggers restarting even by normal exit. Transient triggers restarting only when the thread is terminated by exception. Temporary never triggers restarting.

Refer to Erlang/OTP for more detail of restart type concept.

newMonitoredChildSpec creates a new ChildSpec from a MonitoredAction and a restart type value. newChildSpec is short cut function creating a ChildSpec from a plain IO action and a restart type value. addMonitor adds another monitor to existing ChildSpec.

data Restart Source #

Restart defines when terminated child thread triggers restart operation by its supervisor. Restart only defines when it triggers restart operation. It does not directly means if the thread will be or will not be restarted. It is determined by restart strategy of supervisor. For example, a static Temporary child never triggers restart on its termination but static Temporary child will be restarted if another Permanent or Transient thread with common supervisor triggered restart operation and the supervisor has OneForAll strategy.

Constructors

Permanent

Permanent thread always triggers restart.

Transient

Transient thread triggers restart only if it was terminated by exception.

Temporary

Temporary thread never triggers restart.

Instances
Eq Restart Source # 
Instance details

Defined in Control.Concurrent.SupervisorInternal

Methods

(==) :: Restart -> Restart -> Bool #

(/=) :: Restart -> Restart -> Bool #

Show Restart Source # 
Instance details

Defined in Control.Concurrent.SupervisorInternal

data ChildSpec Source #

ChildSpec is representation of IO action which can be supervised by supervisor. Supervisor can run the IO action with separate thread, watch its termination and restart it based on restart type.

newChildSpec Source #

Arguments

:: Restart

Restart type of resulting ChildSpec. One of Permanent, Transient or Temporary.

-> IO ()

User supplied IO action which the ChildSpec actually does.

-> ChildSpec 

Create a ChildSpec from plain IO action.

newMonitoredChildSpec Source #

Arguments

:: Restart

Restart type of resulting ChildSpec. One of Permanent, Transient or Temporary.

-> MonitoredAction

User supplied monitored IO action which the ChildSpec actually does.

-> ChildSpec 

Create a ChildSpec from MonitoredAction.

addMonitor Source #

Arguments

:: Monitor

Callback function called when the IO action of the ChildSpec terminated.

-> ChildSpec

Existing ChildSpec where the Monitor is going to be added.

-> ChildSpec

Newly created ChildSpec with the Monitor added.

Add a Monitor function to existing ChildSpec.

Supervisor

data RestartSensitivity Source #

RestartSensitivity defines condition how supervisor determines intensive restart is happening. If more than restartSensitivityIntensity time of restart is triggered within restartSensitivityPeriod, supervisor decides intensive restart is happening and it terminates itself. Default intensity (maximum number of acceptable restart) is 1. Default period is 5 seconds.

Constructors

RestartSensitivity 

Fields

data IntenseRestartDetector Source #

IntenseRestartDetector keeps data used for detecting intense restart. It keeps maxR (maximum restart intensity), maxT (period of majoring restart intensity) and history of restart with system timestamp in Monotonic form.

newIntenseRestartDetector :: RestartSensitivity -> IntenseRestartDetector Source #

Create new IntenseRestartDetector with given RestartSensitivity parameters.

detectIntenseRestart Source #

Arguments

:: IntenseRestartDetector

Intense restart detector containing history of past restart with maxT and maxR

-> TimeSpec

System timestamp in Monotonic form when the last restart was triggered.

-> (Bool, IntenseRestartDetector)

Returns True if intensive restart is happening. Returns new history of restart which has the oldest history removed if possible.

Determine if the last restart results intensive restart. It pushes the last restart timestamp to the DelayedQueue of restart history held inside of the IntenseRestartDetector then check if the oldest restart record is pushed out from the queue. If no record was pushed out, there are less number of restarts than limit, so it is not intensive. If a record was pushed out, it means we had one more restarts than allowed. If the oldest restart and newest restart happened within allowed time interval, it is intensive.

This function implements pure part of detectIntenseRestartNow.

detectIntenseRestartNow Source #

Arguments

:: IntenseRestartDetector

Intense restart detector containing history of past restart with maxT and maxR.

-> IO (Bool, IntenseRestartDetector) 

Determine if intensive restart is happening now. It is called when restart is triggered by some thread termination.

data Strategy Source #

Restart strategy of supervisor

Constructors

OneForOne

Restart only exited thread.

OneForAll

Restart all threads supervised by the same supervisor of exited thread.

type SupervisorQueue = ActorQ SupervisorMessage Source #

Type synonym for write-end of supervisor's message queue.

newSupervisor Source #

Arguments

:: Strategy

Restarting strategy of monitored threads. OneForOne or OneForAll.

-> RestartSensitivity

Restart intensity sensitivity in restart count and period.

-> [ChildSpec]

List of supervised child specifications.

-> ActorHandler SupervisorMessage () 

Create a supervisor.

When created supervisor IO action started, it automatically creates child threads based on given ChildSpec list and supervise them. After it created such static children, it listens given SupervisorQueue. User can let the supervisor creates dynamic child thread by calling newChild. Dynamic child threads created by newChild are also supervised.

When the supervisor thread is killed or terminated in some reason, all children including static children and dynamic children are all killed.

With OneForOne restart strategy, when a child thread terminated, it is restarted based on its restart type given in ChildSpec. If the terminated thread has Permanent restart type, supervisor restarts it regardless its exit reason. If the terminated thread has Transient restart type, and termination reason is other than Normal (meaning UncaughtException or Killed), it is restarted. If the terminated thread has Temporary restart type, supervisor does not restart it regardless its exit reason.

Created IO action is designed to run in separate thread from main thread. If you try to run the IO action at main thread without having producer of the supervisor queue you gave, the supervisor will dead lock.

newSimpleOneForOneSupervisor :: ActorHandler SupervisorMessage () Source #

Create a supervisor with OneForOne restart strategy and has no static ChildSpec. When it started, it has no child threads. Only newChild can add new thread supervised by the supervisor. Thus the simple one-for-one supervisor only manages dynamic and Temporary children.

newChild Source #

Arguments

:: CallTimeout

Request timeout in microsecond.

-> SupervisorQueue

Inbox message queue of the supervisor to ask new thread.

-> ChildSpec

Child specification of the thread to spawn.

-> IO (Maybe ThreadId) 

Ask the supervisor to spawn new temporary child thread. Returns ThreadId of the new child.

State machine

State machine behavior is most essential behavior in this package. It provides framework for creating IO action of finite state machine running on its own thread. State machine has single Inbox, its local state, and a user supplied message handler. State machine is created with initial state value, waits for incoming message, passes received message and current state to user supplied handler, updates state to returned value from user supplied handler, stops or continue to listen message queue based on what the handler returned.

To create a new state machine, prepare initial state of your state machine and define your message handler driving your state machine, apply newStateMachine to the initial state and handler. You will get a ActorHandler so you can get an actor of the state machine by applying newActor to it.

Actor queue action <-  newActor $ newStateMachine initialState handler

Or you can use short-cut helper.

Actor queue action <-  newStateMachineActor initialState handler

The newStateMachine returns write-end of message queue for the state machine and IO action to run. You can run the IO action by forkIO or async, or you can let supervisor run it.

User supplied message handler must have following type signature.

handler :: (state -> message -> IO (Either result state))

When a message is sent to state machine's queue, it is automatically received by state machine framework, then the handler is called with current state and the message. The handler must return either result or next state. When 'Left result' is returned, the state machine stops and returned value of the IO action is IO result. When 'Right state' is returned, the state machine updates current state with the returned state and wait for next incoming message.

newStateMachine Source #

Arguments

:: state

Initial state of the state machine.

-> (state -> message -> IO (Either result state))

Message handler which processes event and returns result or next state.

-> ActorHandler message result 

Create a new finite state machine.

The state machine waits for new message at Inbox then callback user supplied message handler. The message handler must return Right with new state or Left with final result. When Right is returned, the state machine waits for next message. When Left is returned, the state machine terminates and returns the result.

newStateMachine returns an IO action wrapping the state machine described above. The returned IO action can be executed within an Async or bare thread.

Created IO action is designed to run in separate thread from main thread. If you try to run the IO action at main thread without having producer of the message queue you gave, the state machine will dead lock.

newStateMachineActor :: state -> (state -> message -> IO (Either result state)) -> IO (Actor message result) Source #

create an unbound actor of newStateMachine. Short-cut of following.

newActor $ newStateMachine initialState messageHandler

Simple server behavior

Server behavior provides synchronous request-response style communication, a.k.a. ask pattern, with actor. Server behavior allows user to send a request to an actor then wait for response form the actor. This package provides a framework for implementing such actor.

Server behavior in this package is actually a set of helper functions and type synonym to help implementing ask pattern over actor. User need to follow some of rules described below to utilize those helpers.

Define ADT type for messages

First, user need to define an algebraic data type for message to the server in following form.

data myServerCommand
    = ReqWithoutResp1
    | ReqWithoutResp2 Arg1
    | ReqWithoutResp3 Arg2 Arg3
    | ReqWithResp1 (ServerCallback Result1)
    | ReqWithResp1 ArgX (ServerCallback Result2)
    | ReqWithResp2 ArgY ArgZ (ServerCallback Result3)

The rule is this:

  • Define an ADT containing all requests.
  • If a request doesn't return response, define a value type for the request as usual element of sum type.
  • If a request returns a response, put (ServerCallback ResultType) at the last argument of the constructor for the request where ResultType is type of returned value.

ServerCallback is type synonym of a function type as following.

type ServerCallback a = (a -> IO ())

So real definition of your myServerCommand is:

data MyServerCommand
    = ReqWithoutResp1
    | ReqWithoutResp2 Arg1
    | ReqWithoutResp3 Arg2 Arg3
    | ReqWithResp1 (Result1 -> IO ())
    | ReqWithResp2 ArgX (Result2 -> IO ())
    | ReqWithResp3 ArgY ArgZ (Result3 -> IO ())

Define message handler

Next, user need to define an actor handling the message. In this example, we will use state machine behavior so that we can focus on core message handling part. For simplicity, this example doesn't have internal state and it never finishes.

Define a state machine message handler handling myServerCommand.

myHandler :: () -> MyServerCommand -> IO (Either () ())
myHandler _  ReqWithoutResp1                  = doJob1 $> Right ()
myHandler _ (ReqWithoutResp2 arg1)            = doJob2 arg1 $> Right ()
myHandler _ (ReqWithoutResp3 arg2 arg3)       = doJob3 arg2 arg3 $> Right ()
myHandler _ (ReqWithResp1 cont1)              = (doJob4 >>= cont1) $> Right ()
myHandler _ (ReqWithResp2 argX cont2)         = (doJob5 argX >>= cont2) $> Right ()
myHandler _ (ReqWithResp3 argY argZ cont3)    = (doJob6 argY argZ >>= cont3) $> Right ()

The core idea here is implementing request handler in CPS style. If a request returns a response, the request message comes with callback function (a.k.a. continuation). You can send back response for the request by calling the callback.

Requesting to server

Function call, callAsync, and callIgnore are helper functions to implement request-response communication with server. They install callback to message, send the message, returns response to caller. They receive partially applied server message constructor, apply it to callback function, then send it to server. The installed callback handles response from the server. You can use call like following.

    maybeResult1 <- call def myServerActor ReqWithResp1
    maybeResult2 <- call def myServerActor $ ReqWithResp2 argX
    maybeResult3 <- call def myServerActor $ ReqWithResp3 argY argZ

When you send a request without response, use cast.

    cast myServerActor ReqWithoutResp1
    cast myServerActor $ ReqWithoutResp2 arg1
    cast myServerActor $ ReqWithoutResp3 arg2 arg3

When you send a request with response but ignore it, use callIgnore.

    callIgnore myServerActor ReqWithResp1
    callIgnore myServerActor $ ReqWithResp2 argX
    callIgnore myServerActor $ ReqWithResp3 argY argZ

Generally, ask pattern, or synchronous request-response communication is not recommended in actor model. It is because synchronous request blocks entire actor until it receives response or timeout. You can mitigate the situation by wrapping the synchronous call with async. Use callAsync for such purpose.

newtype CallTimeout Source #

Timeout of call method for server behavior in microseconds. Default is 5 second.

Constructors

CallTimeout Int 
Instances
Default CallTimeout Source # 
Instance details

Defined in Control.Concurrent.SupervisorInternal

Methods

def :: CallTimeout #

type ServerCallback a = a -> IO () Source #

Type synonym of callback function to obtain return value.

cast Source #

Arguments

:: ActorQ cmd

Message queue of the target server.

-> cmd

Request to the server.

-> IO () 

Send an asynchronous request to a server.

call Source #

Arguments

:: CallTimeout

Timeout.

-> ActorQ cmd

Message queue of the target server.

-> (ServerCallback a -> cmd)

Request to the server without callback supplied.

-> IO (Maybe a) 

Send an synchronous request to a server and waits for a return value until timeout.

callAsync Source #

Arguments

:: CallTimeout

Timeout.

-> ActorQ cmd

Message queue.

-> (ServerCallback a -> cmd)

Request to the server without callback supplied.

-> (Maybe a -> IO b)

callback to process return value of the call. Nothing is given on timeout.

-> IO (Async b) 

Make an asynchronous call to a server and give result in CPS style. The return value is delivered to given callback function. It also can fail by timeout. Calling thread can wait for a return value from the callback.

Use this function with care because there is no guaranteed cancellation of background worker thread other than timeout. Giving infinite timeout (zero) to the CallTimeout argument may cause the background thread left to run, possibly indefinitely.

callIgnore Source #

Arguments

:: ActorQ cmd

Message queue of the target server.

-> (ServerCallback a -> cmd)

Request to the server without callback supplied.

-> IO () 

Send an request to a server but ignore return value.