Skip to content

Commit

Permalink
supervisor updates
Browse files Browse the repository at this point in the history
  • Loading branch information
bcpeinhardt committed Feb 28, 2024
1 parent ca0e84e commit 7e2aa87
Show file tree
Hide file tree
Showing 3 changed files with 34 additions and 32 deletions.
19 changes: 15 additions & 4 deletions src/actors/pantry.gleam
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import gleam/set.{type Set}
// We use this to send messages to the actor. We could abstract that away further,
// so the user doesn't have to manage the subject themselves, but following this
// pattern will help us integrate with something called a `supervisor` later on, and it
// matches how working with normal data in Gleam usually works too `operation(subject, arg1, arg2...)`.
// matches how working with normal data in Gleam usually works too `module.operation(subject, arg1, arg2...)`.
// - Functions that need to get a message back from the actor use `actor.call` to send a message
// and wait for a reply. (this is just a re-export off `process.call`). It is a synchronous operation,
// and it will block the calling process.
Expand All @@ -38,14 +38,17 @@ pub fn take_item(pantry: Subject(Message), item: String) -> Result(String, Nil)
// See that `_`? That's a placeholder for the reply subject. It will be injected for us by `call`.
//
// If the underscore syntax is confusing, it's called a [function capture](https://tour.gleam.run/functions/function-captures/).
// It's a shorthand for `fn(reply_with) { TakeItem(reply_with, item) }`.
// It's a shorthand for `fn(reply_with) { TakeItem(reply_with, item) }` where `reply_with` is a subject owned by
// the calling process. Two way message passing requires two subjects, one for each process.
//
// Also, since we need to wait for a response, we pass a timeout as the last argument so we don't get stuck
// waiting forever if our actor gets struck by lightning or something.
// waiting forever if our actor process gets struck by lightning or something.
actor.call(pantry, TakeItem(_, item), timeout)
}

/// Close the pantry.
/// Shutdown functions like this are often written for manual usage and testing purposes.
/// In a real application, you'd probably want to use a `supervisor` to manage the lifecycle of your actors.
pub fn close(pantry: Subject(Message)) -> Nil {
actor.send(pantry, Shutdown)
}
Expand All @@ -66,7 +69,7 @@ pub type Message {
}

// This is our actor's message handler. It's a function that takes a message and the current state of the actor,
// and returns a new state and a new state for the actor to continue with.
// and returns a new state for the actor to continue with.
//
// There's nothing really magic going on under the hood here. An actor is really just a recursive function that
// holds state in its arguments, receives a message, possibly does some work or send messages back to other processes,
Expand Down Expand Up @@ -108,3 +111,11 @@ fn handle_message(
}
}
}
// That's it! We've implemented a simple pantry actor.
// Note: This example is meant to be straightforward. In a really system,
// you probably don't want an actor like this, whose role is to manage a small
// piece of mutable state.
// Utilizing process and actors to bootstrap OOP patterns based on mutable state
// is, well, a bad idea. Remember, all things in moderation. There are times when
// a simple server to hold some mutable state is exactly what you need. But in a
// functional language like Gleam, it shouldn't be your first choice.
16 changes: 1 addition & 15 deletions src/concurrency_primitives.gleam
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ pub fn main() {
// constructs.
//
// If you want something like a server, a long running process which will
// receive and respond you messages, the "actor" abstraction is the way to
// receive and respond to messages, the "actor" abstraction is the way to
// go. (An "actor" is Gleam's equivalent of Erlang/Elixir's `gen_server`.
// It has a different name because it has a different API due to static typing,
// but it's the same concept.)
Expand All @@ -77,18 +77,4 @@ pub fn main() {
// synchronous code to run concurrently and only block once you need
// the results, you'll want the `Task` module. It's great for the dead simple
// "do this somewhere else and I'll let you know when I need it" case.

// A WORD OF WARNING: OTP's abstractions are really powerful, and have been
// tailored over decades so that they fit the mental model of a lot of
// problems really well. It can be tempting to want to use them as organizational
// constructs in the code base, especially coming from an OOP background, as processes can
// hold and update state, but you should not do this.
// Always ask yourself: Do I REALLY need concurrency here? REALLY REALLY?
// If you find yourself reaching for processes/tasks/actors as a way to hold state,
// rather than because you need concurrency for performance/scalability/fault tolerance reasons,
// you are not the first and you won't be the last.

// Below are some resources for learning functional design patterns to help reduce dependency
// on stateful constructs:
// Todo: find and vet said resources
}
31 changes: 18 additions & 13 deletions src/supervisors.gleam
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,9 @@
////

import gleam/io
import gleam/iterator
import gleam/otp/supervisor
import gleam/otp/actor
import gleam/erlang/process
import supervisors/a_shit_actor.{type Message} as duckduckgoose
import supervisors/a_shit_actor as duckduckgoose

pub fn main() {
let parent_subject = process.new_subject()
Expand All @@ -17,18 +15,25 @@ pub fn main() {
|> supervisor.add(game)
}

// We start the supervisor
let assert Ok(_supervisor_subject) = supervisor.start(children)

// The actor's init function sent us a subject for us to be able
// to send it messages
let assert Ok(game_subject) = process.receive(parent_subject, 1000)

// Good messages, nothing crashes
io.debug(duckduckgoose.duck(game_subject))
io.debug(duckduckgoose.duck(game_subject))
io.debug(duckduckgoose.duck(game_subject))
// // Oh shit, that aint good, our actors gonna crash
// io.debug(duckduckgoose.goose(game_subject))

// // No worries, supervisor turned things back on for us
// process.sleep(10_000)
// io.debug(duckduckgoose.duck(game_subject))
// io.debug(duckduckgoose.duck(game_subject))
let assert Ok("duck") = duckduckgoose.duck(game_subject)

// Oh shit, that ain't good, our actor is gonna crash
let assert Error(_) = duckduckgoose.goose(game_subject)

// Don't worry, the supervisor restarted our actor, and the actor's
// init function sent us back a subject owned by the new process.
let assert Ok(new_game_subject) = process.receive(parent_subject, 1000)
let assert Ok("duck") = duckduckgoose.duck(new_game_subject)

// There will likely be an error report in your terminal, but don't worry
// everythings still working fine. Check it out
io.println("It's all good in the hood baby")
}

0 comments on commit 7e2aa87

Please sign in to comment.