From 5402bea50a9ee877f75feceb5b028b4a5a7a30c8 Mon Sep 17 00:00:00 2001 From: Abner Andino Date: Thu, 14 Nov 2024 19:04:16 +0000 Subject: [PATCH 1/4] fix documentation typo --- src/gleam/otp/actor.gleam | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/gleam/otp/actor.gleam b/src/gleam/otp/actor.gleam index 65f5c3e..d18fabe 100644 --- a/src/gleam/otp/actor.gleam +++ b/src/gleam/otp/actor.gleam @@ -1,7 +1,7 @@ //// This module provides the _Actor_ abstraction, one of the most common //// building blocks of Gleam OTP programs. //// -//// An Actor is a process like any other BEAM process and can be be used to hold +//// An Actor is a process like any other BEAM process and can be used to hold //// state, execute code, and communicate with other processes by sending and //// receiving messages. The advantage of using the actor abstraction over a bare //// process is that it provides a single interface for commonly needed From 1042332d1a73ea0b43e66d4141cbc097a2ccb700 Mon Sep 17 00:00:00 2001 From: Abner Andino Date: Thu, 14 Nov 2024 19:04:47 +0000 Subject: [PATCH 2/4] add significant child_spec to convert_child --- src/gleam/otp/static_supervisor.gleam | 1 + 1 file changed, 1 insertion(+) diff --git a/src/gleam/otp/static_supervisor.gleam b/src/gleam/otp/static_supervisor.gleam index bb9b138..27d4fe1 100644 --- a/src/gleam/otp/static_supervisor.gleam +++ b/src/gleam/otp/static_supervisor.gleam @@ -286,6 +286,7 @@ fn convert_child(child: ChildBuilder) -> Dict(Atom, Dynamic) { |> property("id", child.id) |> property("start", mfa) |> property("restart", child.restart) + |> property("significant", child.significant) |> property("type", type_) |> property("shutdown", shutdown) } From b972de7ad9abf269b0f7e3136a0b5ecad2d65ecf Mon Sep 17 00:00:00 2001 From: Abner Andino Date: Fri, 22 Nov 2024 00:28:49 +0000 Subject: [PATCH 3/4] handle exit reasons for exit_process --- src/gleam/otp/actor.gleam | 38 ++++++++++++++++++++++---------------- 1 file changed, 22 insertions(+), 16 deletions(-) diff --git a/src/gleam/otp/actor.gleam b/src/gleam/otp/actor.gleam index d18fabe..d990c95 100644 --- a/src/gleam/otp/actor.gleam +++ b/src/gleam/otp/actor.gleam @@ -1,6 +1,6 @@ //// This module provides the _Actor_ abstraction, one of the most common //// building blocks of Gleam OTP programs. -//// +//// //// An Actor is a process like any other BEAM process and can be used to hold //// state, execute code, and communicate with other processes by sending and //// receiving messages. The advantage of using the actor abstraction over a bare @@ -25,18 +25,18 @@ //// // Start the actor with initial state of an empty list, and the //// // `handle_message` callback function (defined below). //// // We assert that it starts successfully. -//// // +//// // //// // In real-world Gleam OTP programs we would likely write wrapper functions //// // called `start`, `push` `pop`, `shutdown` to start and interact with the -//// // Actor. We are not doing that here for the sake of showing how the Actor +//// // Actor. We are not doing that here for the sake of showing how the Actor //// // API works. //// let assert Ok(my_actor) = actor.start([], handle_message) -//// +//// //// // We can send a message to the actor to push elements onto the stack. //// process.send(my_actor, Push("Joe")) //// process.send(my_actor, Push("Mike")) //// process.send(my_actor, Push("Robert")) -//// +//// //// // The `Push` message expects no response, these messages are sent purely for //// // the side effect of mutating the state held by the actor. //// // @@ -50,10 +50,10 @@ //// let assert Ok("Robert") = process.call(my_actor, Pop, 10) //// let assert Ok("Mike") = process.call(my_actor, Pop, 10) //// let assert Ok("Joe") = process.call(my_actor, Pop, 10) -//// +//// //// // The stack is now empty, so if we pop again the actor replies with an error. //// let assert Error(Nil) = process.call(my_actor, Pop, 10) -//// +//// //// // Lastly, we can send a message to the actor asking it to shut down. //// process.send(my_actor, Shutdown) //// } @@ -74,18 +74,18 @@ //// // The `Shutdown` message is used to tell the actor to stop. //// // It is the simplest message type, it contains no data. //// Shutdown -//// +//// //// // The `Push` message is used to add a new element to the stack. //// // It contains the item to add, the type of which is the `element` //// // parameterised type. //// Push(push: element) -//// +//// //// // The `Pop` message is used to remove an element from the stack. //// // It contains a `Subject`, which is used to send the response back to the //// // message sender. In this case the reply is of type `Result(element, Nil)`. //// Pop(reply_with: Subject(Result(element, Nil))) //// } -//// +//// //// // The last part is to implement the `handle_message` callback function. //// // //// // This function is called by the Actor for each message it receives. @@ -102,7 +102,7 @@ //// // For the `Shutdown` message we return the `actor.Stop` value, which causes //// // the actor to discard any remaining messages and stop. //// Shutdown -> actor.Stop(process.Normal) -//// +//// //// // For the `Push` message we add the new element to the stack and return //// // `actor.continue` with this new stack, causing the actor to process any //// // queued messages or wait for more. @@ -110,7 +110,7 @@ //// let new_state = [value, ..stack] //// actor.continue(new_state) //// } -//// +//// //// // For the `Pop` message we attempt to remove an element from the stack, //// // sending it or an error back to the caller, before continuing. //// Pop(client) -> @@ -121,7 +121,7 @@ //// process.send(client, Error(Nil)) //// actor.continue([]) //// } -//// +//// //// [first, ..rest] -> { //// // Otherwise we send the first element back and use the remaining //// // elements as the new state. @@ -139,7 +139,8 @@ import gleam/dynamic.{type Dynamic} import gleam/erlang/atom import gleam/erlang/charlist.{type Charlist} import gleam/erlang/process.{ - type ExitReason, type Pid, type Selector, type Subject, Abnormal, + type ExitReason, type Pid, type Selector, type Subject, Abnormal, Killed, + Normal, } import gleam/option.{type Option, None, Some} import gleam/otp/system.{ @@ -255,9 +256,14 @@ pub type Spec(state, msg) { ) } -// TODO: Check needed functionality here to be OTP compatible fn exit_process(reason: ExitReason) -> ExitReason { - // TODO + let self = process.self() + case reason { + Normal -> process.send_exit(self) + Killed -> process.kill(self) + Abnormal(abnormality) -> process.send_abnormal_exit(self, abnormality) + } + reason } From 2a907cb405fc5ef33eae22550f7e2e15b1ab524a Mon Sep 17 00:00:00 2001 From: Abner Andino Date: Fri, 22 Nov 2024 00:29:54 +0000 Subject: [PATCH 4/4] add exit_process tests and fix broken test failed_init_test --- test/gleam/otp/actor_test.gleam | 196 ++++++++++++++++++++++++++++++-- 1 file changed, 189 insertions(+), 7 deletions(-) diff --git a/test/gleam/otp/actor_test.gleam b/test/gleam/otp/actor_test.gleam index a007254..24d9381 100644 --- a/test/gleam/otp/actor_test.gleam +++ b/test/gleam/otp/actor_test.gleam @@ -32,14 +32,47 @@ pub fn get_status_test() { } pub fn failed_init_test() { - actor.Spec( - init: fn() { actor.Failed("not enough wiggles") }, - loop: fn(_msg, state) { actor.continue(state) }, - init_timeout: 10, - ) - |> actor.start_spec - |> result.is_error + let server = + process.start(linked: False, running: fn() { + actor.Spec( + init: fn() { actor.Failed("not enough wiggles") }, + loop: fn(_msg, state) { actor.continue(state) }, + init_timeout: 10, + ) + |> actor.start_spec + |> result.is_error + |> should.be_true + }) + + // Check that our server is alive + process.is_alive(server) |> should.be_true + + // Create a monitor to check if our monitor is down + let monitor = process.monitor_process(server) + let selector = + process.new_selector() + |> process.selecting_process_down(monitor, Mon) + + let result = case process.select(selector, 500) { + // Child got shutdown + Ok(Mon(_down)) -> Error(dynamic.from("init_failed")) + + // Child did not finish initialising in time + _ -> { + Error(dynamic.from("timeout")) + } + } + + result + |> should.equal(Error(dynamic.from("init_failed"))) + + // Check that the server is not longer running + process.is_alive(server) + |> should.be_false + + // teardown + process.demonitor_process(monitor) } pub fn suspend_resume_test() { @@ -205,6 +238,114 @@ pub fn replace_selector_test() { |> should.equal(dynamic.from("unknown message: String")) } +type ActorExitMessage { + Shutdown(reason: process.ExitReason) + GetPid(reply: Subject(Result(Pid, Nil))) +} + +pub fn exit_actor_normally_test() { + // We don't need to create a server and monitor it. An exit with reason + // "normal" won't crash our whole test suite. + // + // More information: https://www.erlang.org/doc/apps/erts/erlang#exit/2 + let assert Ok(actor_exits_normal) = + actor.start(Nil, fn(message: ActorExitMessage, state) { + case message { + Shutdown(reason) -> { + actor.Stop(reason) + } + GetPid(client) -> { + let self = process.self() + process.send(client, Ok(self)) + actor.continue(state) + } + } + }) + + let assert Ok(actor_pid) = process.call(actor_exits_normal, GetPid, 10) + + // Check that the actor is still alive + process.is_alive(actor_pid) + |> should.be_true + + let assert Ok(actor_pid) = process.call(actor_exits_normal, GetPid, 10) + process.send(actor_exits_normal, Shutdown(process.Normal)) + + // Check that the actor has exited after the Shutdown message + process.is_alive(actor_pid) + |> should.be_false +} + +pub fn exit_actor_abnormal_test() { + let exit_type = process.Abnormal("por que maria?") + let server = init_server_exit(exit_type) + + // check that server is still alive + process.is_alive(server) + |> should.be_true + + let monitor = process.monitor_process(server) + let selector = + process.new_selector() + |> process.selecting_process_down(monitor, Mon) + + let result = case process.select(selector, 500) { + // Child got shutdown + Ok(Mon(down)) -> Error(down.reason) + + // Child did not finish initialising in time + _ -> { + Error(dynamic.from("timeout")) + } + } + + result + |> should.equal(Error(dynamic.from(exit_type))) + + // server shouldn't be alive because our Actor got an exit signal + // with a Abnormal(reason). + process.is_alive(server) + |> should.be_false + + // teardown + process.demonitor_process(monitor) +} + +pub fn exit_actor_kill_test() { + let exit_type = process.Killed + let server = init_server_exit(exit_type) + + // check that server is still alive + process.is_alive(server) + |> should.be_true + + let monitor = process.monitor_process(server) + let selector = + process.new_selector() + |> process.selecting_process_down(monitor, Mon) + + let result = case process.select(selector, 500) { + // Child got shutdown + Ok(Mon(down)) -> Error(down.reason) + + // Child did not finish initialising in time + _ -> { + Error(dynamic.from("timeout")) + } + } + + result + |> should.equal(Error(dynamic.from(exit_type))) + + // server shouldn't be alive because our actor got an exit signal + // with a Killed reason (brute force killing a child process). + process.is_alive(server) + |> should.be_false + + // teardown + process.demonitor_process(monitor) +} + fn mapped_selector(mapper: fn(a) -> ActorMessage) { let subject = process.new_subject() @@ -228,6 +369,47 @@ fn get_actor_state(subject: Subject(a)) { |> system.get_state } +type Mon { + Mon(process.ProcessDown) +} + +fn init_server_exit(shutdown: process.ExitReason) -> process.Pid { + // Actors use the option linked which means that if actor throws an exception + // it will stop the execution of the link process. That's why we need to set + // a "server" to run our Actor. The moment we send the Shutdown message to the + // Actor, this will throw an exception and stop the execution of our "server" + // without stoping the `gleam test` command. + + process.start(linked: False, running: fn() { + let assert Ok(actor_subject) = + actor.start(Nil, fn(message: ActorExitMessage, state) { + case message { + Shutdown(reason) -> { + actor.Stop(reason) + } + GetPid(client) -> { + let self = process.self() + process.send(client, Ok(self)) + actor.continue(state) + } + } + }) + + let assert Ok(actor_pid) = process.call(actor_subject, GetPid, 10) + + // First we check that the actor is still alive + process.is_alive(actor_pid) + |> should.be_true + + // the actor has a link to our server which should make it crash + // but it shouldn't be affecting our main test runner + process.send(actor_subject, Shutdown(shutdown)) + + // give some time to the actor to process the message + process.sleep(50) + }) +} + @external(erlang, "erlang", "send") fn raw_send(a: Pid, b: anything) -> anything