Current section

Files

Jump to
gleam_otp src gleam otp factory_supervisor.gleam
Raw

src/gleam/otp/factory_supervisor.gleam

//// A supervisor where child processes are started dynamically from a
//// pre-specified template, so new processes can be created as needed
//// while the program is running.
////
//// When the supervisor is shut down it shuts down all its children
//// concurrently and in no specified order.
////
//// For further detail see the Erlang documentation, particularly the parts
//// about the `simple_one_for_one` restart strategy, which is the Erlang
//// equivilent of the factory supervisor.
//// <https://www.erlang.org/doc/apps/stdlib/supervisor.html>.
////
//// ## Usage
////
//// Add the factory supervisor to your supervision tree using the `supervised`
//// function and a name created at the start of the program. The `new`
//// function takes a "template function", which is a function that takes one
//// argument and starts a linked child process.
////
//// You most likely want to give the factory supervisor a name, and to pass
//// that name to any other processes that will want to cause new child
//// processes to be started under the factory supervisor. In this example a
//// web server is used.
////
//// ```gleam
//// import gleam/erlang/process.{type Name}
//// import gleam/otp/actor.{type StartResult}
//// import gleam/otp/factory_supervisor as factory
//// import gleam/otp/static_supervisor as supervisor
//// import my_app
////
//// /// This function starts the application's supervision tree.
//// ///
//// /// It takes a record as an argument that
//// ///
//// pub fn start_supervision_tree(reporters_name: Name(_)) -> StartResult(_) {
//// // Define a named factory supervisor that can create new child processes
//// // using the `my_app.start_reporter_actor` function, which is defined
//// // elsewhere in the program.
//// let reporter_factory_supervisor =
//// factory.worker_child(my_app.start_reporter_actor)
//// |> factory.named(reporters_name)
//// |> factory.supervised
////
//// // This web server process takes the name, so it can contact the factory
//// // supervisor to command it to start new processes as needed.
//// let web_server = my_app.supervised_web_server(reporters_name)
////
//// // Create the top-level static supervisor with the supervisor and web
//// // server as its children
//// supervisor.new(supervisor.RestForOne)
//// |> supervisor.add(reporter_factory_supervisor)
//// |> supervisor.add(web_server)
//// |> supervisor.start
//// }
//// ```
////
//// Any process with the name of the factory supervisor can use the
//// `get_by_name` function to get a reference to the supervisor, and then use
//// the `start_child` function to have it start new child processes.
////
//// Remember! Each process name created with `process.new_name` is unique.
//// Two names created by calling the function twice are different names, even
//// if the same string is given as an argument. You must create the name value
//// at the start of your program and then pass it down into application code
//// and library code that uses names.
////
//// ```gleam
//// import gleam/http/request.{type Request}
//// import gleam/http/response.{type Response}
//// import gleam/otp/factory_supervisor
//// import my_app
////
//// /// In our example this function is called each time a HTTP request is
//// /// received by the web server.
//// pub fn handle_request(req: Request(_), reporters: Name(_)) -> Response(_) {
//// // Get a reference to the supervisor using the name
//// let supervisor = factory_supervisor.get_by_name(reporters)
////
//// // Start a new child process under the supervisor, passing the request path
//// // to use as the argument for the child-starting template function.
//// let start_result = factory_supervisor.start_child(supervisor, request.path)
////
//// // A response is sent to the HTTP client.
//// // The child starting template function returns a result, with the error case
//// // being used when children fail to start. Because of this the `start_child`
//// // function also returns a result, so it must be handled too.
//// case start_result {
//// Ok(_) -> response.new(200)
//// Error(_) -> response.new(500)
//// }
//// }
//// ```
import gleam/dynamic.{type Dynamic}
import gleam/erlang/atom.{type Atom}
import gleam/erlang/process.{type Pid}
import gleam/option
import gleam/otp/actor
import gleam/otp/internal/result2.{type Result2}
import gleam/otp/supervision.{type ChildSpecification}
const default_intensity = 2
const default_period = 5
const default_restart_strategy = supervision.Transient
/// A reference to the running supervisor
///
/// This supervisor wrap Erlang/OTP's `supervisor` module, and as such it does
/// not use subjects for message sending. If it was implemented in Gleam a
/// subject might be used instead of this type.
///
pub opaque type Supervisor(child_argument, child_data) {
Supervisor(pid: Pid)
NamedSupervisor(name: process.Name(Message(child_argument, child_data)))
}
/// The message type of a factory supervisor. This message type is not used
/// directly, but if you are using a name with a factory supervisor then this
/// will be the message type of the name.
pub type Message(child_argument, child_data)
/// Get a reference to a supervisor using its registered name.
///
/// If no supervisor has been started using this name then functions
/// using this reference will fail.
///
/// # Panics
///
/// Functions using the `Supervisor` reference returned by this function
/// will panic if there is no factory supervisor registered with the name
/// when they are called. Always make sure your supervisors are themselves
/// supervised.
///
pub fn get_by_name(
name: process.Name(Message(child_argument, child_data)),
) -> Supervisor(child_argument, child_data) {
NamedSupervisor(name)
}
/// A builder for configuring and starting a supervisor. See each of the
/// functions that take this type for details of the configuration possible.
///
pub opaque type Builder(child_argument, child_data) {
Builder(
child_type: supervision.ChildType,
template: fn(child_argument) -> actor.StartResult(child_data),
restart_strategy: supervision.Restart,
intensity: Int,
period: Int,
name: option.Option(process.Name(Message(child_argument, child_data))),
)
}
/// Configure a supervisor with a child-starting template function.
///
/// You should use this unless the child processes are also supervisors.
///
/// The default shutdown timeout is 5000ms. This can be changed with the
/// `timeout` function.
///
pub fn worker_child(
template: fn(child_argument) -> actor.StartResult(child_data),
) -> Builder(child_argument, child_data) {
Builder(
template:,
child_type: supervision.Worker(5000),
restart_strategy: default_restart_strategy,
intensity: default_intensity,
period: default_period,
name: option.None,
)
}
/// Configure a supervisor with a template that will start children that are
/// also supervisors.
///
/// You should only use this if the child processes are also supervisors.
///
/// Supervisor children have an unlimited amount of time to shutdown, there is
/// no timeout.
///
pub fn supervisor_child(
template: fn(child_argument) -> actor.StartResult(child_data),
) -> Builder(child_argument, child_data) {
Builder(
template:,
child_type: supervision.Supervisor,
restart_strategy: default_restart_strategy,
intensity: default_intensity,
period: default_period,
name: option.None,
)
}
/// Provide a name for the supervisor to be registered with when started,
/// enabling it be more easily contacted by other processes. This is useful for
/// enabling processes that can take over from an older one that has exited due
/// to a failure.
///
/// If the name is already registered to another process then the factory
/// supervisor will fail to start.
///
pub fn named(
builder: Builder(child_argument, child_data),
name: process.Name(Message(child_argument, child_data)),
) -> Builder(child_argument, child_data) {
Builder(..builder, name: option.Some(name))
}
/// To prevent a supervisor from getting into an infinite loop of child
/// process terminations and restarts, a maximum restart tolerance is
/// defined using two integer values specified with keys intensity and
/// period in the above map. Assuming the values MaxR for intensity and MaxT
/// for period, then, if more than MaxR restarts occur within MaxT seconds,
/// the supervisor terminates all child processes and then itself. The
/// termination reason for the supervisor itself in that case will be
/// shutdown.
///
/// Intensity defaults to 2 and period defaults to 5.
///
pub fn restart_tolerance(
builder: Builder(child_argument, child_data),
intensity intensity: Int,
period period: Int,
) -> Builder(child_argument, child_data) {
Builder(..builder, intensity: intensity, period: period)
}
/// Configure the amount of milliseconds a child has to shut down before
/// being brutal killed by the supervisor.
///
/// If not set the default for a child is 5000ms.
///
/// This will be ignored if the child is a supervisor itself.
///
pub fn timeout(
builder: Builder(argument, data),
ms ms: Int,
) -> Builder(argument, data) {
case builder.child_type {
supervision.Worker(_) ->
Builder(..builder, child_type: supervision.Worker(ms))
_ -> builder
}
}
/// Configure the strategy for restarting children when they exit. See the
/// documentation for the `supervision.Restart` for details.
///
/// If not set the default strategy is `supervision.Transient`, so children
/// will be restarted if they terminate abnormally.
///
pub fn restart_strategy(
builder: Builder(argument, data),
restart_strategy: supervision.Restart,
) -> Builder(argument, data) {
case builder.child_type {
supervision.Worker(_) -> Builder(..builder, restart_strategy:)
_ -> builder
}
}
/// Start a new supervisor process with the configuration and child template
/// specified within the builder.
///
/// Typically you would use the `supervised` function to add your supervisor to
/// a supervision tree instead of using this function directly.
///
/// The supervisor will be linked to the parent process that calls this
/// function.
///
pub fn start(
builder: Builder(child_argument, child_data),
) -> actor.StartResult(Supervisor(child_argument, child_data)) {
let flags =
make_erlang_start_flags([
Strategy(SimpleOneForOne),
Intensity(builder.intensity),
Period(builder.period),
])
let module_atom = atom.create("gleam@otp@factory_supervisor")
let function_atom = atom.create("start_child_callback")
let mfa = #(module_atom, function_atom, [builder.template])
let #(type_, shutdown) = case builder.child_type {
supervision.Supervisor -> #(atom.create("supervisor"), make_timeout(-1))
supervision.Worker(ms) -> #(atom.create("worker"), make_timeout(ms))
}
let child =
make_erlang_child_spec([
Id(0),
Start(mfa),
Restart(builder.restart_strategy),
Type(type_),
Shutdown(shutdown),
])
let configuration = #(flags, [child])
let start_result = case builder.name {
option.None -> unnamed_start(module_atom, configuration)
option.Some(name) -> named_start(Local(name), module_atom, configuration)
}
case start_result {
Ok(pid) -> Ok(actor.Started(pid:, data: Supervisor(pid)))
Error(error) -> Error(convert_erlang_start_error(error))
}
}
@external(erlang, "maps", "from_list")
fn make_erlang_start_flags(
flags: List(ErlangStartFlag(data)),
) -> ErlangStartFlags
type ErlangStartFlags
@external(erlang, "gleam_otp_external", "convert_erlang_start_error")
fn convert_erlang_start_error(dynamic: Dynamic) -> actor.StartError
@external(erlang, "supervisor", "start_link")
fn unnamed_start(
module: Atom,
args: #(ErlangStartFlags, List(ErlangChildSpec)),
) -> Result(Pid, Dynamic)
@external(erlang, "supervisor", "start_link")
fn named_start(
name: ErlangSupervisorName(child_argument, child_data),
module: Atom,
args: #(ErlangStartFlags, List(ErlangChildSpec)),
) -> Result(Pid, Dynamic)
type ErlangSupervisorName(child_argument, child_data) {
Local(process.Name(Message(child_argument, child_data)))
}
type Strategy {
SimpleOneForOne
}
type ErlangStartFlag(data) {
Strategy(Strategy)
Intensity(Int)
Period(Int)
}
type ErlangChildSpec
@external(erlang, "maps", "from_list")
fn make_erlang_child_spec(
properties: List(ErlangChildSpecProperty(argument, data)),
) -> ErlangChildSpec
type ErlangChildSpecProperty(argument, data) {
Id(Int)
Start(
#(
Atom,
Atom,
List(fn(argument) -> Result(actor.Started(data), actor.StartError)),
),
)
Restart(supervision.Restart)
Type(Atom)
Shutdown(Timeout)
}
type Timeout
/// Negative numbers mean an infinite timeout
@external(erlang, "gleam_otp_external", "make_timeout")
fn make_timeout(amount: Int) -> Timeout
/// Create a `ChildSpecification` that adds this supervisor as the child of
/// another, making it fault tolerant and part of the application's supervision
/// tree. You should prefer to starting unsupervised supervisors with the
/// `start` function.
///
/// If any child fails to start the supevisor first terminates all already
/// started child processes with reason shutdown and then terminate itself and
/// returns an error.
///
pub fn supervised(
builder: Builder(child_argument, child_data),
) -> ChildSpecification(Supervisor(child_argument, child_data)) {
supervision.supervisor(fn() { start(builder) })
}
/// Start a new child using the supervisor's child template and the given
/// argument. The start result of the child is returned.
///
pub fn start_child(
supervisor: Supervisor(child_argument, child_data),
argument: child_argument,
) -> actor.StartResult(child_data) {
let start = case supervisor {
NamedSupervisor(name:) -> erlang_start_child_name(name, _)
Supervisor(pid:) -> erlang_start_child_pid(pid, _)
}
case start([argument]) {
result2.Ok(pid, data) -> Ok(actor.Started(pid, data))
result2.Error(reason) -> Error(reason)
}
}
@external(erlang, "supervisor", "start_child")
fn erlang_start_child_name(
supervisor: process.Name(Message(child_argument, child_data)),
argument: List(child_argument),
) -> Result2(Pid, data, actor.StartError)
@external(erlang, "supervisor", "start_child")
fn erlang_start_child_pid(
supervisor: Pid,
argument: List(child_argument),
) -> Result2(Pid, data, actor.StartError)
// Callback used by the Erlang supervisor module.
@internal
pub fn init(start_data: Dynamic) -> Result(Dynamic, never) {
Ok(start_data)
}
// Callback used by the Erlang supervisor module.
@internal
pub fn start_child_callback(
start: fn(argument) -> Result(actor.Started(data), actor.StartError),
argument: argument,
) -> Result2(Pid, data, actor.StartError) {
case start(argument) {
Ok(started) -> result2.Ok(started.pid, started.data)
Error(error) -> result2.Error(error)
}
}