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

Handle updated process exit reasons #90

Draft
wants to merge 3 commits into
base: main
Choose a base branch
from
Draft
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
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
# Changelog

## Unreleased

- Fixed a bug where using `actor.Stop()` with other reasons than `Normal`
would stop the process with `Normal`.

## v0.14.1 - 2024-11-15

- Fixed a bug where the `significant` parameter would not be passed to the
Expand Down
30 changes: 15 additions & 15 deletions src/gleam/otp/actor.gleam
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ 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,
}
import gleam/option.{type Option, None, Some}
import gleam/otp/system.{
Expand Down Expand Up @@ -172,7 +172,11 @@ pub type Next(message, state) {

/// Stop handling messages and shut down.
///
Stop(ExitReason)
Stop

/// Stop handling messages with an abnormal reason.
///
StopAbnormal(reason: String)
}

pub fn continue(state: state) -> Next(message, state) {
Expand Down Expand Up @@ -255,12 +259,6 @@ pub type Spec(state, msg) {
)
}

// TODO: Check needed functionality here to be OTP compatible
fn exit_process(reason: ExitReason) -> ExitReason {
// TODO
reason
}

fn receive_message(self: Self(state, msg)) -> Message(msg) {
let selector = case self.mode {
// When suspended we only respond to system messages
Expand Down Expand Up @@ -315,7 +313,7 @@ fn process_status_info(self: Self(state, msg)) -> StatusInfo {
)
}

fn loop(self: Self(state, msg)) -> ExitReason {
fn loop(self: Self(state, msg)) -> Nil {
case receive_message(self) {
// An OTP system message. This is handled by the actor for the programmer,
// behind the scenes.
Expand Down Expand Up @@ -353,7 +351,10 @@ fn loop(self: Self(state, msg)) -> ExitReason {
// subject or some other messsage that the programmer's selector expects.
Message(msg) ->
case self.message_handler(msg, self.state) {
Stop(reason) -> exit_process(reason)
Stop -> Nil

StopAbnormal(reason) ->
process.send_abnormal_exit(process.self(), reason)

Continue(state: state, selector: new_selector) -> {
let selector =
Expand All @@ -374,7 +375,7 @@ fn log_warning(a: Charlist, b: List(Charlist)) -> Nil
fn initialise_actor(
spec: Spec(state, msg),
ack: Subject(Result(Subject(msg), ExitReason)),
) -> ExitReason {
) -> Nil {
// This is the main subject for the actor, the one that the actor.start
// functions return.
// Once the actor has been initialised this will be sent to the parent for
Expand Down Expand Up @@ -404,10 +405,9 @@ fn initialise_actor(
loop(self)
}

// The init failed. Exit with an error.
// The init failed. Send the reason back to the parent, but exit normally.
Failed(reason) -> {
process.send(ack, Error(Abnormal(reason)))
exit_process(Abnormal(reason))
process.send(ack, Error(Abnormal(dynamic.from(reason))))
}
}
}
Expand All @@ -421,7 +421,7 @@ fn init_selector(subject, selector) {
pub type StartError {
InitTimeout
InitFailed(ExitReason)
InitCrashed(Dynamic)
InitCrashed(ExitReason)
}

/// The result of starting a Gleam actor.
Expand Down
4 changes: 2 additions & 2 deletions src/gleam/otp/supervisor.gleam
Original file line number Diff line number Diff line change
Expand Up @@ -336,9 +336,9 @@ fn handle_exit(pid: Pid, state: State(a)) -> actor.Next(Message, State(a)) {
actor.continue(state)
}
Error(TooManyRestarts) ->
actor.Stop(process.Abnormal(
actor.StopAbnormal(
"Child processes restarted too many times within allowed period",
))
)
}
}

Expand Down
2 changes: 1 addition & 1 deletion test/gleam/otp/actor_documentation_example_test.gleam
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ fn handle_message(
case message {
// 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)
Shutdown -> actor.Stop

// 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
Expand Down
46 changes: 46 additions & 0 deletions test/gleam/otp/actor_test.gleam
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,52 @@ pub fn replace_selector_test() {
|> should.equal(dynamic.from("unknown message: String"))
}

pub fn abnormal_exit_can_be_trapped_test() {
process.trap_exits(True)
let exits =
process.new_selector()
|> process.selecting_trapped_exits(function.identity)

// Make an actor exit with an abnormal reason
let assert Ok(subject) =
actor.start(Nil, fn(_, _) { actor.StopAbnormal("reason") })
process.send(subject, Nil)

let trapped_reason = process.select(exits, 10)

// Stop trapping exits, as otherwise other tests fail
process.trap_exits(False)

trapped_reason
|> should.equal(
Ok(process.ExitMessage(
process.subject_owner(subject),
process.Abnormal(dynamic.from("reason")),
)),
)
}

pub fn killed_exit_can_be_trapped_test() {
process.trap_exits(True)
let exits =
process.new_selector()
|> process.selecting_trapped_exits(function.identity)

// Make an actor exit with a killed reason
let assert Ok(subject) = actor.start(Nil, fn(_, _) { actor.continue(Nil) })
process.kill(process.subject_owner(subject))

let trapped_reason = process.select(exits, 10)

// Stop trapping exits, as otherwise other tests fail
process.trap_exits(False)

trapped_reason
|> should.equal(
Ok(process.ExitMessage(process.subject_owner(subject), process.Killed)),
)
}

fn mapped_selector(mapper: fn(a) -> ActorMessage) {
let subject = process.new_subject()

Expand Down