Skip to content

Commit

Permalink
supervisors
Browse files Browse the repository at this point in the history
  • Loading branch information
bcpeinhardt committed Feb 29, 2024
1 parent 241f9fd commit d9448bb
Show file tree
Hide file tree
Showing 6 changed files with 185 additions and 34 deletions.
6 changes: 6 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"editor.tokenColorCustomizations": {
"comments": "",
"textMateRules": []
}
}
1 change: 1 addition & 0 deletions gleam.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ version = "1.0.0"
gleam_stdlib = "~> 0.34 or ~> 1.0"
gleam_erlang = "~> 0.24"
gleam_otp = "~> 0.9"
prng = "~> 3.0"

[dev-dependencies]
gleeunit = "~> 1.0"
5 changes: 4 additions & 1 deletion manifest.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,17 @@
# You typically do not need to edit this file

packages = [
{ name = "gleam_bitwise", version = "1.3.1", build_tools = ["gleam"], requirements = [], otp_app = "gleam_bitwise", source = "hex", outer_checksum = "B36E1D3188D7F594C7FD4F43D0D2CE17561DE896202017548578B16FE1FE9EFC" },
{ name = "gleam_erlang", version = "0.24.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_erlang", source = "hex", outer_checksum = "26BDB52E61889F56A291CB34167315780EE4AA20961917314446542C90D1C1A0" },
{ name = "gleam_otp", version = "0.9.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_stdlib"], otp_app = "gleam_otp", source = "hex", outer_checksum = "5FADBBEC5ECF3F8B6BE91101D432758503192AE2ADBAD5602158977341489F71" },
{ name = "gleam_otp", version = "0.9.0", build_tools = ["gleam"], requirements = ["gleam_stdlib", "gleam_erlang"], otp_app = "gleam_otp", source = "hex", outer_checksum = "5FADBBEC5ECF3F8B6BE91101D432758503192AE2ADBAD5602158977341489F71" },
{ name = "gleam_stdlib", version = "0.35.1", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "5443EEB74708454B65650FEBBB1EF5175057D1DEC62AEA9D7C6D96F41DA79152" },
{ name = "gleeunit", version = "1.0.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "D364C87AFEB26BDB4FB8A5ABDE67D635DC9FA52D6AB68416044C35B096C6882D" },
{ name = "prng", version = "3.0.1", build_tools = ["gleam"], requirements = ["gleam_bitwise", "gleam_stdlib"], otp_app = "prng", source = "hex", outer_checksum = "C78A80DE41469A0BB1AB3B0B0610CCE5DB70C5659A540E2E0E6C54FA38134290" },
]

