Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implementation of the exit_process for Actor #86

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 22 additions & 16 deletions src/gleam/otp/actor.gleam
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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.
//// //
Expand All @@ -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)
//// }
Expand All @@ -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.
Expand All @@ -102,15 +102,15 @@
//// // 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.
//// Push(value) -> {
//// 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) ->
Expand All @@ -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.
Expand All @@ -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.{
Expand Down Expand Up @@ -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
}

Expand Down
196 changes: 189 additions & 7 deletions test/gleam/otp/actor_test.gleam
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down Expand Up @@ -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()

Expand All @@ -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

Expand Down
Loading