From 612fd986ab1e00b6d34dc1937136250e08e89325 Mon Sep 17 00:00:00 2001 From: "J.J" Date: Thu, 30 May 2024 21:50:02 -0400 Subject: cleanup --- aoc2023/build/packages/gleam_otp/LICENCE | 191 ++++++++ aoc2023/build/packages/gleam_otp/README.md | 91 ++++ aoc2023/build/packages/gleam_otp/gleam.toml | 19 + .../gleam_otp/include/gleam@otp@actor_Continue.hrl | 4 + .../gleam_otp/include/gleam@otp@actor_Ready.hrl | 1 + .../gleam_otp/include/gleam@otp@actor_Spec.hrl | 5 + ...leam@otp@intensity_tracker_IntensityTracker.hrl | 5 + .../include/gleam@otp@supervisor_ChildSpec.hrl | 5 + .../include/gleam@otp@supervisor_Spec.hrl | 6 + .../include/gleam@otp@system_StatusInfo.hrl | 7 + .../gleam_otp/include/gleam@otp@task_Exit.hrl | 1 + .../gleam_otp/include/gleam@otp@task_Task.hrl | 6 + .../packages/gleam_otp/src/gleam/otp/actor.gleam | 504 +++++++++++++++++++++ .../src/gleam/otp/intensity_tracker.gleam | 46 ++ .../packages/gleam_otp/src/gleam/otp/port.gleam | 9 + .../gleam_otp/src/gleam/otp/supervisor.gleam | 410 +++++++++++++++++ .../packages/gleam_otp/src/gleam/otp/system.gleam | 89 ++++ .../packages/gleam_otp/src/gleam/otp/task.gleam | 151 ++++++ .../packages/gleam_otp/src/gleam@otp@actor.erl | 273 +++++++++++ .../gleam_otp/src/gleam@otp@intensity_tracker.erl | 53 +++ .../packages/gleam_otp/src/gleam@otp@port.erl | 8 + .../gleam_otp/src/gleam@otp@supervisor.erl | 322 +++++++++++++ .../packages/gleam_otp/src/gleam@otp@system.erl | 43 ++ .../packages/gleam_otp/src/gleam@otp@task.erl | 111 +++++ .../build/packages/gleam_otp/src/gleam_otp.app.src | 15 + aoc2023/build/packages/gleam_otp/src/gleam_otp.erl | 28 ++ .../build/packages/gleam_otp/src/gleam_otp.gleam | 27 ++ .../packages/gleam_otp/src/gleam_otp_external.erl | 43 ++ 28 files changed, 2473 insertions(+) create mode 100644 aoc2023/build/packages/gleam_otp/LICENCE create mode 100644 aoc2023/build/packages/gleam_otp/README.md create mode 100644 aoc2023/build/packages/gleam_otp/gleam.toml create mode 100644 aoc2023/build/packages/gleam_otp/include/gleam@otp@actor_Continue.hrl create mode 100644 aoc2023/build/packages/gleam_otp/include/gleam@otp@actor_Ready.hrl create mode 100644 aoc2023/build/packages/gleam_otp/include/gleam@otp@actor_Spec.hrl create mode 100644 aoc2023/build/packages/gleam_otp/include/gleam@otp@intensity_tracker_IntensityTracker.hrl create mode 100644 aoc2023/build/packages/gleam_otp/include/gleam@otp@supervisor_ChildSpec.hrl create mode 100644 aoc2023/build/packages/gleam_otp/include/gleam@otp@supervisor_Spec.hrl create mode 100644 aoc2023/build/packages/gleam_otp/include/gleam@otp@system_StatusInfo.hrl create mode 100644 aoc2023/build/packages/gleam_otp/include/gleam@otp@task_Exit.hrl create mode 100644 aoc2023/build/packages/gleam_otp/include/gleam@otp@task_Task.hrl create mode 100644 aoc2023/build/packages/gleam_otp/src/gleam/otp/actor.gleam create mode 100644 aoc2023/build/packages/gleam_otp/src/gleam/otp/intensity_tracker.gleam create mode 100644 aoc2023/build/packages/gleam_otp/src/gleam/otp/port.gleam create mode 100644 aoc2023/build/packages/gleam_otp/src/gleam/otp/supervisor.gleam create mode 100644 aoc2023/build/packages/gleam_otp/src/gleam/otp/system.gleam create mode 100644 aoc2023/build/packages/gleam_otp/src/gleam/otp/task.gleam create mode 100644 aoc2023/build/packages/gleam_otp/src/gleam@otp@actor.erl create mode 100644 aoc2023/build/packages/gleam_otp/src/gleam@otp@intensity_tracker.erl create mode 100644 aoc2023/build/packages/gleam_otp/src/gleam@otp@port.erl create mode 100644 aoc2023/build/packages/gleam_otp/src/gleam@otp@supervisor.erl create mode 100644 aoc2023/build/packages/gleam_otp/src/gleam@otp@system.erl create mode 100644 aoc2023/build/packages/gleam_otp/src/gleam@otp@task.erl create mode 100644 aoc2023/build/packages/gleam_otp/src/gleam_otp.app.src create mode 100644 aoc2023/build/packages/gleam_otp/src/gleam_otp.erl create mode 100644 aoc2023/build/packages/gleam_otp/src/gleam_otp.gleam create mode 100644 aoc2023/build/packages/gleam_otp/src/gleam_otp_external.erl (limited to 'aoc2023/build/packages/gleam_otp') diff --git a/aoc2023/build/packages/gleam_otp/LICENCE b/aoc2023/build/packages/gleam_otp/LICENCE new file mode 100644 index 0000000..619ec77 --- /dev/null +++ b/aoc2023/build/packages/gleam_otp/LICENCE @@ -0,0 +1,191 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + Copyright 2019, Louis Pilfold . + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + diff --git a/aoc2023/build/packages/gleam_otp/README.md b/aoc2023/build/packages/gleam_otp/README.md new file mode 100644 index 0000000..3c313a1 --- /dev/null +++ b/aoc2023/build/packages/gleam_otp/README.md @@ -0,0 +1,91 @@ +# Gleam OTP + +GitHub release +Discord chat +![CI](https://github.com/gleam-lang/otp/workflows/test/badge.svg?branch=main) + +A Gleam library for building fault tolerant multi-core programs using the +actor model. It is compatible with Erlang's OTP framework. + +This library is experimental and will likely have many breaking changes in the +future! + +Gleam’s actor system is built with a few primary goals: + +- Full type safety of actors and messages. +- Be compatible with Erlang’s OTP actor framework. +- Provide fault tolerance and self-healing through supervisors. +- Have equivalent performance to Erlang’s OTP. + +This library documents its abstractions and functionality, but you may also wish +to read the documentation or other material on Erlang’s OTP framework to get a +fuller understanding of OTP, the problems it solves, and and the motivations for +its design. + +## Usage + +Add this library to your Gleam project. + +```shell +gleam add gleam_otp +``` + +## Actor hierarchy + +This library provides several different types of actor that can be used in +Gleam programs. + +### Process + +The process is the lowest level building block of OTP, all other actors are +built on top of processes either directly or indirectly. Typically this +abstraction would be not be used very often in Gleam applications, favour +other actor types that provide more functionality. + +Gleam's process module is defined in the `gleam_erlang` library. + +[[Documentation]](https://hexdocs.pm/gleam_erlang/gleam/erlang/process.html) + +### Actor + +The `actor` is the most commonly used process type in Gleam and serves as a good +building block for other abstractions. Like Erlang's `gen_server` it handles +OTP's system messages automatically to enable OTP's debugging and tracing +functionality. + +[[Documentation]](https://hexdocs.pm/gleam_otp/gleam/otp/actor.html) + +### Task + +A task is a kind of process that performs a single task and then shuts down. +Commonly tasks are used to convert sequential code into concurrent code by +performing computation in another process. + +[[Documentation]](https://hexdocs.pm/gleam_otp/gleam/otp/task.html) + +### Supervisor + +Supervisors is a process that starts and then supervises a group of processes, +restarting them if they crash. Supervisors can start other supervisors, +resulting in a hierarchical process structure called a supervision tree, +providing fault tolerance to a Gleam application. + +[[Documentation]](https://hexdocs.pm/gleam_otp/gleam/otp/supervisor.html) + +## Limitations and known issues + +This library is experimental there are some limitations that not yet been resolved. + +- There is no support for named processes. They are untyped global mutable + variables which may be uninitialized, more research is needed to find a + suitable type safe alternative. +- There are relatively few actor abstractions provided by this library. More + will be added in the future. +- Actors do not yet support all OTP system messages. Unsupported messages are + dropped. +- Supervisors do not yet support different shutdown periods per child. In + practice this means that children that are supervisors do not get an + unlimited amount of time to shut down, as is expected in Erlang or Elixir. +- This library has not seen much testing compared to the Erlang OTP + libraries, both in terms of unit tests and real world testing in + applications. diff --git a/aoc2023/build/packages/gleam_otp/gleam.toml b/aoc2023/build/packages/gleam_otp/gleam.toml new file mode 100644 index 0000000..26e451b --- /dev/null +++ b/aoc2023/build/packages/gleam_otp/gleam.toml @@ -0,0 +1,19 @@ +name = "gleam_otp" +version = "0.8.0" +licences = ["Apache-2.0"] +description = "Fault tolerant multicore Gleam programs with OTP" + +gleam = ">= 0.32.0" + +repository = { type = "github", user = "gleam-lang", repo = "otp" } +links = [ + { title = "Website", href = "https://gleam.run" }, + { title = "Sponsor", href = "https://github.com/sponsors/lpil" }, +] + +[dependencies] +gleam_stdlib = "~> 0.32" +gleam_erlang = "~> 0.22" + +[dev-dependencies] +gleeunit = "~> 1.0" diff --git a/aoc2023/build/packages/gleam_otp/include/gleam@otp@actor_Continue.hrl b/aoc2023/build/packages/gleam_otp/include/gleam@otp@actor_Continue.hrl new file mode 100644 index 0000000..85677d1 --- /dev/null +++ b/aoc2023/build/packages/gleam_otp/include/gleam@otp@actor_Continue.hrl @@ -0,0 +1,4 @@ +-record(continue, { + state :: any(), + selector :: gleam@option:option(gleam@erlang@process:selector(any())) +}). diff --git a/aoc2023/build/packages/gleam_otp/include/gleam@otp@actor_Ready.hrl b/aoc2023/build/packages/gleam_otp/include/gleam@otp@actor_Ready.hrl new file mode 100644 index 0000000..75faa95 --- /dev/null +++ b/aoc2023/build/packages/gleam_otp/include/gleam@otp@actor_Ready.hrl @@ -0,0 +1 @@ +-record(ready, {state :: any(), selector :: gleam@erlang@process:selector(any())}). diff --git a/aoc2023/build/packages/gleam_otp/include/gleam@otp@actor_Spec.hrl b/aoc2023/build/packages/gleam_otp/include/gleam@otp@actor_Spec.hrl new file mode 100644 index 0000000..5287439 --- /dev/null +++ b/aoc2023/build/packages/gleam_otp/include/gleam@otp@actor_Spec.hrl @@ -0,0 +1,5 @@ +-record(spec, { + init :: fun(() -> gleam@otp@actor:init_result(any(), any())), + init_timeout :: integer(), + loop :: fun((any(), any()) -> gleam@otp@actor:next(any(), any())) +}). diff --git a/aoc2023/build/packages/gleam_otp/include/gleam@otp@intensity_tracker_IntensityTracker.hrl b/aoc2023/build/packages/gleam_otp/include/gleam@otp@intensity_tracker_IntensityTracker.hrl new file mode 100644 index 0000000..3ed0b01 --- /dev/null +++ b/aoc2023/build/packages/gleam_otp/include/gleam@otp@intensity_tracker_IntensityTracker.hrl @@ -0,0 +1,5 @@ +-record(intensity_tracker, { + limit :: integer(), + period :: integer(), + events :: list(integer()) +}). diff --git a/aoc2023/build/packages/gleam_otp/include/gleam@otp@supervisor_ChildSpec.hrl b/aoc2023/build/packages/gleam_otp/include/gleam@otp@supervisor_ChildSpec.hrl new file mode 100644 index 0000000..7afd07f --- /dev/null +++ b/aoc2023/build/packages/gleam_otp/include/gleam@otp@supervisor_ChildSpec.hrl @@ -0,0 +1,5 @@ +-record(child_spec, { + start :: fun((any()) -> {ok, gleam@erlang@process:subject(any())} | + {error, gleam@otp@actor:start_error()}), + returning :: fun((any(), gleam@erlang@process:subject(any())) -> any()) +}). diff --git a/aoc2023/build/packages/gleam_otp/include/gleam@otp@supervisor_Spec.hrl b/aoc2023/build/packages/gleam_otp/include/gleam@otp@supervisor_Spec.hrl new file mode 100644 index 0000000..b10bd9f --- /dev/null +++ b/aoc2023/build/packages/gleam_otp/include/gleam@otp@supervisor_Spec.hrl @@ -0,0 +1,6 @@ +-record(spec, { + argument :: any(), + max_frequency :: integer(), + frequency_period :: integer(), + init :: fun((gleam@otp@supervisor:children(any())) -> gleam@otp@supervisor:children(any())) +}). diff --git a/aoc2023/build/packages/gleam_otp/include/gleam@otp@system_StatusInfo.hrl b/aoc2023/build/packages/gleam_otp/include/gleam@otp@system_StatusInfo.hrl new file mode 100644 index 0000000..99ab4cb --- /dev/null +++ b/aoc2023/build/packages/gleam_otp/include/gleam@otp@system_StatusInfo.hrl @@ -0,0 +1,7 @@ +-record(status_info, { + module :: gleam@erlang@atom:atom_(), + parent :: gleam@erlang@process:pid_(), + mode :: gleam@otp@system:mode(), + debug_state :: gleam@otp@system:debug_state(), + state :: gleam@dynamic:dynamic_() +}). diff --git a/aoc2023/build/packages/gleam_otp/include/gleam@otp@task_Exit.hrl b/aoc2023/build/packages/gleam_otp/include/gleam@otp@task_Exit.hrl new file mode 100644 index 0000000..7c83874 --- /dev/null +++ b/aoc2023/build/packages/gleam_otp/include/gleam@otp@task_Exit.hrl @@ -0,0 +1 @@ +-record(exit, {reason :: gleam@dynamic:dynamic_()}). diff --git a/aoc2023/build/packages/gleam_otp/include/gleam@otp@task_Task.hrl b/aoc2023/build/packages/gleam_otp/include/gleam@otp@task_Task.hrl new file mode 100644 index 0000000..959bea8 --- /dev/null +++ b/aoc2023/build/packages/gleam_otp/include/gleam@otp@task_Task.hrl @@ -0,0 +1,6 @@ +-record(task, { + owner :: gleam@erlang@process:pid_(), + pid :: gleam@erlang@process:pid_(), + monitor :: gleam@erlang@process:process_monitor(), + selector :: gleam@erlang@process:selector(gleam@otp@task:message(any())) +}). diff --git a/aoc2023/build/packages/gleam_otp/src/gleam/otp/actor.gleam b/aoc2023/build/packages/gleam_otp/src/gleam/otp/actor.gleam new file mode 100644 index 0000000..9f6a6c4 --- /dev/null +++ b/aoc2023/build/packages/gleam_otp/src/gleam/otp/actor.gleam @@ -0,0 +1,504 @@ +//// 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 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 +//// process is that it provides a single interface for commonly needed +//// functionality, including support for the [tracing and debugging +//// features in OTP](erlang-sys). +//// +//// Gleam's Actor is similar to Erlang's `gen_server` and Elixir's `GenServer` +//// but differs in that it offers a fully typed interface. This different API is +//// why Gleam uses the name Actor rather than some variation of generic-server. +//// +//// [erlang-sys]: https://www.erlang.org/doc/man/sys.html +//// +//// ## Example +//// +//// An Actor can be used to create a client-server interaction between an Actor +//// (the server) and other processes (the clients). In this example we have an +//// Actor that works as a stack, allowing clients to push and pop elements. +//// +//// ```gleam +//// pub fn main() { +//// // 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 a 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 +//// // API works. +//// let assert Ok(actor) = actor.start([], handle_message) +//// +//// // We can send a message to the actor to push elements onto the stack. +//// process.send(actor, Push("Joe")) +//// process.send(actor, Push("Mike")) +//// process.send(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. +//// // +//// // We can also send the `Pop` message to take a value off of the actor's +//// // stack. This message expects a response, so we use `process.call` to send a +//// // message and wait until a reply is received. +//// // +//// // In this instance we are giving the actor 10 milliseconds to reply, if the +//// // `call` function doesn't get a reply within this time it will panic and +//// // crash the client process. +//// let assert Ok("Robert") = process.call(actor, Pop, 10) +//// let assert Ok("Mike") = process.call(actor, Pop, 10) +//// let assert Ok("Joe") = process.call(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(actor, Pop, 10) +//// +//// // Lastly, we can send a message to the actor asking it to shut down. +//// process.send(actor, Shutdown) +//// } +//// ``` +//// +//// Here is the code that is used to implement this actor: +//// +//// ```gleam +//// // First step of implementing the stack Actor is to define the message type that +//// // it can receive. +//// // +//// // The type of the elements in the stack is no fixed so a type parameter is used +//// // for it instead of a concrete type such as `String` or `Int`. +//// pub type Message(element) { +//// // 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 each for each message it receives. +//// // Actor is single threaded only does one thing at a time, so it handles +//// // messages sequentially and one at a time, in the order they are received. +//// // +//// // The function takes the message and the current state, and returns a data +//// // structure that indicates what to do next, along with the new state. +//// fn handle_message( +//// message: Message(e), +//// stack: List(e), +//// ) -> actor.Next(Message(e), List(e)) { +//// 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) +//// +//// // 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) -> +//// case stack { +//// [] -> { +//// // When the stack is empty we can't pop an element, so we send an +//// // error back. +//// 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. +//// process.send(client, Ok(first)) +//// actor.continue(rest) +//// } +//// } +//// } +//// } +//// ``` + +import gleam/erlang/process.{ + type ExitReason, type Pid, type Selector, type Subject, Abnormal, +} +import gleam/erlang/charlist.{type Charlist} +import gleam/otp/system.{ + type DebugState, type Mode, type StatusInfo, type SystemMessage, GetState, + GetStatus, Resume, Running, StatusInfo, Suspend, Suspended, +} +import gleam/string +import gleam/dynamic.{type Dynamic} +import gleam/erlang/atom +import gleam/option.{type Option, None, Some} + +type Message(message) { + /// A regular message excepted by the process + Message(message) + + /// An OTP system message, for debugging or maintenance + System(SystemMessage) + + /// An unexpected message + Unexpected(Dynamic) +} + +/// The type used to indicate what to do after handling a message. +/// +pub type Next(message, state) { + /// Continue handling messages. + /// + Continue(state: state, selector: Option(Selector(message))) + + /// Stop handling messages and shut down. + /// + Stop(ExitReason) +} + +pub fn continue(state: state) -> Next(message, state) { + Continue(state, None) +} + +pub fn with_selector( + value: Next(message, state), + selector: Selector(message), +) -> Next(message, state) { + case value { + Continue(state, _) -> Continue(state, Some(selector)) + _ -> value + } +} + +/// The type used to indicate whether an actor has started successfully or not. +/// +pub type InitResult(state, message) { + /// The actor has successfully initialised. The actor can start handling + /// messages and actor's channel sender can be returned to the parent + /// process. + /// + Ready(state: state, selector: Selector(message)) + + /// The actor has failed to initialise. The actor shuts down and an error is + /// returned to the parent process. + /// + Failed(String) +} + +type Self(state, msg) { + Self( + mode: Mode, + parent: Pid, + state: state, + subject: Subject(msg), + selector: Selector(Message(msg)), + debug_state: DebugState, + message_handler: fn(msg, state) -> Next(msg, state), + ) +} + +/// This data structure holds all the values required by the `start_spec` +/// function in order to create an actor. +/// +/// If you do not need to configure the initialisation behaviour of your actor +/// consider using the `start` function. +/// +pub type Spec(state, msg) { + Spec( + /// The initialisation functionality for the actor. This function is called + /// just after the actor starts but before the channel sender is returned + /// to the parent. + /// + /// This function is used to ensure that any required data or state is + /// correct. If this function returns an error it means that the actor has + /// failed to start and an error is returned to the parent. + /// + init: fn() -> InitResult(state, msg), + /// How many milliseconds the `init` function has to return before it is + /// considered to have taken too long and failed. + /// + init_timeout: Int, + /// This function is called to handle each message that the actor receives. + /// + loop: fn(msg, state) -> Next(msg, state), + ) +} + +// 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 + Suspended -> + process.new_selector() + |> selecting_system_messages + + // When running we respond to all messages + Running -> + // We add the handler for unexpected messages first so that the user + // supplied selector can override it if desired + process.new_selector() + |> process.selecting_anything(Unexpected) + |> process.merge_selector(self.selector) + |> selecting_system_messages + } + + process.select_forever(selector) +} + +fn selecting_system_messages( + selector: Selector(Message(msg)), +) -> Selector(Message(msg)) { + selector + |> process.selecting_record3( + atom.create_from_string("system"), + convert_system_message, + ) +} + +@external(erlang, "gleam_otp_external", "convert_system_message") +fn convert_system_message(a: Dynamic, b: Dynamic) -> Message(msg) + +fn process_status_info(self: Self(state, msg)) -> StatusInfo { + StatusInfo( + module: atom.create_from_string("gleam@otp@actor"), + parent: self.parent, + mode: self.mode, + debug_state: self.debug_state, + state: dynamic.from(self.state), + ) +} + +fn loop(self: Self(state, msg)) -> ExitReason { + case receive_message(self) { + System(system) -> + case system { + GetState(callback) -> { + callback(dynamic.from(self.state)) + loop(self) + } + Resume(callback) -> { + callback() + loop(Self(..self, mode: Running)) + } + Suspend(callback) -> { + callback() + loop(Self(..self, mode: Suspended)) + } + GetStatus(callback) -> { + callback(process_status_info(self)) + loop(self) + } + } + + Unexpected(message) -> { + log_warning( + charlist.from_string("Actor discarding unexpected message: ~s"), + [charlist.from_string(string.inspect(message))], + ) + loop(self) + } + + Message(msg) -> + case self.message_handler(msg, self.state) { + Stop(reason) -> exit_process(reason) + Continue(state: state, selector: new_selector) -> { + let selector = + new_selector + |> option.map(init_selector(self.subject, _)) + |> option.unwrap(self.selector) + loop(Self(..self, state: state, selector: selector)) + } + } + } +} + +// TODO: replace this when we have Gleam bindings to the logger +@external(erlang, "logger", "warning") +fn log_warning(a: Charlist, b: List(Charlist)) -> Nil + +fn initialise_actor( + spec: Spec(state, msg), + ack: Subject(Result(Subject(msg), ExitReason)), +) { + let subject = process.new_subject() + case spec.init() { + Ready(state, selector) -> { + let selector = init_selector(subject, selector) + // Signal to parent that the process has initialised successfully + process.send(ack, Ok(subject)) + // Start message receive loop + let self = + Self( + state: state, + parent: process.subject_owner(ack), + subject: subject, + selector: selector, + message_handler: spec.loop, + debug_state: system.debug_state([]), + mode: Running, + ) + loop(self) + } + + Failed(reason) -> { + process.send(ack, Error(Abnormal(reason))) + exit_process(Abnormal(reason)) + } + } +} + +fn init_selector(subject, selector) { + process.new_selector() + |> process.selecting(subject, Message) + |> process.merge_selector(process.map_selector(selector, Message)) +} + +pub type StartError { + InitTimeout + InitFailed(ExitReason) + InitCrashed(Dynamic) +} + +/// The result of starting a Gleam actor. +/// +/// This type is compatible with Gleam supervisors. If you wish to convert it +/// to a type compatible with Erlang supervisors see the `ErlangStartResult` +/// type and `erlang_start_result` function. +/// +pub type StartResult(msg) = + Result(Subject(msg), StartError) + +/// An Erlang supervisor compatible process start result. +/// +/// If you wish to convert this into a `StartResult` compatible with Gleam +/// supervisors see the `from_erlang_start_result` and `wrap_erlang_starter` +/// functions. +/// +pub type ErlangStartResult = + Result(Pid, Dynamic) + +/// Convert a Gleam actor start result into an Erlang supervisor compatible +/// process start result. +/// +pub fn to_erlang_start_result(res: StartResult(msg)) -> ErlangStartResult { + case res { + Ok(x) -> Ok(process.subject_owner(x)) + Error(x) -> Error(dynamic.from(x)) + } +} + +type StartInitMessage(msg) { + Ack(Result(Subject(msg), ExitReason)) + Mon(process.ProcessDown) +} + +// TODO: test init_timeout. Currently if we test it eunit prints an error from +// the process death. How do we avoid this? +// +/// Start an actor from a given specification. If the actor's `init` function +/// returns an error or does not return within `init_timeout` then an error is +/// returned. +/// +/// If you do not need to specify the initialisation behaviour of your actor +/// consider using the `start` function. +/// +pub fn start_spec(spec: Spec(state, msg)) -> Result(Subject(msg), StartError) { + let ack_subject = process.new_subject() + + let child = + process.start( + linked: True, + running: fn() { initialise_actor(spec, ack_subject) }, + ) + + let monitor = process.monitor_process(child) + let selector = + process.new_selector() + |> process.selecting(ack_subject, Ack) + |> process.selecting_process_down(monitor, Mon) + + let result = case process.select(selector, spec.init_timeout) { + // Child started OK + Ok(Ack(Ok(channel))) -> Ok(channel) + + // Child initialiser returned an error + Ok(Ack(Error(reason))) -> Error(InitFailed(reason)) + + // Child went down while initialising + Ok(Mon(down)) -> Error(InitCrashed(down.reason)) + + // Child did not finish initialising in time + Error(Nil) -> { + process.kill(child) + Error(InitTimeout) + } + } + + // Remove the monitor used for the starting of the actor as to avoid an extra + // message arriving at the parent if the child dies later. + process.demonitor_process(monitor) + + result +} + +/// Start an actor with a given initial state and message handling loop +/// function. +/// +/// This function returns a `Result` but it will always be `Ok` so it is safe +/// to use with `assert` if you are not starting this actor as part of a +/// supervision tree. +/// +/// If you wish to configure the initialisation behaviour of a new actor see +/// the `Spec` record and the `start_spec` function. +/// +pub fn start( + state: state, + loop: fn(msg, state) -> Next(msg, state), +) -> Result(Subject(msg), StartError) { + start_spec(Spec( + init: fn() { Ready(state, process.new_selector()) }, + loop: loop, + init_timeout: 5000, + )) +} + +/// Send a message over a given channel. +/// +/// This is a re-export of `process.send`, for the sake of convenience. +/// +pub fn send(subject: Subject(msg), msg: msg) -> Nil { + process.send(subject, msg) +} + +// TODO: test +/// Send a synchronous message and wait for a response from the receiving +/// process. +/// +/// If a reply is not received within the given timeout then the sender process +/// crashes. If you wish receive a `Result` rather than crashing see the +/// `process.try_call` function. +/// +/// This is a re-export of `process.call`, for the sake of convenience. +/// +pub fn call( + selector: Subject(message), + make_message: fn(Subject(reply)) -> message, + timeout: Int, +) -> reply { + process.call(selector, make_message, timeout) +} diff --git a/aoc2023/build/packages/gleam_otp/src/gleam/otp/intensity_tracker.gleam b/aoc2023/build/packages/gleam_otp/src/gleam/otp/intensity_tracker.gleam new file mode 100644 index 0000000..2044be0 --- /dev/null +++ b/aoc2023/build/packages/gleam_otp/src/gleam/otp/intensity_tracker.gleam @@ -0,0 +1,46 @@ +//// The intensity tracker is used to monitor how frequently an event happens, +//// erroring if it happens too many times within a period of time. + +import gleam/list + +// TODO: test +pub opaque type IntensityTracker { + IntensityTracker(limit: Int, period: Int, events: List(Int)) +} + +pub type TooIntense { + TooIntense +} + +pub fn new(limit limit: Int, period period: Int) -> IntensityTracker { + IntensityTracker(limit: limit, period: period, events: []) +} + +@external(erlang, "erlang", "monotonic_time") +fn monotonic_time(a: Int) -> Int + +fn now_seconds() -> Int { + monotonic_time(1) +} + +pub fn trim_window(events: List(Int), now: Int, period: Int) -> List(Int) { + case events { + [] -> [] + [event, ..events] -> + case now >= event + period { + True -> [event, ..trim_window(events, now, period)] + False -> [] + } + } +} + +pub fn add_event( + tracker: IntensityTracker, +) -> Result(IntensityTracker, TooIntense) { + let now = now_seconds() + let events = trim_window([now, ..tracker.events], now, tracker.period) + case list.length(events) >= tracker.limit { + True -> Error(TooIntense) + False -> Ok(IntensityTracker(..tracker, events: events)) + } +} diff --git a/aoc2023/build/packages/gleam_otp/src/gleam/otp/port.gleam b/aoc2023/build/packages/gleam_otp/src/gleam/otp/port.gleam new file mode 100644 index 0000000..4e1b4d8 --- /dev/null +++ b/aoc2023/build/packages/gleam_otp/src/gleam/otp/port.gleam @@ -0,0 +1,9 @@ +/// Ports are how code running on the Erlang virtual machine interacts with +/// the outside world. Bytes of data can be sent to and read from ports, +/// providing a form of message passing to an external program or resource. +/// +/// For more information on ports see the [Erlang ports documentation][1]. +/// +/// [1]: https://erlang.org/doc/reference_manual/ports.html +/// +pub type Port diff --git a/aoc2023/build/packages/gleam_otp/src/gleam/otp/supervisor.gleam b/aoc2023/build/packages/gleam_otp/src/gleam/otp/supervisor.gleam new file mode 100644 index 0000000..b99ad8e --- /dev/null +++ b/aoc2023/build/packages/gleam_otp/src/gleam/otp/supervisor.gleam @@ -0,0 +1,410 @@ +// TODO: specify amount of time permitted for shut-down +import gleam/result +import gleam/string +import gleam/option.{type Option, None, Some} +import gleam/erlang/process.{type Pid, type Subject} +import gleam/otp/actor.{type StartError} +import gleam/otp/intensity_tracker.{type IntensityTracker} +import gleam/erlang/node.{type Node} + +/// This data structure holds all the values required by the `start_spec` +/// function in order to create an supervisor. +/// +/// If you do not need to configure the behaviour of your supervisor consider +/// using the `start` function. +/// +pub type Spec(argument, return) { + Spec( + argument: argument, + max_frequency: Int, + frequency_period: Int, + init: fn(Children(argument)) -> Children(return), + ) +} + +/// This type represents the starting children of a supervisor within the +/// `init` function. +/// +pub opaque type Children(argument) { + Ready(Starter(argument)) + Failed(ChildStartError) +} + +/// This type contains all the information required to start a new child and +/// add it to the `Children`. +/// +/// This is typically created with the `worker` function. +/// +pub opaque type ChildSpec(msg, argument, returning) { + ChildSpec( + // TODO: merge this into one field + start: fn(argument) -> Result(Subject(msg), StartError), + returning: fn(argument, Subject(msg)) -> returning, + ) +} + +type ChildStartError { + ChildStartError(previous_pid: Option(Pid), error: StartError) +} + +pub opaque type Message { + Exit(process.ExitMessage) + RetryRestart(Pid) +} + +type Instruction { + StartAll + StartFrom(Pid) +} + +type State(a) { + State( + restarts: IntensityTracker, + starter: Starter(a), + retry_restarts: Subject(Pid), + ) +} + +type Starter(argument) { + Starter( + argument: argument, + exec: Option( + fn(Instruction) -> + Result(#(Starter(argument), Instruction), ChildStartError), + ), + ) +} + +type Child(argument) { + Child(pid: Pid, argument: argument) +} + +fn start_child( + child_spec: ChildSpec(msg, argument_in, argument_out), + argument: argument_in, +) -> Result(Child(argument_out), ChildStartError) { + use subject <- result.then( + child_spec.start(argument) + |> result.map_error(ChildStartError(None, _)), + ) + + Ok(Child( + pid: process.subject_owner(subject), + // Merge the new child's pid into the argument to produce the new argument + // used to start any remaining children. + argument: child_spec.returning(argument, subject), + )) +} + +// TODO: more sophsiticated stopping of processes. i.e. give supervisors +// more time to shut down. +fn shutdown_child(pid: Pid, _spec: ChildSpec(msg, arg_1, arg_2)) -> Nil { + process.send_exit(pid) +} + +fn perform_instruction_for_child( + argument: argument_in, + instruction: Instruction, + child_spec: ChildSpec(msg, argument_in, argument_out), + child: Child(argument_out), +) -> Result(#(Child(argument_out), Instruction), ChildStartError) { + let current = child.pid + case instruction { + // This child is older than the StartFrom target, we don't need to + // restart it + StartFrom(target) if target != current -> Ok(#(child, instruction)) + + // This pid either is the cause of the problem, or we have the StartAll + // instruction. Either way it and its younger siblings need to be restarted. + _ -> { + shutdown_child(current, child_spec) + use child <- result.then(start_child(child_spec, argument)) + Ok(#(child, StartAll)) + } + } +} + +fn add_child_to_starter( + starter: Starter(argument_in), + child_spec: ChildSpec(msg, argument_in, argument_out), + child: Child(argument_out), +) -> Starter(argument_out) { + let starter = fn(instruction) { + // Restart the older children. We use `try` to return early if the older + // children failed to start + use #(starter, instruction) <- result.then(case starter.exec { + Some(start) -> start(instruction) + None -> Ok(#(starter, instruction)) + }) + + // Perform the instruction, restarting the child as required + use #(child, instruction) <- result.then(perform_instruction_for_child( + starter.argument, + instruction, + child_spec, + child, + )) + + // Create a new starter for the next time the supervisor needs to restart + let starter = add_child_to_starter(starter, child_spec, child) + + Ok(#(starter, instruction)) + } + + Starter(exec: Some(starter), argument: child.argument) +} + +fn start_and_add_child( + state: Starter(argument_0), + child_spec: ChildSpec(msg, argument_0, argument_1), +) -> Children(argument_1) { + case start_child(child_spec, state.argument) { + Ok(child) -> Ready(add_child_to_starter(state, child_spec, child)) + Error(reason) -> Failed(reason) + } +} + +/// Add a child to the collection of children of the supervisor +/// +/// This function starts the child from the child spec. +/// +pub fn add( + children: Children(argument), + child_spec: ChildSpec(msg, argument, new_argument), +) -> Children(new_argument) { + case children { + // If one of the previous children has failed then we cannot continue + Failed(fail) -> Failed(fail) + + // If everything is OK so far then we can add the child + Ready(state) -> start_and_add_child(state, child_spec) + } +} + +// TODO: test +// TODO: unlimitd shut down duration +/// Prepare a new supervisor type child. +/// +/// If you wish to prepare a new non-supervisor type child see the `worker` +/// function. +/// +/// If you wish to change the type of the argument for later children see the +/// `returning` function. +/// +/// Note: Gleam supervisors do not yet support different shutdown periods per +/// child so this function is currently identical in behaviour to `worker`. It is +/// recommended to use this function for supervisor children nevertheless so the +/// correct shut down behaviour is used in later releases of this library. +/// +pub fn supervisor( + start: fn(argument) -> Result(Subject(msg), StartError), +) -> ChildSpec(msg, argument, argument) { + ChildSpec(start: start, returning: fn(argument, _channel) { argument }) +} + +/// Prepare a new worker type child. +/// +/// If you wish to prepare a new supervisor type child see the `supervisor` +/// function. +/// +/// If you wish to change the type of the argument for later children see the +/// `returning` function. +/// +pub fn worker( + start: fn(argument) -> Result(Subject(msg), StartError), +) -> ChildSpec(msg, argument, argument) { + ChildSpec(start: start, returning: fn(argument, _channel) { argument }) +} + +// TODO: test +/// As each child is added to a supervisors children a new argument is prepared +/// with which to start the next child. By default argument is the same as the +/// previous argument, but this function can be used to change it to something +/// else by passing a function that takes the previous argument and the sender +/// of the previous child. +/// +pub fn returning( + child: ChildSpec(msg, argument_a, argument_b), + updater: fn(argument_a, Subject(msg)) -> argument_c, +) -> ChildSpec(msg, argument_a, argument_c) { + ChildSpec(start: child.start, returning: updater) +} + +fn init( + spec: Spec(argument, return), +) -> actor.InitResult(State(return), Message) { + // Create a subject so that we can asynchronously retry restarting when we + // fail to bring an exited child + let retry = process.new_subject() + + // Trap exits so that we get a message when a child crashes + process.trap_exits(True) + + // Combine selectors + let selector = + process.new_selector() + |> process.selecting(retry, RetryRestart) + |> process.selecting_trapped_exits(Exit) + + // Start any children + let result = + Starter(argument: spec.argument, exec: None) + |> Ready + |> spec.init + + // Pass back up the result + case result { + Ready(starter) -> { + let restarts = + intensity_tracker.new( + limit: spec.max_frequency, + period: spec.frequency_period, + ) + let state = + State(starter: starter, restarts: restarts, retry_restarts: retry) + actor.Ready(state, selector) + } + + Failed(error) -> + actor.Failed(case error.error { + actor.InitTimeout -> "Child initialisation timed out" + actor.InitCrashed(reason) -> + string.append( + "Child crashed during initialisation: ", + string.inspect(reason), + ) + actor.InitFailed(reason) -> + string.append( + "Child failed to start during initialisation: ", + string.inspect(reason), + ) + }) + } +} + +type HandleExitError { + RestartFailed(pid: Pid, restarts: IntensityTracker) + TooManyRestarts +} + +fn handle_exit(pid: Pid, state: State(a)) -> actor.Next(Message, State(a)) { + let outcome = { + // If we are handling an exit then we must have some children + let assert Some(start) = state.starter.exec + + // Check to see if there has been too many restarts in this period + use restarts <- result.then( + state.restarts + |> intensity_tracker.add_event + |> result.map_error(fn(_) { TooManyRestarts }), + ) + + // Restart the exited child and any following children + use #(starter, _) <- result.then( + start(StartFrom(pid)) + |> result.map_error(fn(e: ChildStartError) { + RestartFailed(option.unwrap(e.previous_pid, pid), restarts) + }), + ) + + Ok(State(..state, starter: starter, restarts: restarts)) + } + + case outcome { + Ok(state) -> actor.continue(state) + Error(RestartFailed(failed_child, restarts)) -> { + // Asynchronously enqueue the restarting of this child again as we were + // unable to restart them this time. We do this asynchronously as we want + // to have a chance to handle any system messages that have come in. + process.send(state.retry_restarts, failed_child) + let state = State(..state, restarts: restarts) + actor.continue(state) + } + Error(TooManyRestarts) -> + actor.Stop(process.Abnormal( + "Child processes restarted too many times within allowed period", + )) + } +} + +fn loop( + message: Message, + state: State(argument), +) -> actor.Next(Message, State(argument)) { + case message { + Exit(exit_message) -> handle_exit(exit_message.pid, state) + RetryRestart(pid) -> handle_exit(pid, state) + } +} + +/// Start a supervisor from a given specification. +/// +pub fn start_spec(spec: Spec(a, b)) -> Result(Subject(Message), StartError) { + actor.start_spec(actor.Spec( + init: fn() { init(spec) }, + loop: loop, + init_timeout: 60_000, + )) +} + +/// Start a supervisor from a given `init` function. +/// +/// The init argument passed to children will be `Nil` and the maximum restart +/// intensity will be 1 restart per 5 seconds (the same as the default for +/// [Erlang supervisors][erl-sup]). If you wish to specify these values, see +/// the `start_spec` function and the `Spec` type. +/// +/// [erl-sup]: https://www.erlang.org/doc/design_principles/sup_princ.html#maximum-restart-intensity +/// +pub fn start( + init: fn(Children(Nil)) -> Children(a), +) -> Result(Subject(Message), StartError) { + start_spec(Spec( + init: init, + argument: Nil, + max_frequency: 1, + frequency_period: 5, + )) +} + +/// A type used to describe the situation in which an Erlang based application +/// is starting. +/// +/// For more information see the [Erlang distributed application +/// documentation][1] and the Learn Your Some Erlang chapter on [distributed +/// applications][2]. +/// +/// [1]: https://erlang.org/doc/design_principles/distributed_applications.html +/// [2]: https://learnyousomeerlang.com/distributed-otp-applications +/// +pub type ApplicationStartMode { + Normal + Takeover(Node) + Failover(Node) +} + +pub type ApplicationStop + +@external(erlang, "gleam_otp_external", "application_stopped") +pub fn application_stopped() -> ApplicationStop + +/// The result of starting a Gleam actor. +/// +/// This type is compatible with Gleam supervisors. If you wish to convert it +/// to a type compatible with Erlang supervisors see the `ErlangStartResult` +/// type and `erlang_start_result` function. +/// +pub type StartResult(msg) = + actor.StartResult(msg) + +/// An Erlang supervisor compatible process start result. +/// +pub type ErlangStartResult = + actor.ErlangStartResult + +/// Convert a Gleam actor start result into an Erlang supervisor compatible +/// process start result. +/// +pub fn to_erlang_start_result(res: StartResult(msg)) -> ErlangStartResult { + actor.to_erlang_start_result(res) +} diff --git a/aoc2023/build/packages/gleam_otp/src/gleam/otp/system.gleam b/aoc2023/build/packages/gleam_otp/src/gleam/otp/system.gleam new file mode 100644 index 0000000..c05646b --- /dev/null +++ b/aoc2023/build/packages/gleam_otp/src/gleam/otp/system.gleam @@ -0,0 +1,89 @@ +import gleam/dynamic.{type Dynamic} +import gleam/erlang/atom.{type Atom} +import gleam/erlang/process.{type Pid} + +pub type Mode { + Running + Suspended +} + +pub type DebugOption { + NoDebug +} + +pub type DebugState + +@external(erlang, "sys", "debug_options") +pub fn debug_state(a: List(DebugOption)) -> DebugState + +pub type StatusInfo { + StatusInfo( + module: Atom, + parent: Pid, + mode: Mode, + debug_state: DebugState, + state: Dynamic, + ) +} + +// TODO: document +// TODO: implement remaining messages +pub type SystemMessage { + // {replace_state, StateFn} + // {change_code, Mod, Vsn, Extra} + // {terminate, Reason} + // {debug, {log, Flag}} + // {debug, {trace, Flag}} + // {debug, {log_to_file, FileName}} + // {debug, {statistics, Flag}} + // {debug, no_debug} + // {debug, {install, {Func, FuncState}}} + // {debug, {install, {FuncId, Func, FuncState}}} + // {debug, {remove, FuncOrId}} + Resume(fn() -> Nil) + Suspend(fn() -> Nil) + GetState(fn(Dynamic) -> Nil) + GetStatus(fn(StatusInfo) -> Nil) +} + +type DoNotLeak + +/// Get the state of a given OTP compatible process. This function is only +/// intended for debugging. +/// +/// For more information see the [Erlang documentation][1]. +/// +/// [1]: https://erlang.org/doc/man/sys.html#get_state-1 +/// +@external(erlang, "sys", "get_state") +pub fn get_state(from from: Pid) -> Dynamic + +@external(erlang, "sys", "suspend") +fn erl_suspend(a: Pid) -> DoNotLeak + +/// Request an OTP compatible process to suspend, causing it to only handle +/// system messages. +/// +/// For more information see the [Erlang documentation][1]. +/// +/// [1]: https://erlang.org/doc/man/sys.html#suspend-1 +/// +pub fn suspend(pid: Pid) -> Nil { + erl_suspend(pid) + Nil +} + +@external(erlang, "sys", "resume") +fn erl_resume(from from: Pid) -> DoNotLeak + +/// Request a suspended OTP compatible process to result, causing it to handle +/// all messages rather than only system messages. +/// +/// For more information see the [Erlang documentation][1]. +/// +/// [1]: https://erlang.org/doc/man/sys.html#resume-1 +/// +pub fn resume(pid: Pid) -> Nil { + erl_resume(pid) + Nil +} diff --git a/aoc2023/build/packages/gleam_otp/src/gleam/otp/task.gleam b/aoc2023/build/packages/gleam_otp/src/gleam/otp/task.gleam new file mode 100644 index 0000000..b2b2c5c --- /dev/null +++ b/aoc2023/build/packages/gleam_otp/src/gleam/otp/task.gleam @@ -0,0 +1,151 @@ +//// A task is a kind of process that performs a single task and then shuts +//// down. Commonly tasks are used to convert sequential code into concurrent +//// code by performing computation in another process. +//// +//// let task = task.async(fn() { do_some_work() }) +//// let value = do_some_other_work() +//// value + task.await(task, 100) +//// +//// Tasks spawned with async can be awaited on by their caller process (and +//// only their caller) as shown in the example above. They are implemented by +//// spawning a process that sends a message to the caller once the given +//// computation is performed. +//// +//// There are two important things to consider when using `async`: +//// +//// 1. If you are using async tasks, you must await a reply as they are always +//// sent. +//// +//// 2. async tasks link the caller and the spawned process. This means that, +//// if the caller crashes, the task will crash too and vice-versa. This is +//// on purpose: if the process meant to receive the result no longer +//// exists, there is no purpose in completing the computation. +//// +//// This module is inspired by Elixir's [Task module][1]. +//// +//// [1]: https://hexdocs.pm/elixir/master/Task.html +//// + +// TODO: await_many +import gleam/erlang/process.{type Pid, type ProcessMonitor, type Selector} +import gleam/dynamic.{type Dynamic} + +pub opaque type Task(value) { + Task( + owner: Pid, + pid: Pid, + monitor: ProcessMonitor, + selector: Selector(Message(value)), + ) +} + +// TODO: test +/// Spawn a task process that calls a given function in order to perform some +/// work. The result of this function is send back to the parent and can be +/// received using the `await` function. +/// +/// See the top level module documentation for more information on async/await. +/// +pub fn async(work: fn() -> value) -> Task(value) { + let owner = process.self() + let subject = process.new_subject() + let pid = + process.start(linked: True, running: fn() { process.send(subject, work()) }) + let monitor = process.monitor_process(pid) + let selector = + process.new_selector() + |> process.selecting_process_down(monitor, FromMonitor) + |> process.selecting(subject, FromSubject) + Task(owner: owner, pid: pid, monitor: monitor, selector: selector) +} + +pub type AwaitError { + Timeout + Exit(reason: Dynamic) +} + +// We can only wait on a task if we are the owner of it so crash if we are +// waiting on a task we don't own. +fn assert_owner(task: Task(a)) -> Nil { + let self = process.self() + case task.owner == self { + True -> Nil + False -> + process.send_abnormal_exit( + self, + "awaited on a task that does not belong to this process", + ) + } +} + +type Message(value) { + FromMonitor(process.ProcessDown) + FromSubject(value) +} + +// TODO: test +/// Wait for the value computed by a task. +/// +/// If the a value is not received before the timeout has elapsed or if the +/// task process crashes then an error is returned. +/// +pub fn try_await(task: Task(value), timeout: Int) -> Result(value, AwaitError) { + assert_owner(task) + case process.select(task.selector, timeout) { + // The task process has sent back a value + Ok(FromSubject(x)) -> { + process.demonitor_process(task.monitor) + Ok(x) + } + + // The task process crashed without sending a value + Ok(FromMonitor(process.ProcessDown(reason: reason, ..))) -> + Error(Exit(reason)) + + // The task process is alive but has not sent a value yet + Error(Nil) -> Error(Timeout) + } +} + +// TODO: test +/// Wait for the value computed by a task. +/// +/// If the a value is not received before the timeout has elapsed or if the +/// task process crashes then this function crashes. +/// +pub fn await(task: Task(value), timeout: Int) -> value { + let assert Ok(value) = try_await(task, timeout) + value +} + +/// Wait endlessly for the value computed by a task. +/// +/// Be Careful! This function does not return until there is a value to +/// receive. If a value is not received then the process will be stuck waiting +/// forever. +/// +pub fn try_await_forever(task: Task(value)) -> Result(value, AwaitError) { + assert_owner(task) + case process.select_forever(task.selector) { + // The task process has sent back a value + FromSubject(x) -> { + process.demonitor_process(task.monitor) + Ok(x) + } + + // The task process crashed without sending a value + FromMonitor(process.ProcessDown(reason: reason, ..)) -> Error(Exit(reason)) + } +} + +/// Wait endlessly for the value computed by a task. +/// +/// Be Careful! Like `try_await_forever`, this function does not return until there is a value to +/// receive. +/// +/// If the task process crashes then this function crashes. +/// +pub fn await_forever(task: Task(value)) -> value { + let assert Ok(value) = try_await_forever(task) + value +} diff --git a/aoc2023/build/packages/gleam_otp/src/gleam@otp@actor.erl b/aoc2023/build/packages/gleam_otp/src/gleam@otp@actor.erl new file mode 100644 index 0000000..0606147 --- /dev/null +++ b/aoc2023/build/packages/gleam_otp/src/gleam@otp@actor.erl @@ -0,0 +1,273 @@ +-module(gleam@otp@actor). +-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function]). + +-export([continue/1, with_selector/2, to_erlang_start_result/1, start_spec/1, start/2, send/2, call/3]). +-export_type([message/1, next/2, init_result/2, self/2, spec/2, start_error/0, start_init_message/1]). + +-type message(GAS) :: {message, GAS} | + {system, gleam@otp@system:system_message()} | + {unexpected, gleam@dynamic:dynamic_()}. + +-type next(GAT, GAU) :: {continue, + GAU, + gleam@option:option(gleam@erlang@process:selector(GAT))} | + {stop, gleam@erlang@process:exit_reason()}. + +-type init_result(GAV, GAW) :: {ready, GAV, gleam@erlang@process:selector(GAW)} | + {failed, binary()}. + +-type self(GAX, GAY) :: {self, + gleam@otp@system:mode(), + gleam@erlang@process:pid_(), + GAX, + gleam@erlang@process:subject(GAY), + gleam@erlang@process:selector(message(GAY)), + gleam@otp@system:debug_state(), + fun((GAY, GAX) -> next(GAY, GAX))}. + +-type spec(GAZ, GBA) :: {spec, + fun(() -> init_result(GAZ, GBA)), + integer(), + fun((GBA, GAZ) -> next(GBA, GAZ))}. + +-type start_error() :: init_timeout | + {init_failed, gleam@erlang@process:exit_reason()} | + {init_crashed, gleam@dynamic:dynamic_()}. + +-type start_init_message(GBB) :: {ack, + {ok, gleam@erlang@process:subject(GBB)} | + {error, gleam@erlang@process:exit_reason()}} | + {mon, gleam@erlang@process:process_down()}. + +-spec continue(GBI) -> next(any(), GBI). +continue(State) -> + {continue, State, none}. + +-spec with_selector(next(GBM, GBN), gleam@erlang@process:selector(GBM)) -> next(GBM, GBN). +with_selector(Value, Selector) -> + case Value of + {continue, State, _} -> + {continue, State, {some, Selector}}; + + _ -> + Value + end. + +-spec exit_process(gleam@erlang@process:exit_reason()) -> gleam@erlang@process:exit_reason(). +exit_process(Reason) -> + Reason. + +-spec selecting_system_messages(gleam@erlang@process:selector(message(GBY))) -> gleam@erlang@process:selector(message(GBY)). +selecting_system_messages(Selector) -> + _pipe = Selector, + gleam@erlang@process:selecting_record3( + _pipe, + erlang:binary_to_atom(<<"system"/utf8>>), + fun gleam_otp_external:convert_system_message/2 + ). + +-spec receive_message(self(any(), GBU)) -> message(GBU). +receive_message(Self) -> + Selector = case erlang:element(2, Self) of + suspended -> + _pipe = gleam_erlang_ffi:new_selector(), + selecting_system_messages(_pipe); + + running -> + _pipe@1 = gleam_erlang_ffi:new_selector(), + _pipe@2 = gleam@erlang@process:selecting_anything( + _pipe@1, + fun(Field@0) -> {unexpected, Field@0} end + ), + _pipe@3 = gleam_erlang_ffi:merge_selector( + _pipe@2, + erlang:element(6, Self) + ), + selecting_system_messages(_pipe@3) + end, + gleam_erlang_ffi:select(Selector). + +-spec process_status_info(self(any(), any())) -> gleam@otp@system:status_info(). +process_status_info(Self) -> + {status_info, + erlang:binary_to_atom(<<"gleam@otp@actor"/utf8>>), + erlang:element(3, Self), + erlang:element(2, Self), + erlang:element(7, Self), + gleam@dynamic:from(erlang:element(4, Self))}. + +-spec init_selector( + gleam@erlang@process:subject(GGN), + gleam@erlang@process:selector(GGN) +) -> gleam@erlang@process:selector(message(GGN)). +init_selector(Subject, Selector) -> + _pipe = gleam_erlang_ffi:new_selector(), + _pipe@1 = gleam@erlang@process:selecting( + _pipe, + Subject, + fun(Field@0) -> {message, Field@0} end + ), + gleam_erlang_ffi:merge_selector( + _pipe@1, + gleam_erlang_ffi:map_selector( + Selector, + fun(Field@0) -> {message, Field@0} end + ) + ). + +-spec loop(self(any(), any())) -> gleam@erlang@process:exit_reason(). +loop(Self) -> + case receive_message(Self) of + {system, System} -> + case System of + {get_state, Callback} -> + Callback(gleam@dynamic:from(erlang:element(4, Self))), + loop(Self); + + {resume, Callback@1} -> + Callback@1(), + loop(erlang:setelement(2, Self, running)); + + {suspend, Callback@2} -> + Callback@2(), + loop(erlang:setelement(2, Self, suspended)); + + {get_status, Callback@3} -> + Callback@3(process_status_info(Self)), + loop(Self) + end; + + {unexpected, Message} -> + logger:warning( + unicode:characters_to_list( + <<"Actor discarding unexpected message: ~s"/utf8>> + ), + [unicode:characters_to_list(gleam@string:inspect(Message))] + ), + loop(Self); + + {message, Msg} -> + case (erlang:element(8, Self))(Msg, erlang:element(4, Self)) of + {stop, Reason} -> + exit_process(Reason); + + {continue, State, New_selector} -> + Selector = begin + _pipe = New_selector, + _pipe@1 = gleam@option:map( + _pipe, + fun(_capture) -> + init_selector(erlang:element(5, Self), _capture) + end + ), + gleam@option:unwrap(_pipe@1, erlang:element(6, Self)) + end, + loop( + erlang:setelement( + 6, + erlang:setelement(4, Self, State), + Selector + ) + ) + end + end. + +-spec initialise_actor( + spec(any(), GCP), + gleam@erlang@process:subject({ok, gleam@erlang@process:subject(GCP)} | + {error, gleam@erlang@process:exit_reason()}) +) -> gleam@erlang@process:exit_reason(). +initialise_actor(Spec, Ack) -> + Subject = gleam@erlang@process:new_subject(), + case (erlang:element(2, Spec))() of + {ready, State, Selector} -> + Selector@1 = init_selector(Subject, Selector), + gleam@erlang@process:send(Ack, {ok, Subject}), + Self = {self, + running, + gleam@erlang@process:subject_owner(Ack), + State, + Subject, + Selector@1, + sys:debug_options([]), + erlang:element(4, Spec)}, + loop(Self); + + {failed, Reason} -> + gleam@erlang@process:send(Ack, {error, {abnormal, Reason}}), + exit_process({abnormal, Reason}) + end. + +-spec to_erlang_start_result( + {ok, gleam@erlang@process:subject(any())} | {error, start_error()} +) -> {ok, gleam@erlang@process:pid_()} | {error, gleam@dynamic:dynamic_()}. +to_erlang_start_result(Res) -> + case Res of + {ok, X} -> + {ok, gleam@erlang@process:subject_owner(X)}; + + {error, X@1} -> + {error, gleam@dynamic:from(X@1)} + end. + +-spec start_spec(spec(any(), GDD)) -> {ok, gleam@erlang@process:subject(GDD)} | + {error, start_error()}. +start_spec(Spec) -> + Ack_subject = gleam@erlang@process:new_subject(), + Child = gleam@erlang@process:start( + fun() -> initialise_actor(Spec, Ack_subject) end, + true + ), + Monitor = gleam@erlang@process:monitor_process(Child), + Selector = begin + _pipe = gleam_erlang_ffi:new_selector(), + _pipe@1 = gleam@erlang@process:selecting( + _pipe, + Ack_subject, + fun(Field@0) -> {ack, Field@0} end + ), + gleam@erlang@process:selecting_process_down( + _pipe@1, + Monitor, + fun(Field@0) -> {mon, Field@0} end + ) + end, + Result = case gleam_erlang_ffi:select(Selector, erlang:element(3, Spec)) of + {ok, {ack, {ok, Channel}}} -> + {ok, Channel}; + + {ok, {ack, {error, Reason}}} -> + {error, {init_failed, Reason}}; + + {ok, {mon, Down}} -> + {error, {init_crashed, erlang:element(3, Down)}}; + + {error, nil} -> + gleam@erlang@process:kill(Child), + {error, init_timeout} + end, + gleam_erlang_ffi:demonitor(Monitor), + Result. + +-spec start(GDJ, fun((GDK, GDJ) -> next(GDK, GDJ))) -> {ok, + gleam@erlang@process:subject(GDK)} | + {error, start_error()}. +start(State, Loop) -> + start_spec( + {spec, + fun() -> {ready, State, gleam_erlang_ffi:new_selector()} end, + 5000, + Loop} + ). + +-spec send(gleam@erlang@process:subject(GDQ), GDQ) -> nil. +send(Subject, Msg) -> + gleam@erlang@process:send(Subject, Msg). + +-spec call( + gleam@erlang@process:subject(GDS), + fun((gleam@erlang@process:subject(GDU)) -> GDS), + integer() +) -> GDU. +call(Selector, Make_message, Timeout) -> + gleam@erlang@process:call(Selector, Make_message, Timeout). diff --git a/aoc2023/build/packages/gleam_otp/src/gleam@otp@intensity_tracker.erl b/aoc2023/build/packages/gleam_otp/src/gleam@otp@intensity_tracker.erl new file mode 100644 index 0000000..8792f14 --- /dev/null +++ b/aoc2023/build/packages/gleam_otp/src/gleam@otp@intensity_tracker.erl @@ -0,0 +1,53 @@ +-module(gleam@otp@intensity_tracker). +-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function]). + +-export([new/2, trim_window/3, add_event/1]). +-export_type([intensity_tracker/0, too_intense/0]). + +-opaque intensity_tracker() :: {intensity_tracker, + integer(), + integer(), + list(integer())}. + +-type too_intense() :: too_intense. + +-spec new(integer(), integer()) -> intensity_tracker(). +new(Limit, Period) -> + {intensity_tracker, Limit, Period, []}. + +-spec now_seconds() -> integer(). +now_seconds() -> + erlang:monotonic_time(1). + +-spec trim_window(list(integer()), integer(), integer()) -> list(integer()). +trim_window(Events, Now, Period) -> + case Events of + [] -> + []; + + [Event | Events@1] -> + case Now >= (Event + Period) of + true -> + [Event | trim_window(Events@1, Now, Period)]; + + false -> + [] + end + end. + +-spec add_event(intensity_tracker()) -> {ok, intensity_tracker()} | + {error, too_intense()}. +add_event(Tracker) -> + Now = now_seconds(), + Events = trim_window( + [Now | erlang:element(4, Tracker)], + Now, + erlang:element(3, Tracker) + ), + case gleam@list:length(Events) >= erlang:element(2, Tracker) of + true -> + {error, too_intense}; + + false -> + {ok, erlang:setelement(4, Tracker, Events)} + end. diff --git a/aoc2023/build/packages/gleam_otp/src/gleam@otp@port.erl b/aoc2023/build/packages/gleam_otp/src/gleam@otp@port.erl new file mode 100644 index 0000000..b205739 --- /dev/null +++ b/aoc2023/build/packages/gleam_otp/src/gleam@otp@port.erl @@ -0,0 +1,8 @@ +-module(gleam@otp@port). +-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function]). + +-export_type([port_/0]). + +-type port_() :: any(). + + diff --git a/aoc2023/build/packages/gleam_otp/src/gleam@otp@supervisor.erl b/aoc2023/build/packages/gleam_otp/src/gleam@otp@supervisor.erl new file mode 100644 index 0000000..39118f1 --- /dev/null +++ b/aoc2023/build/packages/gleam_otp/src/gleam@otp@supervisor.erl @@ -0,0 +1,322 @@ +-module(gleam@otp@supervisor). +-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function]). + +-export([add/2, supervisor/1, worker/1, returning/2, start_spec/1, start/1, application_stopped/0, to_erlang_start_result/1]). +-export_type([spec/2, children/1, child_spec/3, child_start_error/0, message/0, instruction/0, state/1, starter/1, child/1, handle_exit_error/0, application_start_mode/0, application_stop/0]). + +-type spec(GLS, GLT) :: {spec, + GLS, + integer(), + integer(), + fun((children(GLS)) -> children(GLT))}. + +-opaque children(GLU) :: {ready, starter(GLU)} | {failed, child_start_error()}. + +-opaque child_spec(GLV, GLW, GLX) :: {child_spec, + fun((GLW) -> {ok, gleam@erlang@process:subject(GLV)} | + {error, gleam@otp@actor:start_error()}), + fun((GLW, gleam@erlang@process:subject(GLV)) -> GLX)}. + +-type child_start_error() :: {child_start_error, + gleam@option:option(gleam@erlang@process:pid_()), + gleam@otp@actor:start_error()}. + +-opaque message() :: {exit, gleam@erlang@process:exit_message()} | + {retry_restart, gleam@erlang@process:pid_()}. + +-type instruction() :: start_all | {start_from, gleam@erlang@process:pid_()}. + +-type state(GLY) :: {state, + gleam@otp@intensity_tracker:intensity_tracker(), + starter(GLY), + gleam@erlang@process:subject(gleam@erlang@process:pid_())}. + +-type starter(GLZ) :: {starter, + GLZ, + gleam@option:option(fun((instruction()) -> {ok, + {starter(GLZ), instruction()}} | + {error, child_start_error()}))}. + +-type child(GMA) :: {child, gleam@erlang@process:pid_(), GMA}. + +-type handle_exit_error() :: {restart_failed, + gleam@erlang@process:pid_(), + gleam@otp@intensity_tracker:intensity_tracker()} | + too_many_restarts. + +-type application_start_mode() :: normal | + {takeover, gleam@erlang@node:node_()} | + {failover, gleam@erlang@node:node_()}. + +-type application_stop() :: any(). + +-spec start_child(child_spec(any(), GME, GMF), GME) -> {ok, child(GMF)} | + {error, child_start_error()}. +start_child(Child_spec, Argument) -> + gleam@result:then( + begin + _pipe = (erlang:element(2, Child_spec))(Argument), + gleam@result:map_error( + _pipe, + fun(_capture) -> {child_start_error, none, _capture} end + ) + end, + fun(Subject) -> + {ok, + {child, + gleam@erlang@process:subject_owner(Subject), + (erlang:element(3, Child_spec))(Argument, Subject)}} + end + ). + +-spec shutdown_child( + gleam@erlang@process:pid_(), + child_spec(any(), any(), any()) +) -> nil. +shutdown_child(Pid, _) -> + gleam@erlang@process:send_exit(Pid). + +-spec perform_instruction_for_child( + GMS, + instruction(), + child_spec(any(), GMS, GMU), + child(GMU) +) -> {ok, {child(GMU), instruction()}} | {error, child_start_error()}. +perform_instruction_for_child(Argument, Instruction, Child_spec, Child) -> + Current = erlang:element(2, Child), + case Instruction of + {start_from, Target} when Target =/= Current -> + {ok, {Child, Instruction}}; + + _ -> + shutdown_child(Current, Child_spec), + gleam@result:then( + start_child(Child_spec, Argument), + fun(Child@1) -> {ok, {Child@1, start_all}} end + ) + end. + +-spec add_child_to_starter( + starter(GNC), + child_spec(any(), GNC, GNF), + child(GNF) +) -> starter(GNF). +add_child_to_starter(Starter, Child_spec, Child) -> + Starter@3 = fun(Instruction) -> + gleam@result:then(case erlang:element(3, Starter) of + {some, Start} -> + Start(Instruction); + + none -> + {ok, {Starter, Instruction}} + end, fun(_use0) -> + {Starter@1, Instruction@1} = _use0, + gleam@result:then( + perform_instruction_for_child( + erlang:element(2, Starter@1), + Instruction@1, + Child_spec, + Child + ), + fun(_use0@1) -> + {Child@1, Instruction@2} = _use0@1, + Starter@2 = add_child_to_starter( + Starter@1, + Child_spec, + Child@1 + ), + {ok, {Starter@2, Instruction@2}} + end + ) + end) + end, + {starter, erlang:element(3, Child), {some, Starter@3}}. + +-spec start_and_add_child(starter(GNL), child_spec(any(), GNL, GNO)) -> children(GNO). +start_and_add_child(State, Child_spec) -> + case start_child(Child_spec, erlang:element(2, State)) of + {ok, Child} -> + {ready, add_child_to_starter(State, Child_spec, Child)}; + + {error, Reason} -> + {failed, Reason} + end. + +-spec add(children(GNT), child_spec(any(), GNT, GNW)) -> children(GNW). +add(Children, Child_spec) -> + case Children of + {failed, Fail} -> + {failed, Fail}; + + {ready, State} -> + start_and_add_child(State, Child_spec) + end. + +-spec supervisor( + fun((GOB) -> {ok, gleam@erlang@process:subject(GOC)} | + {error, gleam@otp@actor:start_error()}) +) -> child_spec(GOC, GOB, GOB). +supervisor(Start) -> + {child_spec, Start, fun(Argument, _) -> Argument end}. + +-spec worker( + fun((GOJ) -> {ok, gleam@erlang@process:subject(GOK)} | + {error, gleam@otp@actor:start_error()}) +) -> child_spec(GOK, GOJ, GOJ). +worker(Start) -> + {child_spec, Start, fun(Argument, _) -> Argument end}. + +-spec returning( + child_spec(GOR, GOS, any()), + fun((GOS, gleam@erlang@process:subject(GOR)) -> GOY) +) -> child_spec(GOR, GOS, GOY). +returning(Child, Updater) -> + {child_spec, erlang:element(2, Child), Updater}. + +-spec init(spec(any(), GPD)) -> gleam@otp@actor:init_result(state(GPD), message()). +init(Spec) -> + Retry = gleam@erlang@process:new_subject(), + gleam_erlang_ffi:trap_exits(true), + Selector = begin + _pipe = gleam_erlang_ffi:new_selector(), + _pipe@1 = gleam@erlang@process:selecting( + _pipe, + Retry, + fun(Field@0) -> {retry_restart, Field@0} end + ), + gleam@erlang@process:selecting_trapped_exits( + _pipe@1, + fun(Field@0) -> {exit, Field@0} end + ) + end, + Result = begin + _pipe@2 = {starter, erlang:element(2, Spec), none}, + _pipe@3 = {ready, _pipe@2}, + (erlang:element(5, Spec))(_pipe@3) + end, + case Result of + {ready, Starter} -> + Restarts = gleam@otp@intensity_tracker:new( + erlang:element(3, Spec), + erlang:element(4, Spec) + ), + State = {state, Restarts, Starter, Retry}, + {ready, State, Selector}; + + {failed, Error} -> + {failed, case erlang:element(3, Error) of + init_timeout -> + <<"Child initialisation timed out"/utf8>>; + + {init_crashed, Reason} -> + gleam@string:append( + <<"Child crashed during initialisation: "/utf8>>, + gleam@string:inspect(Reason) + ); + + {init_failed, Reason@1} -> + gleam@string:append( + <<"Child failed to start during initialisation: "/utf8>>, + gleam@string:inspect(Reason@1) + ) + end} + end. + +-spec handle_exit(gleam@erlang@process:pid_(), state(GPJ)) -> gleam@otp@actor:next(message(), state(GPJ)). +handle_exit(Pid, State) -> + Outcome = begin + _assert_subject = erlang:element(3, erlang:element(3, State)), + {some, Start} = case _assert_subject of + {some, _} -> _assert_subject; + _assert_fail -> + erlang:error(#{gleam_error => let_assert, + message => <<"Assertion pattern match failed"/utf8>>, + value => _assert_fail, + module => <<"gleam/otp/supervisor"/utf8>>, + function => <<"handle_exit"/utf8>>, + line => 293}) + end, + gleam@result:then( + begin + _pipe = erlang:element(2, State), + _pipe@1 = gleam@otp@intensity_tracker:add_event(_pipe), + gleam@result:map_error(_pipe@1, fun(_) -> too_many_restarts end) + end, + fun(Restarts) -> + gleam@result:then( + begin + _pipe@2 = Start({start_from, Pid}), + gleam@result:map_error( + _pipe@2, + fun(E) -> + {restart_failed, + gleam@option:unwrap( + erlang:element(2, E), + Pid + ), + Restarts} + end + ) + end, + fun(_use0) -> + {Starter, _} = _use0, + {ok, + erlang:setelement( + 2, + erlang:setelement(3, State, Starter), + Restarts + )} + end + ) + end + ) + end, + case Outcome of + {ok, State@1} -> + gleam@otp@actor:continue(State@1); + + {error, {restart_failed, Failed_child, Restarts@1}} -> + gleam@erlang@process:send(erlang:element(4, State), Failed_child), + State@2 = erlang:setelement(2, State, Restarts@1), + gleam@otp@actor:continue(State@2); + + {error, too_many_restarts} -> + {stop, + {abnormal, + <<"Child processes restarted too many times within allowed period"/utf8>>}} + end. + +-spec loop(message(), state(GPO)) -> gleam@otp@actor:next(message(), state(GPO)). +loop(Message, State) -> + case Message of + {exit, Exit_message} -> + handle_exit(erlang:element(2, Exit_message), State); + + {retry_restart, Pid} -> + handle_exit(Pid, State) + end. + +-spec start_spec(spec(any(), any())) -> {ok, + gleam@erlang@process:subject(message())} | + {error, gleam@otp@actor:start_error()}. +start_spec(Spec) -> + gleam@otp@actor:start_spec( + {spec, fun() -> init(Spec) end, 60000, fun loop/2} + ). + +-spec start(fun((children(nil)) -> children(any()))) -> {ok, + gleam@erlang@process:subject(message())} | + {error, gleam@otp@actor:start_error()}. +start(Init) -> + start_spec({spec, nil, 1, 5, Init}). + +-spec application_stopped() -> application_stop(). +application_stopped() -> + gleam_otp_external:application_stopped(). + +-spec to_erlang_start_result( + {ok, gleam@erlang@process:subject(any())} | + {error, gleam@otp@actor:start_error()} +) -> {ok, gleam@erlang@process:pid_()} | {error, gleam@dynamic:dynamic_()}. +to_erlang_start_result(Res) -> + gleam@otp@actor:to_erlang_start_result(Res). diff --git a/aoc2023/build/packages/gleam_otp/src/gleam@otp@system.erl b/aoc2023/build/packages/gleam_otp/src/gleam@otp@system.erl new file mode 100644 index 0000000..622e5ea --- /dev/null +++ b/aoc2023/build/packages/gleam_otp/src/gleam@otp@system.erl @@ -0,0 +1,43 @@ +-module(gleam@otp@system). +-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function]). + +-export([debug_state/1, get_state/1, suspend/1, resume/1]). +-export_type([mode/0, debug_option/0, debug_state/0, status_info/0, system_message/0, do_not_leak/0]). + +-type mode() :: running | suspended. + +-type debug_option() :: no_debug. + +-type debug_state() :: any(). + +-type status_info() :: {status_info, + gleam@erlang@atom:atom_(), + gleam@erlang@process:pid_(), + mode(), + debug_state(), + gleam@dynamic:dynamic_()}. + +-type system_message() :: {resume, fun(() -> nil)} | + {suspend, fun(() -> nil)} | + {get_state, fun((gleam@dynamic:dynamic_()) -> nil)} | + {get_status, fun((status_info()) -> nil)}. + +-type do_not_leak() :: any(). + +-spec debug_state(list(debug_option())) -> debug_state(). +debug_state(A) -> + sys:debug_options(A). + +-spec get_state(gleam@erlang@process:pid_()) -> gleam@dynamic:dynamic_(). +get_state(From) -> + sys:get_state(From). + +-spec suspend(gleam@erlang@process:pid_()) -> nil. +suspend(Pid) -> + sys:suspend(Pid), + nil. + +-spec resume(gleam@erlang@process:pid_()) -> nil. +resume(Pid) -> + sys:resume(Pid), + nil. diff --git a/aoc2023/build/packages/gleam_otp/src/gleam@otp@task.erl b/aoc2023/build/packages/gleam_otp/src/gleam@otp@task.erl new file mode 100644 index 0000000..e004284 --- /dev/null +++ b/aoc2023/build/packages/gleam_otp/src/gleam@otp@task.erl @@ -0,0 +1,111 @@ +-module(gleam@otp@task). +-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function]). + +-export([async/1, try_await/2, await/2, try_await_forever/1, await_forever/1]). +-export_type([task/1, await_error/0, message/1]). + +-opaque task(FWJ) :: {task, + gleam@erlang@process:pid_(), + gleam@erlang@process:pid_(), + gleam@erlang@process:process_monitor(), + gleam@erlang@process:selector(message(FWJ))}. + +-type await_error() :: timeout | {exit, gleam@dynamic:dynamic_()}. + +-type message(FWK) :: {from_monitor, gleam@erlang@process:process_down()} | + {from_subject, FWK}. + +-spec async(fun(() -> FWL)) -> task(FWL). +async(Work) -> + Owner = erlang:self(), + Subject = gleam@erlang@process:new_subject(), + Pid = gleam@erlang@process:start( + fun() -> gleam@erlang@process:send(Subject, Work()) end, + true + ), + Monitor = gleam@erlang@process:monitor_process(Pid), + Selector = begin + _pipe = gleam_erlang_ffi:new_selector(), + _pipe@1 = gleam@erlang@process:selecting_process_down( + _pipe, + Monitor, + fun(Field@0) -> {from_monitor, Field@0} end + ), + gleam@erlang@process:selecting( + _pipe@1, + Subject, + fun(Field@0) -> {from_subject, Field@0} end + ) + end, + {task, Owner, Pid, Monitor, Selector}. + +-spec assert_owner(task(any())) -> nil. +assert_owner(Task) -> + Self = erlang:self(), + case erlang:element(2, Task) =:= Self of + true -> + nil; + + false -> + gleam@erlang@process:send_abnormal_exit( + Self, + <<"awaited on a task that does not belong to this process"/utf8>> + ) + end. + +-spec try_await(task(FWP), integer()) -> {ok, FWP} | {error, await_error()}. +try_await(Task, Timeout) -> + assert_owner(Task), + case gleam_erlang_ffi:select(erlang:element(5, Task), Timeout) of + {ok, {from_subject, X}} -> + gleam_erlang_ffi:demonitor(erlang:element(4, Task)), + {ok, X}; + + {ok, {from_monitor, {process_down, _, Reason}}} -> + {error, {exit, Reason}}; + + {error, nil} -> + {error, timeout} + end. + +-spec await(task(FWT), integer()) -> FWT. +await(Task, Timeout) -> + _assert_subject = try_await(Task, Timeout), + {ok, Value} = case _assert_subject of + {ok, _} -> _assert_subject; + _assert_fail -> + erlang:error(#{gleam_error => let_assert, + message => <<"Assertion pattern match failed"/utf8>>, + value => _assert_fail, + module => <<"gleam/otp/task"/utf8>>, + function => <<"await"/utf8>>, + line => 117}) + end, + Value. + +-spec try_await_forever(task(FWV)) -> {ok, FWV} | {error, await_error()}. +try_await_forever(Task) -> + assert_owner(Task), + case gleam_erlang_ffi:select(erlang:element(5, Task)) of + {from_subject, X} -> + gleam_erlang_ffi:demonitor(erlang:element(4, Task)), + {ok, X}; + + {from_monitor, {process_down, _, Reason}} -> + {error, {exit, Reason}} + end. + +-spec await_forever(task(FWZ)) -> FWZ. +await_forever(Task) -> + _assert_subject = try_await_forever(Task), + {ok, Value} = case _assert_subject of + {ok, _} -> _assert_subject; + _assert_fail -> + erlang:error(#{gleam_error => let_assert, + message => <<"Assertion pattern match failed"/utf8>>, + value => _assert_fail, + module => <<"gleam/otp/task"/utf8>>, + function => <<"await_forever"/utf8>>, + line => 149}) + end, + Value. diff --git a/aoc2023/build/packages/gleam_otp/src/gleam_otp.app.src b/aoc2023/build/packages/gleam_otp/src/gleam_otp.app.src new file mode 100644 index 0000000..5c52295 --- /dev/null +++ b/aoc2023/build/packages/gleam_otp/src/gleam_otp.app.src @@ -0,0 +1,15 @@ +{application, gleam_otp, [ + {vsn, "0.8.0"}, + {applications, [gleam_erlang, + gleam_stdlib, + gleeunit]}, + {description, "Fault tolerant multicore Gleam programs with OTP"}, + {modules, [gleam@otp@actor, + gleam@otp@intensity_tracker, + gleam@otp@port, + gleam@otp@supervisor, + gleam@otp@system, + gleam@otp@task, + gleam_otp]}, + {registered, []} +]}. diff --git a/aoc2023/build/packages/gleam_otp/src/gleam_otp.erl b/aoc2023/build/packages/gleam_otp/src/gleam_otp.erl new file mode 100644 index 0000000..9381ad2 --- /dev/null +++ b/aoc2023/build/packages/gleam_otp/src/gleam_otp.erl @@ -0,0 +1,28 @@ +-module(gleam_otp). +-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function]). + +-export([main/0]). + +-spec spawn_task(integer()) -> gleam@otp@task:task(nil). +spawn_task(I) -> + gleam@otp@task:async(fun() -> case (I rem 500) =:= 0 of + true -> + gleam@io:println( + <<"Hello from "/utf8, (gleam@int:to_string(I))/binary>> + ); + + false -> + nil + end end). + +-spec main() -> integer(). +main() -> + gleam@io:debug( + gleam_otp_test_external:get_message_queue_length(erlang:self()) + ), + _pipe = gleam@list:range(0, 1000000), + _pipe@1 = gleam@list:map(_pipe, fun spawn_task/1), + gleam@list:each(_pipe@1, fun gleam@otp@task:await_forever/1), + gleam@io:debug( + gleam_otp_test_external:get_message_queue_length(erlang:self()) + ). diff --git a/aoc2023/build/packages/gleam_otp/src/gleam_otp.gleam b/aoc2023/build/packages/gleam_otp/src/gleam_otp.gleam new file mode 100644 index 0000000..69cdd5b --- /dev/null +++ b/aoc2023/build/packages/gleam_otp/src/gleam_otp.gleam @@ -0,0 +1,27 @@ +import gleam/io +import gleam/int +import gleam/list +import gleam/otp/task +import gleam/erlang/process.{type Pid} + +@external(erlang, "gleam_otp_test_external", "get_message_queue_length") +fn get_message_queue_length(pid pid: Pid) -> Int + +fn spawn_task(i) { + task.async(fn() { + case i % 500 == 0 { + True -> io.println("Hello from " <> int.to_string(i)) + False -> Nil + } + }) +} + +pub fn main() { + io.debug(get_message_queue_length(process.self())) + + list.range(0, 1_000_000) + |> list.map(spawn_task) + |> list.each(task.await_forever) + + io.debug(get_message_queue_length(process.self())) +} diff --git a/aoc2023/build/packages/gleam_otp/src/gleam_otp_external.erl b/aoc2023/build/packages/gleam_otp/src/gleam_otp_external.erl new file mode 100644 index 0000000..8910a67 --- /dev/null +++ b/aoc2023/build/packages/gleam_otp/src/gleam_otp_external.erl @@ -0,0 +1,43 @@ +-module(gleam_otp_external). + +-export([application_stopped/0, convert_system_message/2]). + +% TODO: support other system messages +% {replace_state, StateFn} +% {change_code, Mod, Vsn, Extra} +% {terminate, Reason} +% {debug, {log, Flag}} +% {debug, {trace, Flag}} +% {debug, {log_to_file, FileName}} +% {debug, {statistics, Flag}} +% {debug, no_debug} +% {debug, {install, {Func, FuncState}}} +% {debug, {install, {FuncId, Func, FuncState}}} +% {debug, {remove, FuncOrId}} +% GetStatus(Subject(StatusInfo)) +convert_system_message({From, Ref}, Request) when is_pid(From) -> + Reply = fun(Msg) -> + erlang:send(From, {Ref, Msg}), + nil + end, + System = fun(Callback) -> + {system, {Request, Callback}} + end, + case Request of + get_status -> System(fun(Status) -> Reply(process_status(Status)) end); + get_state -> System(Reply); + suspend -> System(fun() -> Reply(ok) end); + resume -> System(fun() -> Reply(ok) end); + Other -> {unexpeceted, Other} + end. + +process_status({status_info, Module, Parent, Mode, DebugState, State}) -> + Data = [ + get(), Mode, Parent, DebugState, + [{header, "Status for Gleam process " ++ pid_to_list(self())}, + {data, [{'Status', Mode}, {'Parent', Parent}, {'State', State}]}] + ], + {status, self(), {module, Module}, Data}. + +application_stopped() -> + ok. -- cgit v1.2.3