[requirements]
gleam_erlang = { version = "~> 0.24" }
gleam_otp = { version = "~> 0.9" }
gleam_stdlib = { version = "~> 0.34 or ~> 1.0" }
gleeunit = { version = "~> 1.0" }
prng = { version = "~> 3.0"}
7 changes: 4 additions & 3 deletions src/actors/pantry.gleam
Original file line number Diff line number Diff line change
Expand Up @@ -113,14 +113,15 @@ 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,
// Note: This example is meant to be straightforward. In a real 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
// Utilizing processes 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.
//
// There's a lot going on with this example, so don't worry if you need to sit
// with it for a while.
// with it for a while. When you think you've got it, I recommend heading to the
// supervisors section next.
114 changes: 102 additions & 12 deletions src/supervisors.gleam
Original file line number Diff line number Diff line change
@@ -1,15 +1,78 @@
//// Alright time to learn about Supervisors
////
//// OTP applications are structured as "supervision" trees.
//// There are two types of processes in a supervision tree:
//// 1. Workers (these are the leaf nodes of the tree, they do all work)
//// 2. Supervisors (parent nodes of the tree, they manage the lifecycle of their child nodes,
//// starting, stopping, restarting, and brutally killing them if necessary)
//// Pretty straightforward, workers do work, supervisors supervise.
////
//// Ok, why is this good?
//// The propoganda goes like this:
////
//// -----------------------------------------------------------------------------------------
////
//// There are two kinds of bugs in software: consistent bugs and transient bugs.
////
//// Consistent (aka reproducible) bugs are really common but easy to discover and fix.
//// Good testing (and in Gleam's case, a good type system) can help with this.
////
//// Transient bugs are more rare, but they are a pain to diagnose and fix, and so they
//// are often left in the codebase for a long time.
////
//// The best approach we have for fixing these bugs when we can't reproduce them is to
//// turn the damn thing off and on again.
////
//// Turning your tv off and on again is lame, but doable.
//// Turning a running production system off and on again is a non starter.
////
//// This is where supervisors come in. They are designed to provide common sense strategies
//// for turning their child processes off and on again when they crash.
////
//// By designing our application as a tree of these supervisors and workers, we can turn
//// JUST THE BROKEN PART off and on again, while the rest of the system keeps on running.
////
//// A supervisor will "detect" what the broken part is by slowly restarting layers, starting
//// at the smallest layer and incrementing its way up the tree, until everything works again.
////
//// This architecture plus the BEAM's ability to update code while it is still running
//// are why these technologies are so good at building fault-tolerant systems.
////
//// Systems with transient concurrency bugs still operate mostly correctly, and one
//// can debug and fix the broken part without taking the system down.
////
//// -----------------------------------------------------------------------------------------
////
//// Evangelism complete. I don't need to give you the "everything's a tradeoff,
//// there's no free lunch" speech, right?
////
//// Ok, I should put my money where my mouth is and show you some code.
//// To show how this works in practice, we need to write a program that crashes EVERY NOW AND THEN.
//// Such a program has been written in `supervisors/a_shit_actor.gleam`. Go read that and then
//// come back here.
////
//// Back? Great, read on.

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

