diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..926d143 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,6 @@ +{ + "editor.tokenColorCustomizations": { + "comments": "", + "textMateRules": [] + } +} \ No newline at end of file diff --git a/gleam.toml b/gleam.toml index e652086..2493151 100644 --- a/gleam.toml +++ b/gleam.toml @@ -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" diff --git a/manifest.toml b/manifest.toml index fe91f12..016ca36 100644 --- a/manifest.toml +++ b/manifest.toml @@ -2,10 +2,12 @@ # 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] @@ -13,3 +15,4 @@ 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"} diff --git a/src/actors/pantry.gleam b/src/actors/pantry.gleam index 4265a41..f7002ac 100644 --- a/src/actors/pantry.gleam +++ b/src/actors/pantry.gleam @@ -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. diff --git a/src/supervisors.gleam b/src/supervisors.gleam index 01de638..b64119a 100644 --- a/src/supervisors.gleam +++ b/src/supervisors.gleam @@ -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) @@ -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. diff --git a/src/supervisors/a_shit_actor.gleam b/src/supervisors/a_shit_actor.gleam index de1e208..f3d0c07 100644 --- a/src/supervisors/a_shit_actor.gleam +++ b/src/supervisors/a_shit_actor.gleam @@ -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) -> {