OTP Supervisor

Typically, the application in the entry point will kick off a single supervision tree that will contain other supervision trees as well as various worker processes (typically gen servers but absolutely not strictly so).

A supervisor has a name (because we might want to interact with it programatically once it has started), and typically exposes a function startLink that will call Supervisor.StartLink passing in a callback to an ‘init’ function that’ll be invoked by OTP within the context of supervision process in order to get the children that need starting as part of this supervision tree.

1
2
3
startLink :: Effect (StartLinkResult SupervisorPid)
startLink = do
  Sup.startLink (Just $ Local $ atom "example_usp") init

At the heart of it, a supervision tree is just a list of children that will be spun up by the supervisor, and instructions (flags) on what to do when those children crash. The names of everything in this specification map onto the names in the underlying erlang API so for the most part no explicit documentation is required from it.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
init :: Effect SupervisorSpec
init = do
  connectionString <- BookConfig.connectionString
  webPort <- BookConfig.webPort
  -- Unsurprisingly, this looks just like the specs in the Erlang docs
  -- this is intentional
  -- the difference being that the args for the startLinks are passed in to create Effect Units 
  -- which means they're typed
  pure
    { flags:
        { strategy: OneForOne
        , intensity: 1
        , period: Seconds 5.0
        }
    , childSpecs:
        (worker "book_web" $ BookWeb.startLink { webPort })
        : (worker "empty_server" $ EmptyGenServer.startLink {})
        : (worker "book_library" $ BookLibrary.startLink { connectionString })
        : (worker "handle_info_example" $ HandleInfoExample.startLink {})
        : (worker "monitor_example" $ MonitorExample.startLink {})
        : (worker "one_for_one_example" $ OneForOneSup.startLink)
        : nil
    }

That worker function just contains the defaults for our specific supervisor:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
worker ::
  forall childProcess.
  HasPid childProcess =>
  String -> Effect (StartLinkResult childProcess) -> ErlChildSpec
worker id start =
  spec
    { id
    , childType: Worker
    , start
    , restartStrategy: RestartTransient
    , shutdownStrategy: ShutdownTimeout $ Milliseconds 5000.0
    }

So we can see in this code we simply return the flags for this tree and a list of children that need starting. Each of those children is just an (Effect childPid) and with this mechanism, it means that the arguments for each child are type checked (unlike in straight Erlang).