pub fn main() {
// Let's set up our supervisor tree.
// This one will be really simple.
// One worker (the game), and one supervisor.

// We set up our worker, and we give the actor a subject for this process to send
// us messages with on init. Remember, it needs to send us back a subject so we
// can talk to it directly.
let parent_subject = process.new_subject()
let game = supervisor.worker(duckduckgoose.start(_, parent_subject))

// The supervisor API is really simple. All a supervisor needs is a function
// with which to intialize itself.
// There's a `supervisor.start_spec` function as well for tuning the
// restart frequency and the initial state to pass to children.
let children = fn(children) {
children
|> supervisor.add(game)
Expand All @@ -22,18 +85,45 @@ pub fn main() {
// to send it messages
let assert Ok(game_subject) = process.receive(parent_subject, 1000)

// Good messages, nothing crashes
let assert Ok("duck") = duckduckgoose.duck(game_subject)
// Let's play the game a bit
play_game(parent_subject, game_subject, 100)
}

/// This function will play the duck duck goose game 100 times
/// (As in 100 chances to be a goose, not 100 geese total)
fn play_game(
parent_subject: Subject(Subject(duckduckgoose.Message)),
game_subject: Subject(duckduckgoose.Message),
times n: Int,
) {
case n {
// Base Case, recess is over
0 -> Nil
_ -> {
case duckduckgoose.play_game(game_subject) {
// We're just a noraml old duck, so we keep playing
Ok(msg) -> {
io.println(msg)
play_game(parent_subject, game_subject, n - 1)
}

// Oh shit, that ain't good, our actor is gonna crash
let assert Error(_) = duckduckgoose.goose(game_subject)
// Oh no, a goose crashed our actor!
Error(_) -> {
io.println("Oh no, a goose crashed our actor!")

// 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)
// The supervisor should restart our actor for us,
// but it'll be on a different process now! Don't
// worry though, the game's init function should
// send us a new subject to use.
let assert Ok(new_game_subject) =
process.receive(parent_subject, 1000)

// 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")
// Keep playing the game with the new subject
play_game(parent_subject, new_game_subject, n - 1)
}
}
}
}
}
// Run this module and checkout the output. You should get a couple crash reports,
// but the game should keep on running printing duck messages as well.
86 changes: 68 additions & 18 deletions src/supervisors/a_shit_actor.gleam
Original file line number Diff line number Diff line change
@@ -1,55 +1,105 @@
//// This module implements a shit actor that crashes every now and then.
//// Having an actor that fails every now and then will help us test out our supervisors.
//// It's also a nice refresher on actors. Read the code and make sure it makes sense.
//// If you need a refresher on actors, go revisit the `actor.gleam` and `actor/pantry` code.
////
//// Alright, let's implement of game of Duck Duck Goose as an actor.
////
//// (If this game is unfamiliar to you, children sit in a circle while
//// one of them walks around behind the rest tapping them and saying "duck"
//// or "goose". They say "duck" for awhile, and nothing happens, but when
//// they choose the "goose" all hell breaks loose and they chase each other
//// around the circle. Interestingly in the midwest of the United States the
//// game is often called "Duck, Duck, Grey Duck".)

import gleam/otp/actor
import gleam/erlang/process.{type Subject}
import gleam/function
import prng/random

/// Okay, well this is new.
/// We're going to hand this actor off to supervisor,
/// which will manage starting it for us.
///
/// That means we can't simply get the subject out from
/// the return, since we dont call the start function
/// directly. Instead, we'll have to send the subject
/// to the parent process when the actor starts up.
///
/// The `actor.start_spec` function gives us more fine-grained
/// control over how the actor gets created. We get to
/// provide a startup function to produce the initial state,
/// instead of simply providing the initial state directly.
///
/// We'll take advantadge of getting the chance to compute
/// things on the new process to send ourselves back a subject
/// for the actor.
///
/// This isn't a hack, it's the intended design. The subject
/// produced by the `actor.start_spec` function is for the
/// supervisor to use, not for us to use directly.
pub fn start(
_input: Nil,
parent_subject: Subject(Subject(Message)),
) -> Result(Subject(Message), actor.StartError) {
actor.start_spec(actor.Spec(
init: fn() {
// Create a new subject and send it to the parent process,
// so that the parent process can send us messages.
let actor_subject = process.new_subject()
process.send(parent_subject, actor_subject)
actor.Ready(
Nil,
process.selecting(
process.new_selector(),
actor_subject,
function.identity,
),
)

// Initialize the actor.
// Notice we provide a selector rather than a simple subject.
//
// We can send out multiple subjects on startup if we want,
// so the actor can be communicated with from multiple processes.
// The selector allows us to handle messages as they come in, no
// matter which subject they were sent to.
//
// In our case, we only send out the one subject though.

let selector =
process.new_selector()
|> process.selecting(actor_subject, function.identity)

actor.Ready(Nil, selector)
},
// You might call other processes to start up your actor,
// so we set a timeout to prevent the supervisor from
// waiting forever for the actor to start.
init_timeout: 1000,
// This is the function that will be called when the actor
// get's sent a message. We'll define it below.
loop: handle_message,
))
}

/// We provide this function in case we want to manually stop the actor,
/// but in reality the supervisor will handle that for us.
pub fn shutdown(subject: Subject(Message)) {
actor.send(subject, Shutdown)
}

pub fn duck(
subject: Subject(Message),
) -> Result(String, process.CallError(String)) {
process.try_call(subject, Duck, 1000)
}
/// This is how we play the game.
/// We are at the whim of the child as to whether we are a
/// humble duck or the mighty goose.
pub fn play_game(subject: Subject(Message)) {
let msg_generator = random.weighted(#(9.0, Duck), [#(1.0, Goose)])
let msg = random.random_sample(msg_generator)

pub fn goose(
subject: Subject(Message),
) -> Result(String, process.CallError(String)) {
process.try_call(subject, Goose, 1000)
process.try_call(subject, msg, 1000)
}

/// This is the type of messages that the actor will receive.
/// Remember, any time we want to reply to a message, that message
/// must contain a subject to reply with.
pub type Message {
Duck(client: Subject(String))
Goose(client: Subject(String))
Shutdown
}

/// And finally, we play the game
fn handle_message(message: Message, _state: Nil) -> actor.Next(Message, Nil) {
case message {
Duck(client) -> {
Expand Down

0 comments on commit d9448bb

Please sign in to comment.