From 3e82f69d14594e77278b28004b5e4b32bd499c51 Mon Sep 17 00:00:00 2001 From: Louis Pilfold Date: Sun, 21 Sep 2025 17:17:59 +0100 Subject: [PATCH 1/3] Application module --- src/gleam/otp/application.gleam | 81 +++++++++++++++++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 src/gleam/otp/application.gleam diff --git a/src/gleam/otp/application.gleam b/src/gleam/otp/application.gleam new file mode 100644 index 0000000..4b5ec60 --- /dev/null +++ b/src/gleam/otp/application.gleam @@ -0,0 +1,81 @@ +import gleam +import gleam/erlang/atom.{type Atom} +import gleam/erlang/node +import gleam/erlang/process +import gleam/otp/actor + +// TODO: document +pub opaque type Application(state) { + Application( + start: fn(StartType) -> actor.StartResult(state), + before_stop: fn(state) -> state, + after_stop: fn(state) -> Nil, + ) +} + +// TODO: document +pub type StartType { + Normal + Takeover(node.Node) + Failover(node.Node) +} + +// +// OTP application callbacks +// + +/// +/// +/// ```erlang +/// -callback start(StartType :: start_type(), StartArgs :: term()) -> +/// {ok, pid()} | {ok, pid(), State :: term()} | {error, Reason :: term()}. +/// ``` +/// +@internal +pub fn start( + start_type: StartType, + application_module: Atom, +) -> ErlangResult2(process.Pid, state, actor.StartError) { + let application: Application(state) = + apply(application_module, atom.create("main"), []) + case application.start(start_type) { + gleam.Ok(started) -> Ok(started.pid, started.data) + gleam.Error(error) -> Error(error) + } +} + +/// +/// +/// ```erlang +/// -callback prep_stop(State) -> NewState when State :: term(), NewState :: term(). +/// ``` +/// +@internal +pub fn pre_stop( + state: #(Application(state), state), +) -> #(Application(state), state) { + let #(application, state) = state + let state = application.before_stop(state) + #(application, state) +} + +/// +/// +/// ```erlang +/// -callback stop(State :: term()) -> term(). +/// ``` +/// +@internal +pub fn stop(state: #(Application(state), state)) -> Nil { + let #(application, state) = state + application.after_stop(state) +} + +@internal +pub type ErlangResult2(data1, data2, error) { + Ok(data1, data2) + Error(error) +} + +@external(erlang, "erlang", "apply") +fn apply(module: Atom, function: Atom, arguments: List(argument)) -> returned From 84ea982b9fb5edde93a74eb28b83169d18438fa7 Mon Sep 17 00:00:00 2001 From: Louis Pilfold Date: Sun, 21 Sep 2025 19:10:13 +0100 Subject: [PATCH 2/3] Documentation --- src/gleam/otp/application.gleam | 93 ++++++++++++++++++++++++++- test/gleam/otp/application_test.gleam | 0 2 files changed, 91 insertions(+), 2 deletions(-) create mode 100644 test/gleam/otp/application_test.gleam diff --git a/src/gleam/otp/application.gleam b/src/gleam/otp/application.gleam index 4b5ec60..9692493 100644 --- a/src/gleam/otp/application.gleam +++ b/src/gleam/otp/application.gleam @@ -1,10 +1,38 @@ +//// In the context of the OTP framework an "application" is a collection of +//// code that can be loaded into the virtual machine. Each Gleam package is a +//// single OTP application. +//// +//// One feature of OTP applications that makes them different from packages or +//// libraries in other languages is that they have the option of defining a +//// module through which they can be _started_ and _stopped_, and they can +//// configured using Erlang's configuration system. +//// +//// ## OTP application programs +//// +//// Long running Gleam programs (such as backend web applications) typically +//// want to define an application module, and to use it as the entrypoint for +//// the program in favour of the `main` function. +//// +//// ## OTP application libraries +//// +//// It is always preferred for libraries to not be stateful! Instead they +//// should expose functions for Gleam apps to call, passing configuration as +//// arguments. There may be some libraries for which it makes sense to have +//// this implicit global mutable state, but they are very rare. +//// + +// TODO: give example of how to use it + import gleam import gleam/erlang/atom.{type Atom} import gleam/erlang/node import gleam/erlang/process import gleam/otp/actor -// TODO: document +/// A recipe of how to start the stateful OTP application. +/// +/// See the module documentation for how to use this type in your program. +/// pub opaque type Application(state) { Application( start: fn(StartType) -> actor.StartResult(state), @@ -13,10 +41,71 @@ pub opaque type Application(state) { ) } -// TODO: document +// TODO: test +/// Create a new application recipe from a starter function. This function is +/// called whenever an application is started, and it starts the supervision tree +/// the OTP application. +/// +/// The `actor.StartResult` data returned from the starter function is used as the +/// state of the application and will be passed to the `before_stop` and +/// `after_stop` callbacks when the application is stopped. +/// +pub fn new( + start: fn(StartType) -> actor.StartResult(state), +) -> Application(state) { + Application( + before_stop: fn(state) { state }, + after_stop: fn(_state) { Nil }, + start:, + ) +} + +// TODO: test +/// Configure the application with a callback function to be run before the +/// application is stopped. This callback function can modify the application's +/// state value, which will then be passed to the `after_stop` callback. +/// +/// This is a best-effort API! There is no guarentee that this function will be +/// called before an application stops, for example, it likely may not be +/// called if the VM crashes. +/// +pub fn before_stop( + application: Application(state), + before_stop: fn(state) -> state, +) -> Application(state) { + Application(..application, before_stop:) +} + +// TODO: test +/// Configure the application with a callback function to be run after the +/// application is stopped. +/// +/// This is a best-effort API! There is no guarentee that this function will be +/// called after an application stops, for example, it likely may not be +/// called if the VM crashes. +/// +pub fn after_stop( + application: Application(state), + after_stop: fn(state) -> Nil, +) -> Application(state) { + Application(..application, after_stop:) +} + +/// A value of this type is passed as an argument to a stateful OTP +/// application's when it starts, to indicate the context in which the +/// application been started. +/// pub type StartType { + /// The application is starting normally. Normal + /// The application is distributed and started at the current node because of + /// a takeover from the other node. Takeover(node.Node) + /// The application is distributed and started at the current node because of + /// a failover from the other node, and the application is configured with + /// "start phases". See the Erlang/OTP application documentation for more + /// information. + /// Failover(node.Node) } diff --git a/test/gleam/otp/application_test.gleam b/test/gleam/otp/application_test.gleam new file mode 100644 index 0000000..e69de29 From 84d0a5aa7da0a554227b925b80929df756f031d0 Mon Sep 17 00:00:00 2001 From: Louis Pilfold Date: Wed, 24 Sep 2025 16:21:46 +0100 Subject: [PATCH 3/3] Feedback from PR --- src/gleam/otp/application.gleam | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/gleam/otp/application.gleam b/src/gleam/otp/application.gleam index 9692493..0f28634 100644 --- a/src/gleam/otp/application.gleam +++ b/src/gleam/otp/application.gleam @@ -4,7 +4,7 @@ //// //// One feature of OTP applications that makes them different from packages or //// libraries in other languages is that they have the option of defining a -//// module through which they can be _started_ and _stopped_, and they can +//// module through which they can be _started_ and _stopped_, and they can be //// configured using Erlang's configuration system. //// //// ## OTP application programs @@ -20,6 +20,9 @@ //// arguments. There may be some libraries for which it makes sense to have //// this implicit global mutable state, but they are very rare. //// +//// ## Usage +//// +//// // TODO: give example of how to use it @@ -44,7 +47,7 @@ pub opaque type Application(state) { // TODO: test /// Create a new application recipe from a starter function. This function is /// called whenever an application is started, and it starts the supervision tree -/// the OTP application. +/// of the OTP application. /// /// The `actor.StartResult` data returned from the starter function is used as the /// state of the application and will be passed to the `before_stop` and @@ -140,7 +143,7 @@ pub fn start( /// ``` /// @internal -pub fn pre_stop( +pub fn prep_stop( state: #(Application(state), state), ) -> #(Application(state), state) { let #(application, state) = state