├── .github
└── FUNDING.yml
├── LICENSE
├── README.md
└── chapters
├── ch_0.0_start.livemd
├── ch_1.1_concurrency_in_elixir.livemd
├── ch_1.2_immutability_and_memory_management.livemd
├── ch_2.1_process_internals.livemd
├── ch_2.2_process_basics.livemd
├── ch_2.3_process_linking.livemd
├── ch_2.4_process_monitoring_and_hibernation.livemd
├── ch_2.5_group_leaders_and_process_naming.livemd
├── ch_3.1_genserver_introduction.livemd
├── ch_3.2_building_a_genserver.livemd
├── ch_3.3_genserver_examples.livemd
├── ch_3.4_other_genserver_functions.livemd
├── ch_4.0_the_registry_module.livemd
├── ch_5.1_supervisors_introduction.livemd
├── ch_5.2_supervision_strategies.livemd
├── ch_5.3_restart_strategies.livemd
├── ch_5.4_introduction_to_dynamic_supervisor.livemd
├── ch_5.5_partition_supervisor.ex.livemd
├── ch_5.6_scaling_dynamic_supervisor.livemd
├── ch_6.0_project_building_a_download_manager.livemd
├── ch_7.1_intro_to_tasks.livemd
├── ch_7.2_awaiting_tasks.livemd
├── ch_7.3_task_async_stream.livemd
├── ch_7.4_supervised_tasks.livemd
├── ch_8.0_agents.livemd
├── ch_9.0_gotchas.livemd
├── images
├── beam_scheduling_architecture.svg
├── concurrent_vs_parallel.svg
├── download_manager_architecture.png
├── genserver_lifecycle.svg
├── one_for_all.png
├── one_for_one.png
└── rest_for_one.png
└── sample_data
└── top_websites.csv
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | # These are supported funding model platforms
2 |
3 | github: Arp-G
4 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 Async Elixir
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Async Elixir 🔮
2 |
3 | Welcome to the **Async Elixir** book repository!
4 |
5 | The **Async Elixir** book is a deep dive into Elixir's concurrency features. If you're already comfortable with Elixir basics and eager to explore concurrent programming and process management, you're in the right place.
6 |
7 | [](https://livebook.dev/run?url=https%3A%2F%2Fgithub.com%2FArp-G%2Fasync-elixir%2Fblob%2Fmaster%2Fchapters%2Fch_0.0_start.livemd)
8 |
9 | ## Getting Started
10 |
11 | * Clone this repository
12 | * Ensure you have [Livebook](https://livebook.dev/) installed
13 | * Open livebook and then open the [Course Overview](chapters/ch_0.0_start.livemd) file in Livebook.
14 |
15 | ## Contributions
16 |
17 | If you encounter any issues, find typos, or have valuable suggestions to improve the course content, don't hesitate to create an issue in this repository. Your contributions are highly appreciated!
18 |
--------------------------------------------------------------------------------
/chapters/ch_0.0_start.livemd:
--------------------------------------------------------------------------------
1 | # Async Elixir
2 |
3 | ## Overview
4 |
5 | #### Welcome to Async Elixir
6 |
7 | Welcome to the **Async Elixir** course, a comprehensive exploration of Elixir's advanced concurrency features. This course is tailored for individuals with a foundational understanding of Elixir programming and a desire to elevate their expertise to the next level.
8 |
9 | #### Course Overview
10 |
11 | This course is your gateway to becoming proficient in Elixir's asynchronous capabilities. Whether you're an experienced developer expanding your skill set or a curious learner fascinated by concurrent programming, you'll gain a deep understanding of Elixir's powerful asynchronous features.
12 |
13 | By the conclusion of this course, you will have developed a robust understanding of processes, OTP patterns, and the Elixir standard library's utilization for effective process management. You'll be well-equipped to apply these concepts confidently in real-world situations.
14 |
15 | #### Key Topics Covered
16 |
17 | * **In-Depth Process and Concurrency:** Explore the intricate workings of Elixir processes and the art of achieving concurrency.
18 |
19 | * **Essential Process Management Concepts:** Learn vital concepts such as process linking, monitoring and more.
20 |
21 | * **Abstractions for Resilience and Control:** Navigate essential abstractions like GenServers, Supervisors, and core Elixir modules such as Task, Registry, and Agents.
22 |
23 | * **Applied Learning with a Project:** Put your knowledge into practice through a hands-on project, solidifying your grasp of asynchronous programming and process management.
24 |
25 | #### Course Focus
26 |
27 | * **Prerequisite Elixir Knowledge:** This course assumes a foundational familiarity with Elixir's syntax and concepts. It DOES NOT teach Elixir basics but instead focuses on the asynchronous capabilities of Elixir.
28 |
29 | * **Core Concepts, Not Libraries:** Our emphasis remains on core concepts. The course doesn't cover libraries like Phoenix or Phoenix LiveView. Instead, it equips you with a strong foundation to comprehend such libraries more effectively.
30 |
31 | ### Prerequisites
32 |
33 | Before diving into the course, make sure you have the following:
34 |
35 | * Installed [Elixir](https://elixir-lang.org/install.html)
36 | * Installed [Livebook](https://livebook.dev/)
37 | * A basic understanding of Elixir syntax and the ability to write code in Elixir. If you need a refresher, you can review Elixir basics [here](https://elixir-lang.org/getting-started/introduction.html).
38 |
39 | [Livebook](https://livebook.dev/) is an excellent tool for learning Elixir and experimenting with its concepts. Learn more about Livebook [here](https://github.com/livebook-dev/livebook).
40 |
41 | #### About me
42 |
43 | I am a full-stack software engineer with four years of focused experience in Elixir programming. Over this period, I have actively engaged in various Elixir projects, extensively delved into Elixir literature, including books and blogs, and honed my skills in this powerful language. My fascination with Elixir's process-oriented architecture has inspired me to condense my insights into this course.
44 |
45 | ## Table of Contents
46 |
47 | #### Part 1: Concurrency and Processes
48 |
49 | * 🌀 [Chapter 1.1: Concurrency in Elixir](ch_1.1_concurrency_in_elixir.livemd)
50 | * 🧠 [Chapter 1.2: Immutability and Memory Management](ch_1.2_immutability_and_memory_management.livemd)
51 |
52 | #### Part 2: Processes
53 |
54 | * 🔄 [Chapter 2.1: Process Internals](ch_2.1_process_internals.livemd)
55 | * 🚀 [Chapter 2.2: Process Basics](ch_2.2_process_basics.livemd)
56 | * 🔗 [Chapter 2.3: Process Linking](ch_2.3_process_linking.livemd)
57 | * 💡 [Chapter 2.4: Process Monitoring and Hibernation](ch_2.4_process_monitoring_and_hibernation.livemd)
58 | * 🛌 [Chapter 2.5: Group Leaders and Process Naming](ch_2.5_group_leaders_and_process_naming.livemd)
59 |
60 | #### Part 3: GenServer
61 |
62 | * 🧪 [Chapter 3.1: GenServer Introduction](ch_3.1_genserver_introduction.livemd)
63 | * 🏗️ [Chapter 3.2: Building a GenServer](ch_3.2_building_a_genserver.livemd)
64 | * 🌐 [Chapter 3.3: GenServer Examples](ch_3.3_genserver_examples.livemd)
65 | * 🔧 [Chapter 3.4: Other GenServer Functions](ch_3.4_other_genserver_functions.livemd)
66 |
67 | #### Part 4: Registry Module
68 |
69 | * 📚 [Chapter 4: The Registry Module](ch_4.0_the_registry_module.livemd)
70 |
71 | #### Part 5: Supervision
72 |
73 | * 👥 [Chapter 5.1: Supervisors Introduction](ch_5.1_supervisors_introduction.livemd)
74 | * 🔄 [Chapter 5.2: Supervision Strategies](ch_5.2_supervision_strategies.livemd)
75 | * 🔄 [Chapter 5.3: Restart Strategies](ch_5.3_restart_strategies.livemd)
76 | * 🚀 [Chapter 5.4: Introduction to Dynamic Supervisor](ch_5.4_introduction_to_dynamic_supervisor.livemd)
77 | * 🗄️ [Chapter 5.5: Partition Supervisor Example](ch_5.5_partition_supervisor.ex.livemd)
78 | * ⚖️ [Chapter 5.6: Scaling with Dynamic Supervisor](ch_5.6_scaling_dynamic_supervisor.livemd)
79 |
80 | #### Part 6: Project: Building a Download Manager
81 |
82 | * 🛠️ [Chapter 6: Building a Download Manager](ch_6.0_project_building_a_download_manager.livemd)
83 |
84 | #### Part 7: Tasks
85 |
86 | * ⚙️ [Chapter 7.1: Introduction to Tasks](ch_7.1_intro_to_tasks.livemd)
87 | * 🔄 [Chapter 7.2: Awaiting Tasks](ch_7.2_awaiting_tasks.livemd)
88 | * 🔀 [Chapter 7.3: Task Async Stream](ch_7.3_task_async_stream.livemd)
89 | * 🛡️ [Chapter 7.4: Supervised Tasks](ch_7.4_supervised_tasks.livemd)
90 |
91 | #### Part 8: Agents
92 |
93 | * 🤖 [Chapter 8: Agents](ch_8.0_agents.livemd)
94 |
95 | #### Part 9: Misc
96 |
97 | * 💡 [Chapter 9: Gotchas](ch_9.0_gotchas.livemd)
98 |
99 | ---
100 |
101 |
102 |
103 | So without further ado, let's [get started](ch_1.1_concurrency_in_elixir.livemd)... 🚀
104 |
--------------------------------------------------------------------------------
/chapters/ch_1.1_concurrency_in_elixir.livemd:
--------------------------------------------------------------------------------
1 | # Concurrency in Elixir
2 |
3 | ## Navigation
4 |
5 |
15 |
16 | ## Concurrency vs Parallelism
17 |
18 | Concurrency and parallelism are often used interchangeably but are two distinct concepts. Concurrency refers to the execution of multiple tasks that overlap in time, with each task being interrupted and resumed intermittently by the CPU(context switching). This can create an illusion of tasks running simultaneously, but in reality, they are taking turns executing on a single time-sliced CPU.
19 |
20 | Parallelism, on the other hand, involves the simultaneous execution of multiple tasks on a hardware system that has multiple computing resources, such as a multi-core CPU. Parallelism allows tasks to run literally at the same time, without having to share CPU time.
21 |
22 | In essence, concurrency deals with handling multiple tasks at once, while parallelism deals with actually performing multiple tasks at the same time. While a system can exhibit both concurrency and parallelism, it is possible to have a concurrent system that is not parallel.
23 |
24 |
25 |
26 | 
27 |
28 |
29 |
30 | In todays world with machines having power multi-core CPUs writing code that can run cocurrently and parallely can lead to huge performance benifits. We should strive to leverage the capabilities of modern hardware advancements by writing code that can fully utilize them.
31 |
32 | Furthermore, as we develop software, we often encounter problems that require background tasks and can benefit greatly from the use of parallel programming. Examples of such tasks include image processing, video transcoding, and make third party api calls, to name a few.
33 |
34 | ## Concurrency and parallelism in Elixir
35 |
36 | Due to the functional and immutable nature of Elixir writing parallel and concurrent code becomes much simpler. Unlike many other languages that require locks and mutexes to handle issues related to shared state in parallel programming, Elixir's design mitigates these problems. As a result, parallel and concurrent code is a first-class citizen in Elixir, requiring less effort and complexity to implement effectively.
37 |
38 |
39 |
40 | In Elixir, **the Erlang Virtual Machine (BEAM)** serves as the backbone for managing concurrency and parallelism. Let's take a closer look at how it works under the hood to provide us with these superpowers.
41 |
42 | Elixir leverages lightweight processes that are expertly managed by the VM. These processes are not true OS processes, making them highly lightweight and allowing for thousands or even [millions](https://phoenixframework.org/blog/the-road-to-2-million-websocket-connections) of them to run concurrently without impacting performance.
43 |
44 | This lightweight process model has given rise to several powerful applications, including the [Cowboy web server](https://github.com/ninenines/cowboy) which creates a process for every incoming web request to keep heavy work or errors within a single request from affecting others. Other examples include [Phoenix Channels](https://hexdocs.pm/phoenix/channels.html) and [Phoenix live view](https://hexdocs.pm/phoenix_live_view/Phoenix.LiveView.html) that employ an Erlang process per WebSocket connection.
45 |
46 | ## Processes in Elixir
47 |
48 | In Elixir and Erlang, the term "processes" does not refer to operating system processes or threads. Instead, they are akin to [green threads](https://en.wikipedia.org/wiki/Green_thread#:~:text=In%20computer%20programming%2C%20a%20green,underlying%20operating%20system%20(OS).) or actors. These processes run concurrently on a single-core CPU and in parallel on multiple cores, managed and scheduled by the Erlang Virtual Machine.
49 |
50 | Surprisingly, each process in Elixir and Erlang requires only around 300 words of memory and takes microseconds to start, making them incredibly lightweight. In fact, within the Erlang Virtual Machine, every entity executing code operates within a process.
51 |
52 | For instance, in [Phoenix](https://www.phoenixframework.org/), when making a regular HTTP request using Phoenix controllers, the corresponding connection is allocated its own process. This process is swiftly terminated once the response is sent and the connection is closed. In [LiveView](https://github.com/phoenixframework/phoenix_live_view) we keep that process alive since we work with websockets.
53 |
54 | Each process is capable of executing code and possesses a **first-in-first-out** mailbox to which other processes can send messages. Likewise, it can send messages to other processes. Processes in Elixir and Erlang are inherently **sequential**, meaning they handle one message at a time.
55 |
56 | Similar to an operating system scheduler, the Erlang VM has the ability to start, pause, or preempt work as needed (In computing, preemption is the act of temporarily interrupting an executing task, with the intention of resuming it at a later time).
57 |
58 | While waiting for a message, a process is completely ignored by the scheduler. As a result, **idle processes do not consume any system resources**.
59 |
60 | #### Reductions
61 |
62 | Erlang uses "reductions" as work units to decide when a process might be paused. A reduction in Erlang is a unit of work done by BEAM, including tasks like function application, arithmetic operations, and message passing. The scheduler monitors reductions for each process, pausing a process once it reaches a set reduction count, which lets another process take its turn to run. This ensures fairness in scheduling by preventing processes from hogging the CPU.
63 |
64 | Additionally, reductions are applied flexibly based on the operation type. For instance, I/O operations consume reductions differently, allowing the scheduler to handle various operations effectively. Unlike traditional blocking I/O, Erlang's non-blocking model lets processes continue working during I/O waits, improving overall system performance.
65 |
66 |
67 |
68 | #### Scheduling in BEAM
69 |
70 | BEAM, the underlying virtual machine, employs a single OS thread per core, and each thread runs its own scheduler. Every scheduler is responsible for pulling processes from its own run queue, with the BEAM being responsible for populating these queues with Erlang processes for execution.
71 |
72 | (Note: To utilize more than one core the Erlang Runtime System Application(ERTS) has to be built in SMP mode. SMP stands for Symmetric MultiProcessing, that is, the ability to execute a processes on any one of multiple CPUs.)
73 |
74 | The scheduler manages two queues: a ready queue containing processes that are prepared to run and a waiting queue containing processes that are waiting to receive a message.
75 |
76 | When a process is selected from the ready queue, it is handed over to BEAM for the execution of one CPU time slice. BEAM interrupts the running process and places it at the end of the ready queue when the time slice expires. However, if the process is blocked in a receive operation before the time slice runs out, it is added to the waiting queue.
77 |
78 | ###### Loadbalancer
79 |
80 | A load balancer is also in place, responsible for executing migration logic to allocate processes across the run queues on separate cores. This logic assists in maintaining load balance by taking jobs away from overloaded queues (known as [task stealing](https://blog.stenmans.org/theBeamBook/#_task_stealing)) and assigning them to empty or underloaded queues (known as ["task migration"](https://blog.stenmans.org/theBeamBook/#_migration)).
81 |
82 | In simpler terms, if one scheduler's queue becomes crowded due to processes taking an extended time, other schedulers step in to distribute the workload more evenly. For example, if a process accumulates a high number of function calls (reductions), without completing, the scheduler will preemptively pause it which means freeze it, mid-run, and send it back to the end of the work queue. This **preemptive multitasking** approach ensures that no single task can monopolize the system for an extended period, ensuring consistently **low latency**, a key feature of the BEAM.
83 |
84 | The load balancer strives to maintain an equal maximum number of run-able processes across schedulers.
85 |
86 | Looking beyond Erlang's internal run queues, the operating system also manages the scheduling of threads onto CPU cores at an OS level. This means that processes can not only swap within Erlang's run queue but also undergo complete context switches or be relocated to different cores by the OS.
87 |
88 |
89 |
90 | 
91 |
92 |
93 |
94 | You can find the numer of schedulers in your IEX session using the `System.schedulers/0` function.
95 |
96 | ```elixir
97 | # Returns the number of schedulers in the VM.
98 | System.schedulers()
99 |
100 | # Returns the number of schedulers online in the VM.
101 | # Here online means total number of schedulers which are active and actually being used.
102 | System.schedulers_online()
103 | ```
104 |
105 | #### Process priority
106 |
107 | Erlang's priority system has four levels: low, normal, high, and max (reserved for Erlang's internal use). Each level has its own run queue and follows a round-robin scheduling method, except for max.
108 |
109 | Processes in max or high priority queues are executed exclusively, blocking lower-priority processes until they're done. This design emphasizes efficiency for critical tasks but can cause bottlenecks if high-priority processes are overused, impacting overall application responsiveness.
110 |
111 | Low and normal queues are more flexible, allowing interleaved execution without blocking each other. However, using high priority sparingly is crucial to avoid performance issues.
112 |
113 | Additionally, Erlang permits communication across different priority levels, although a high-priority process waiting for a message from a lower-priority one will effectively lower its own priority.
114 |
115 | Process priority can be changed in elixir using `Process.flag(:priority, :high)`
116 |
117 | ## Resources
118 |
119 | * https://medium.com/flatiron-labs/elixir-and-the-beam-how-concurrency-really-works-3cc151cddd61
120 | * https://blog.stenmans.org/theBeamBook/#CH-Scheduling
121 | * https://blog.appsignal.com/2024/04/23/deep-diving-into-the-erlang-scheduler.html
122 | * https://fly.io/phoenix-files/a-liveview-is-a-process/
123 | * https://underjord.io/unpacking-elixir-concurrency.html
124 |
125 | ## Navigation
126 |
127 |
137 |
--------------------------------------------------------------------------------
/chapters/ch_1.2_immutability_and_memory_management.livemd:
--------------------------------------------------------------------------------
1 | # Immutability and memory management
2 |
3 | ## Navigation
4 |
5 |
19 |
20 | ## Immutability in elixir
21 |
22 | In Elixir, variables function as **labels** that refer to specific values. These values are immutable, meaning they cannot be changed. However, the label or variable can be reassigned to a different value. This provides the flexibility to bind the same value to multiple labels or variables.
23 |
24 | In Erlang, it is not possible to reassign or rebind a variable. Attempting to do so will result in an error, as demonstrated in the following code:
25 |
26 | ```erlang
27 | X = 5,
28 | X = X * 10. % throws an exception error: no match of right hand side value 50
29 | ```
30 |
31 | The error in the Erlang code occurs because the variable X is initially assigned to the value of 5, but then an attempt is made to reassign it to a new value (i.e. X * 10). Since variables in Erlang are immutable, this operation is not allowed and the code will fail to compile.
32 |
33 | On the other hand, Elixir allows for rebinding of values, which makes the same code valid in Elixir. This is because in Erlang, **the = operator functions as a match operator rather than an assignment operator**.
34 |
35 | Therefore, in the Erlang code `X = X * 10`, the left-hand side of the match (X) is already bound to the value of 5, and trying to match it with the right-hand side (X * 10) which evaluates to 50, results in a mismatch.
36 |
37 | In Elixir, the ^ (pin) operator can be used to force a match, while the assignment operation uses the regular = operator. For example:
38 |
39 | ```
40 | iex(1)> x = 5
41 | 5
42 | iex(2)> x = x * 10 # Assignment
43 | 50
44 | iex(3)> ^x = x * 10 # Matching
45 | ** (MatchError) no match of right hand side value: 500
46 | (stdlib 4.0.1) erl_eval.erl:496: :erl_eval.expr/6
47 | iex:3: (file)
48 | ```
49 |
50 | In Elixir, any input passed into a function to be transformed creates a new value without modifying the original value. This allows for safe concurrent access to the same data by multiple processes. Since there is **no shared memory** that is getting mutated by multiple processes, concurrency is easier to manage. Any transformation on the original data will result in new data being created. Processes do not share state, they can only communicate asynchronously through message passing. This ensures it is safe to run them at the same time.
51 |
52 | ## Example of immutability & closures in elixir
53 |
54 | ```elixir
55 | list = [1, 2, 3, 4]
56 |
57 | # Returns a new list
58 | Enum.filter(list, fn num -> num > 2 end)
59 |
60 | # [1, 2, 3, 4] Original list remains unchanged
61 | list
62 | ```
63 |
64 | ```elixir
65 | x = 1
66 |
67 | # An anonymous function
68 | anon = fn ->
69 | # Closure captures the value of x
70 | IO.puts(x)
71 | x = 0
72 | end
73 |
74 | # Outputs 1
75 | anon.()
76 |
77 | # Outputs 1
78 | IO.puts(x)
79 |
80 | x = 5
81 |
82 | # Outputs 1
83 | anon.()
84 | ```
85 |
86 | ## Persistent Datastructures
87 |
88 | At this point you might be wandering that performing a full copy of the entire data whenever something changes would be an expensive and slow operation and lead to a high performance overhead.
89 |
90 | To solve this problem there is a class of datastructures known as **persistent data structures**.
91 |
92 | Persistent data structures are data structures that allow for the efficient storage and retrieval of data even after multiple modifications or updates have been made. These data structures preserve the previous versions of data and allow for efficient access to those versions.
93 |
94 | A persistent data structure **maintains the previous versions of data** by using a technique known as structural sharing. Structural sharing allows the data structure to **share the unchanged parts of its structure** across multiple versions of the data, rather than copying the entire structure each time a modification is made. This sharing of unchanged structure makes persistent data structures efficient in both time and space.
95 |
96 | Persistent data structures are widely used in functional programming languages, where immutability is a core concept.
97 |
98 | Under the hood, the BEAM leverages persistent data structures in order to provide
99 | immutability as a first-class citizen while not having to copy the entire data structure
100 | any time something changes (with the exceptions of when data is passed between
101 | processes or when data is extracted from native data stores like ETS).
102 |
103 | For example, in Elixir lists are actually linked lists.
104 | A linked list is a Tree with one branch.
105 |
106 | ```
107 | Elixir: list = [1, 2, 3, 4]
108 | Tree: 1 -> 2 -> 3 -> 4
109 |
110 | Every time you prepend an element to a list, it will share its tail:
111 | Elixir: [0 | list]
112 | Tree: 0 -> (1 -> 2 -> 3 -> 4)
113 | ```
114 |
115 | ## High level overview of garbage collection in elixir
116 |
117 | Erlang employs a generational copying garbage collection system where each process has its own private heap. This heap is divided into two segments - the young and old generations. Newly allocated data resides in the young generation, while data that has survived multiple garbage collection cycles is stored in the old generation.
118 |
119 | The young generation undergoes more frequent garbage collection, whereas the old generation is only garbage collected during a full sweep, which occurs after a certain number of generational GC cycles. It can also be collected if not enough memory is reclaimed or if manually invoked. This process is referred to as **soft real-time** garbage collection because it halts only the process undergoing GC without affecting other processes. This characteristic is well-suited for soft real-time systems, as the entire runtime doesn't need to pause.
120 |
121 | In addition to this, Erlang implements [reference counting garbage collection](https://en.wikipedia.org/wiki/Garbage_collection_\(computer_science\)#Reference_counting) for the shared heap. Here, objects in the shared heap are assigned reference counters that keep track of the number of references held by other objects. When an object's reference count drops to zero, it's considered inaccessible and gets destroyed.
122 |
123 | For a deeper dive into garbage collection, you can explore this informative [video](https://www.youtube.com/watch?v=OSdaXNQ0xhQ) and this insightful [book](https://hamidreza-s.github.io/erlang%20garbage%20collection%20memory%20layout%20soft%20realtime/2015/08/24/erlang-garbage-collection-details-and-why-it-matters.html).
124 |
125 | ## Resources
126 |
127 | * https://stackoverflow.com/questions/30203227/does-elixir-have-persistent-data-structures-similar-to-clojure
128 |
129 | * https://elixirpatterns.dev/
130 |
131 | * https://gist.github.com/josevalim/ce2f5871a96b6cbcf2c1
132 |
133 | * https://elixirforum.com/t/how-would-you-explain-elixir-immutability/47323/11
134 |
135 | * [Video on garbage collection](https://www.youtube.com/watch?v=OSdaXNQ0xhQ)
136 |
137 | * [The beam book -garbage collection](https://hamidreza-s.github.io/erlang%20garbage%20collection%20memory%20layout%20soft%20realtime/2015/08/24/erlang-garbage-collection-details-and-why-it-matters.html)
138 |
139 | ## Navigation
140 |
141 |
155 |
--------------------------------------------------------------------------------
/chapters/ch_2.1_process_internals.livemd:
--------------------------------------------------------------------------------
1 | # Process Internals
2 |
3 | ## Navigation
4 |
5 |
19 |
20 | ## What are processes?
21 |
22 | A process is a **self-contained** entity where code is executed. It safeguards the system from errors in our code by restricting the effects of the error to the process that is executing the faulty code. Processes have their own address space and can communicate with other processes via signals and messages, and their execution is managed by a preemptive scheduler.
23 |
24 | It is important to note that Elixir processes are not the same as operating system processes. Elixir processes are remarkably **lightweight** in terms of memory and CPU usage, even when compared to threads in other programming languages. Therefore, it is not uncommon to run tens or even [hundreds of thousands](https://phoenixframework.org/blog/the-road-to-2-million-websocket-connections) of processes simultaneously.
25 |
26 | ## Internals of a process
27 |
28 | Let's explore the structure of an Elixir process at a high level.
29 |
30 | An Elixir process consists of four primary memory blocks: the **stack**, the **heap**, the message area (also known as the **mailbox**), and the **Process Control Block** (PCB). The stack is responsible for tracking program execution by storing return addresses, passing function arguments, and keeping local variables. The heap, on the other hand, stores larger structures such as lists and tuples.
31 |
32 | The message area or mailbox is used to hold messages sent from other processes to the target process. The PCB maintains the state of the process, while the stack, heap, and mailbox are dynamically allocated and can grow or shrink based on usage. Conversely, the PCB is statically allocated and contains several fields that control the process.
33 |
34 | **Message passing** is the primary means of communication between Elixir processes. When one process sends a message to another, the message is copied from the sender's heap to the recipient's mailbox. In certain circumstances, such as when a process is suspended and no other processes are attempting to send it messages, the message may be directly copied to the recipient's mailbox. In other cases, the message is stored in an m-buf and moved to the heap after a garbage collection. M-bufs are variable-length heap fragments, and a process may have several m-bufs.
35 |
36 | (It is worth noting that in the early versions of Erlang, parallelism was not available, so only one process could execute at any given time. In such versions, the sending process could write directly to the heap of the receiving process. However, with the rise of multicore systems, message copying across process heaps is managed using locks and queues. To learn more about this topic, please see this [article](https://blog.stenmans.org/theBeamBook/#_the_process_of_sending_a_message_to_a_process).)
37 |
38 | ## Resources
39 |
40 | * https://elixir-lang.org/getting-started/processes.html
41 | * https://hexdocs.pm/elixir/1.12/Process.html
42 | * https://www.erlang-solutions.com/blog/understanding-processes-for-elixir-developers/
43 |
44 | ## Navigation
45 |
46 |
60 |
--------------------------------------------------------------------------------
/chapters/ch_2.2_process_basics.livemd:
--------------------------------------------------------------------------------
1 | # Process Basics
2 |
3 | ## Navigation
4 |
5 |
19 |
20 | ## Process introspection
21 |
22 | To check processes we have in a running system: `:shell_default.i`.
23 |
24 | You might notice that many processes have a heap size of 233, that is because it is the default starting heap size of a process.
25 |
26 | If there is a large number for the heap size, then the process uses a lot of memory and if there is a large number for the reductions then the process has executed a lot of code.
27 |
28 | Get lot more infor about a process using `Process.info/1`.
29 |
30 | ```elixir
31 | Process.whereis(:code_server)
32 | pid = Process.whereis(:code_server)
33 | Process.info(pid)
34 | ```
35 |
36 | [Process.info/2](http://Process.info/2) can be used to view additional info like backtrace `Process.info(pid, :backtrace)`
37 |
38 | The [observer](https://elixir-lang.org/getting-started/debugging.html#observer) is also a great tool to observe processes.
39 |
40 | ## Process Dictionary
41 |
42 | There is actually one more memory area in a process where Erlang terms can be stored, the *Process Dictionary*.
43 |
44 | The *Process Dictionary* (PD) is a process local key-value store. One advantage with this is that all keys and values are stored on the heap and there is no copying as with `send/2` or an ETS table.
45 |
46 | ([ETS](https://elixirschool.com/en/lessons/storage/ets#overview-0) or Erlang Term Storage is a in-memory store for Elixir and Erlang objects that comes included. ETS is capable of storing large amounts of data and offers constant time data access. Tables in ETS are created and owned by individual processes. When an owner process terminates, its tables are destroyed)
47 |
48 | ```elixir
49 | # Stores the given key-value pair in the process dictionary.
50 | Process.put(:count, 1)
51 | Process.put(:locale, "en")
52 | ```
53 |
54 | ```elixir
55 | # Returns the value for the given key in the process dictionary
56 | Process.get(:count)
57 | ```
58 |
59 | ```elixir
60 | # Returns all keys in the process dictionary
61 | Process.get_keys()
62 | ```
63 |
64 | ```elixir
65 | # Deletes the given key from the process dictionary
66 | Process.delete(:count)
67 | ```
68 |
69 | ```elixir
70 | # Returns all key-value pairs in the process dictionary.
71 | Process.get()
72 | ```
73 |
74 | ## Spawning processes
75 |
76 | The most fundamental way to create processes in Elixir is by using the [spawn/1](https://hexdocs.pm/elixir/1.12/Kernel.html#spawn/1), [receive/1](https://hexdocs.pm/elixir/1.12/Kernel.SpecialForms.html#receive/1), and [send/2](https://hexdocs.pm/elixir/1.12/Kernel.html#send/2) functions. They enable us to spawn a process, wait for messages, and send messages to a process, respectively.
77 |
78 | Many higher-level abstractions, such as Task, GenServer, and Agent, are built on top of these primitive functions.
79 |
80 | These functions are part of the [Kernel](https://hexdocs.pm/elixir/1.12/Kernel.html) module and are automatically imported, allowing us to call them directly without needing to use the `Kernel.` prefix.
81 |
82 | Let's take a look at some examples of their usage...
83 |
84 | ```elixir
85 | # Spawn a process, by passing it a function to execute.
86 | # spawn/1 returns the pid (process identifier) of the spawed process
87 | pid = spawn(fn -> IO.puts("Hello world") end)
88 |
89 | # Once the process has finished excuting it will exit
90 | Process.alive?(pid) |> IO.inspect()
91 |
92 | # Sleep for 100ms to wait for process to exit
93 | :timer.sleep(100)
94 |
95 | Process.alive?(pid)
96 | ```
97 |
98 | When spawning a process it goes through a lifecycle like so...
99 |
100 |
101 |
102 | ```mermaid
103 | flowchart LR
104 | spawn --> NewProcess --> ExecuteCallbackFunction --> Dead
105 | ```
106 |
107 | ## Exchanging messages between processes
108 |
109 | To exchange messages between processes in Elixir, we can use the `send/2` and `receive/1` functions.
110 |
111 | When a process uses `send/2` to send a message, it **doesn't block** - instead, the message is placed in the recipient's mailbox, and the sending process continues.
112 |
113 | On the other hand, when a process uses `receive/1`, it blocks until a matching message is found in its mailbox. The call to `receive/1` searches the mailbox for a message that matches any of the given patterns.
114 |
115 | `receive/1` supports guards and multiple clauses, such as `case/2`.
116 |
117 | Let's look at an example of a process sending a message to itself.
118 |
119 | ```elixir
120 | # Get the pid of the current process
121 | self_pid = self()
122 |
123 | # Send a message to the current process
124 | send(self_pid, :ping)
125 |
126 | # Check messages in mailbox without consuming them
127 | Process.info(self_pid, :messages) |> IO.inspect(label: "Messages in mailbox")
128 |
129 | # Recieve the message waiting in mailbox (consumes the message in the mailbox)
130 | receive do
131 | :ping -> IO.puts(:pong)
132 | end
133 |
134 | # Check messages in mailbox again
135 | Process.info(self_pid, :messages) |> IO.inspect(label: "Messages in mailbox")
136 | ```
137 |
138 | An optional after clause can be given in case the message was not received after the given timeout period, specified in milliseconds.
139 | (If timeout `0` is given then the message is expected to be already present in the mailbox.)
140 |
141 | ```elixir
142 | receive do
143 | {:message, message} when message in [:start, :stop] -> IO.puts(message)
144 | _ -> IO.puts(:stderr, "Unexpected message received")
145 | after
146 | 1000 -> IO.puts(:stderr, "Timeout, no message in 1 seconds")
147 | end
148 | ```
149 |
150 | In the elixir IEx shell, we have a helper function flush/0 that flushes or consumes and prints all the messages in the mailbox of the shell process.
151 |
152 | ```elixir
153 | send(self(), :hello)
154 |
155 | Process.info(self_pid, :messages) |> IO.inspect(label: "Messages in mailbox before flush")
156 |
157 | # In the iex shell we wont have to use the `IEx.Helpers,` prefix since these helpers functions are imported automatically
158 | IEx.Helpers.flush()
159 |
160 | Process.info(self_pid, :messages) |> IO.inspect(label: "Messages in mailbox after flush")
161 | ```
162 |
163 | ## Navigation
164 |
165 |
179 |
--------------------------------------------------------------------------------
/chapters/ch_2.3_process_linking.livemd:
--------------------------------------------------------------------------------
1 | # Process Linking & Trapping Exists
2 |
3 | ## Navigation
4 |
5 |
19 |
20 | ## Process Linking
21 |
22 | In Elixir, when we create a process, we have the option to link it to its parent process. This means that if the child process encounters an error and fails, the parent process will be notified.
23 |
24 | When we use the `spawn/1` function to create a process, it will not be linked to its parent process. As a result, if the child process encounters an error and fails, the parent process will not be notified.
25 |
26 | To ensure that the parent process is notified of any errors in the child process, we can use the `spawn_link/1` function instead. This function creates a linked process, so if the child process crashes, the parent process will receive an EXIT signal.
27 |
28 | To illustrate this, let's consider an example...
29 |
30 | ```elixir
31 | unlinked_child_process = spawn(fn -> raise("BOOM! Unliked process crashed!") end) |> IO.inspect()
32 |
33 | IO.inspect(Process.info(self(), :links))
34 |
35 | :timer.sleep(100)
36 |
37 | IO.puts("Parent process still alive!")
38 | ```
39 |
40 | In the above example we can see that the parent process is still alive after the spawned process crashes. Lets see what happens if the processes were linked
41 |
42 |
43 |
44 | (Uncomment the code below and run it. After running it comment it out again.
45 | Since the code below crashes the live view process we need to comment it in order to run the rest of the code in this chapter.)
46 |
47 | ```elixir
48 | # linked_child_process = spawn_link(fn ->
49 | # :timer.sleep(100)
50 | # raise("BOOM! Linked process crashed!")
51 | # end)
52 | # |> IO.inspect(label: "Linked process PID")
53 |
54 | # IO.inspect Process.info(self(), :links)
55 |
56 | # :timer.sleep(200)
57 |
58 | # IO.puts "Parent process still alive!"
59 |
60 | # Child process <-> parent process <-> Livebook evaluation process
61 | ```
62 |
63 | This time the print statement "Parent process still alive!" is never printed because when linked process crashes it brings down the parent process with it.
64 |
65 | In our case this also leads the linked live view process to crash.
66 |
67 | When a linked process exits gracefully with a reason `:normal` this does not lead to the parent process to crash. Any other reason other than `:normal` is considered an abnormal termination and will lead to the linked processes exiting as well.
68 |
69 | When a process reaches its end, by default it exits with reason `:normal`
70 |
71 | ```elixir
72 | linked_process =
73 | spawn_link(fn ->
74 | exit(:normal)
75 | Process.sleep(60000)
76 | end)
77 |
78 | :timer.sleep(100)
79 | IO.inspect(Process.alive?(linked_process), label: "Linked process alive?")
80 | ```
81 |
82 | Linking can also be done manually by calling `Process.link/1`, lets see a bigger example...
83 |
84 | ```elixir
85 | defmodule LinkingProcess do
86 | def call do
87 | child_process = spawn(&recursive_link_inspectior/0)
88 |
89 | IO.inspect(self(), label: "Parent process PID")
90 | IO.inspect(child_process, label: "Child process PID")
91 |
92 | IO.inspect(Process.info(self(), :links), label: "Parent process links")
93 |
94 | send(child_process, :inspect_links)
95 |
96 | # Wait for the child process to print its links
97 | :timer.sleep(100)
98 |
99 | # Link the two processes
100 | Process.link(child_process)
101 |
102 | :timer.sleep(100)
103 |
104 | IO.inspect(Process.info(self(), :links), label: "Parent process links")
105 |
106 | send(child_process, :inspect_links)
107 | end
108 |
109 | defp recursive_link_inspectior do
110 | receive do
111 | :inspect_links ->
112 | links = Process.info(self(), :links)
113 | IO.inspect(links, label: "Child process links")
114 | end
115 |
116 | recursive_link_inspectior()
117 | end
118 | end
119 |
120 | LinkingProcess.call()
121 | ```
122 |
123 | When a process is linked to others, a crash in that process can trigger a cascade effect, potentially causing multiple other linked processes to crash as well. For instance, imagine a scenario where five processes (P1 to P5) are linked as follows:
124 |
125 | `P1 <-> P2 <-> P3 <-> P4 <-> P5`
126 |
127 | If any of these processes crash, it will cause all five to fail due to their interconnectivity. For instance, if P4 crashes, it will cause P3 and P5 to crash as well. This, in turn, will lead to the failure of P2, which will ultimately cause P1 to fail as well.
128 |
129 | It's important to remember that **process links are bidirectional**, which means that if one process fails, it will affect the other processes as well.
130 |
131 |
132 |
133 | ### Importance of process linking
134 |
135 | Processes and links play an important role when building fault-tolerant systems. Elixir processes are isolated and don’t share anything by default. Therefore, a failure in a process will never crash or corrupt the state of another process. Links, however, allow processes to establish a relationship in case of failure. We often link our processes to supervisors which will detect when a process dies and start a new process in its place.
136 |
137 | While other languages would require us to catch/handle exceptions, in Elixir we are actually fine with letting processes fail because we expect supervisors to properly restart our systems. “Failing fast” (sometimes referred as “let it crash”) is a common philosophy when writing Elixir software!
138 |
139 |
140 |
141 | ### Trapping EXITS
142 |
143 | For some reason if we want to prevent a process from crashing when a linked process exits we can do so by trapping exit message.
144 |
145 | Normally when a process finishes its work it implicitly calls `exit(:normal)` to communicate with its parent process that its job has been done. Any other argument to `exit/1` other than `:normal` is treated as an error.
146 |
147 | Setting `trap_exit` to true in Elixir means that **exit signals received by a process are converted into messages** of the form `{'EXIT', From, Reason}`. These messages can then be received like any other message in the process's mailbox. On the other hand, if `trap_exit` is set to false, the process will exit if it receives an exit signal that is not a normal exit, and the signal will be passed on to any processes that are linked to it.
148 |
149 | By using `trap_exit` and linking processes, we can prevent the failure of one process from causing the failure of another. This allows the linked process to handle the termination of the other process gracefully, rather than being abruptly terminated itself.
150 |
151 | As always lets look at an example to understand this better...
152 |
153 | ```elixir
154 | # Start trapping exit for the current process
155 | Process.flag(:trap_exit, true)
156 |
157 | # A linked process that will exit abnormally with a reason :boom
158 | p = spawn_link(fn -> exit(:boom) end)
159 |
160 | :timer.sleep(100)
161 |
162 | # Is the child process is alive?
163 | IO.inspect(Process.alive?(p), label: "Child process alive?")
164 |
165 | # Check how the parent process that is trapping exit received an EXIT message
166 | Process.info(self(), :messages) |> IO.inspect(label: "Messages in parent process mailbox")
167 | ```
168 |
169 | As we see from the print messages the linked process crashing does not lead to a crash of the parent process as it is trapping exits, instead the parent receives a message like `{:EXIT, linked_process_pid, :boom}` in its mailbox.
170 |
171 |
172 |
173 | It is generally recommended to avoid trapping exits as it can modify the normal behavior of processes. Instead, it is recommended to utilize monitors and supervisors to handle failures.
174 |
175 | When a process traps exits, it becomes unresponsive to exit signals unless a kill exit reason is explicitly sent to it. Lets look at an example...
176 |
177 | ```elixir
178 | # Un-killable exit trapper process
179 | p =
180 | spawn(fn ->
181 | Process.flag(:trap_exit, true)
182 | :timer.sleep(:infinity)
183 | end)
184 |
185 | IO.inspect(Process.alive?(p), label: "Process alive initially")
186 |
187 | Process.exit(p, :normal)
188 | :timer.sleep(100)
189 | IO.inspect(Process.alive?(p), label: "After :normal exit signal")
190 |
191 | Process.exit(p, :boom)
192 | :timer.sleep(100)
193 | IO.inspect(Process.alive?(p), label: "After :boom exit signal")
194 |
195 | # Only a :kill exit signal can kill a process thats trapping exits.
196 | Process.exit(p, :kill)
197 | :timer.sleep(100)
198 | IO.inspect(Process.alive?(p), label: "After :kill exit signal")
199 | ```
200 |
201 | In Elixir, the `:normal` and `:kill` are special exit reasons. `:normal` signifies a successful and expected process termination. On the other hand, `:kill` is non-trappable, causing immediate process termination. Any other termination reasons are informational and can be trapped if necessary.
202 |
203 | Note that the call to `Process.exit(pid, :normal)` function is silently ignored if the specified `pid` is different from the calling process's own `pid` (`self()`). This is an edge case.
204 |
205 | ## Resources
206 |
207 | * https://eddwardo.github.io/posts/links-in-elixir/
208 |
209 | ## Navigation
210 |
211 |
225 |
--------------------------------------------------------------------------------
/chapters/ch_2.4_process_monitoring_and_hibernation.livemd:
--------------------------------------------------------------------------------
1 | # Process Monitoring and Hibernation
2 |
3 | ## Navigation
4 |
5 |
19 |
20 | ## Process Monitors
21 |
22 | Process **links are bidirectional**, which means that if a linked process exits, it will also bring down the current process. However, if we only want the current process to be notified when a process has exited, instead of linking, we can use monitors.
23 |
24 | Unlike linking, **monitoring is unidirectional**. If there is an error in a monitored process, it does not bring down the current process. Instead, the current process is notified via a `{:DOWN, , :process, , }` message.
25 |
26 | It's worth noting that even when the monitored process exits normally, we still receive a message. In the case of process linking, a process is only notified if the linked process exits abnormally (i.e., with a reason other than :normal).
27 |
28 | Lets look at an example of process monitoring...
29 |
30 | ```elixir
31 | pid = spawn(fn -> :timer.sleep(10000) end)
32 | Process.monitor(pid)
33 | Process.exit(pid, :boom)
34 | :timer.sleep(100)
35 | IO.inspect(Process.info(self(), :messages))
36 | ```
37 |
38 | ## Process hibernation
39 |
40 | We can call `Process.hibernate/3` to hibernate a process.
41 |
42 | From the official [documentation](https://erlang.org/doc/man/erlang.html#hibernate-3)
43 |
44 | > Puts the calling process into a **wait state** where its **memory allocation has been reduced as much as possible**. This is useful if the process does not expect to receive any messages soon. The process is awaken when a message is sent to it, and control resumes in Module:Function with the arguments specified by Args with the call stack emptied.
45 |
46 | > In more technical terms, `erlang:hibernate/3` discards the call stack for the process, and then **garbage collects** the process. After this, all live data is in one continuous heap. The heap is then shrunken to the exact same size as the live data that it holds.
47 |
48 |
49 |
50 | ### When is process hibernation useful?
51 |
52 | Hibernation of a process can be beneficial in situations where the process should not be terminated but is not expected to receive any messages anytime soon. By hibernating the process, we can free up the memory that was allocated to the process during garbage collection and thus prevent unnecessary resource usage.
53 |
54 | Some practical examples where hibernation can be useful include occasionally used processes that should not be dropped, as doing so may be interpreted as a network disconnection by the client. Additionally, any process that is expensive to reinitialize may also be a good candidate for hibernation.
55 |
56 | Lets see an example...
57 |
58 | ```elixir
59 | p1 =
60 | spawn(fn ->
61 | _big_binary = :crypto.strong_rand_bytes(1000)
62 | :timer.sleep(:infinity)
63 | end)
64 |
65 | p2 =
66 | spawn(fn ->
67 | _big_binary = :crypto.strong_rand_bytes(1000)
68 | Process.hibernate(IO, :puts, ["P2 woken from hibernation"])
69 |
70 | # This never executes as execution resumes at the function passed to Process.hibernate/3
71 | IO.puts("Kabooom!")
72 | end)
73 |
74 | Process.info(p1, :total_heap_size) |> IO.inspect(label: "Heap size of P1")
75 | Process.info(p2, :total_heap_size) |> IO.inspect(label: "Heap size of P2")
76 |
77 | # Wake p2 from hibernation by sending it a message
78 | send(p2, :msg)
79 | :timer.sleep(100)
80 |
81 | # Here the process is no longer alive since after executing the IO.puts/1
82 | # call it has no other work and exits normally.
83 | Process.alive?(p2) |> IO.inspect(label: "P2 alive")
84 | ```
85 |
86 | ## Resources
87 |
88 | * https://elixirforum.com/t/when-is-hibernation-of-processes-useful/23181/5
89 | * https://hexdocs.pm/elixir/1.12.3/Process.html
90 |
91 | ## Navigation
92 |
93 |
107 |
--------------------------------------------------------------------------------
/chapters/ch_2.5_group_leaders_and_process_naming.livemd:
--------------------------------------------------------------------------------
1 | # Group leaders and naming processes
2 |
3 | ## Navigation
4 |
5 |
19 |
20 | ## Group Leader
21 |
22 | In Erlang, every process belongs to a process group, and each group has a group leader. The group leader is **responsible for handling I/O** for the processes in its group. When a process is spawned, it inherits the same group leader as its parent process.
23 |
24 | At system start-up, the init process(the first process which coordinates the start-up of the system) is both its own group leader and the group leader of all processes.
25 |
26 | The Erlang VM **models I/O devices as processes**, which enables different nodes in the same network to exchange file processes and read/write files between nodes. The group leader can be configured per process and is used in different situations. For example, when executing code in a remote terminal, it ensures that messages from a remote node are redirected and printed in the terminal that triggered the request.
27 |
28 | The **main responsibility of the group leader is to collect I/O output from all processes in its group and pass it to or from the underlying system**. It essentially owns the standard input, standard output, and standard error channels on behalf of the group.
29 |
30 | When a file is opened using `File.open/2`, it returns a tuple like `{:ok, io_device}`, where `io_device` is the PID of the process that handles the file. This process monitors the process that opened the file (the owner process), and if the owner process terminates, the file is closed, and the process itself terminates too.
31 |
32 |
33 |
34 | ```elixir
35 | {:ok, io_device_pid} = File.open("test.csv", [:write])
36 | IO.write(io_device_pid, "a binary")
37 | ```
38 |
39 | When you call `IO.write(pid, binary)`, the IO module sends a message to the process identified by pid with the desired operation, such as :put_chars.
40 | The message has the following structure: `{:io_request, , , {:put_chars, :unicode, "hello"}}`.
41 |
42 | When you write to :stdio, you are actually sending a message to the group leader, which writes to the standard-output file descriptor.
43 | Therefore, these three code snippets are equivalent:
44 |
45 |
46 |
47 | ```elixir
48 | IO.puts "hello"
49 | IO.puts :stdio, "hello"
50 | IO.puts Process.group_leader, "hello"
51 | ```
52 |
53 | To understand this better let see some examples
54 |
55 | Suppose we have two Erlang nodes named "node1" and "node2".
56 | You can create two `iex` shells for this like
57 |
58 |
59 |
60 | ```elixir
61 | iex --sname node1@localhost
62 | iex --sname node2@localhost
63 | ```
64 |
65 | (Note: If you want to send messages between nodes on different networks, we need to start the named nodes with a shared cookie)
66 |
67 | If we execute the following code in the iex shell of node1:
68 |
69 |
70 |
71 | ```elixir
72 | Node.spawn_link(:node2@localhost, fn ->
73 | IO.puts("I will be executed on node2 but printed on node1 since the group leader is node1")
74 | end)
75 | ```
76 |
77 | The output of the IO.puts operation will be sent to the group leader, which in this case is node1.
78 | Therefore, the output will be printed on node1's standard output stream, even though the process that performed the operation is running on node2.
79 |
80 | On the other hand, if we specify the device PID as the `:init` process on node2, the output will be seen on node2's standard output stream:
81 |
82 |
83 |
84 | ```elixir
85 | Node.spawn_link(:node2@localhost, fn ->
86 | init_process_pid = Process.whereis(:init)
87 |
88 | IO.puts(
89 | init_process_pid,
90 | "I will be executed on node2 and printed on node2 since the device ID passed was node2's init process"
91 | )
92 | end)
93 | ```
94 |
95 | Finally, we can also set the group leader of a process explicitly by calling `Process.group_leader/2`.
96 | In the following example, we set the group leader of the process running on node2 to node2's `:init` process:
97 |
98 |
99 |
100 | ```elixir
101 | Node.spawn_link(:node2@localhost, fn ->
102 | init_process_pid = Process.whereis(:init)
103 | Process.group_leader(self(), init_process_pid)
104 |
105 | IO.puts(
106 | "I will be executed on node2 and printed on node2 since the group leader is set to node2's init process"
107 | )
108 | end)
109 | ```
110 |
111 | In this case, the output of the `IO.puts` operation will be sent to node2's `:init` process, which is the new group leader of the process.
112 | Therefore, the output will be printed on node2's standard output stream.
113 |
114 | ## Process naming
115 |
116 | We can name processes and then refer to them via their registered name.
117 |
118 | ```elixir
119 | Process.register(self(), :my_process)
120 |
121 | Process.registered()
122 | |> Enum.any?(&(&1 == :my_process))
123 | |> IO.inspect(label: ":my_process registered?")
124 | ```
125 |
126 | ```elixir
127 | send(:my_process, "Hello")
128 | Process.info(self(), :messages)
129 | ```
130 |
131 | ```elixir
132 | Process.unregister(:my_process)
133 |
134 | Process.registered()
135 | |> Enum.any?(&(&1 == :my_process))
136 | |> IO.inspect(label: ":my_process registered?")
137 | ```
138 |
139 | ## Resources
140 |
141 | * https://www.erlang.org/doc/man/erlang.html#group_leader-0
142 | * https://stackoverflow.com/questions/36318766/what-is-a-group-leader
143 | * https://rokobasilisk.gitbooks.io/elixir-getting-started/content/io_and_the_file_system/processes_and_group_leaders.html
144 | * https://elixirschool.com/en/lessons/advanced/otp_distribution#a-note-on-io-and-nodes-2
145 |
146 | ## Navigation
147 |
148 |
162 |
--------------------------------------------------------------------------------
/chapters/ch_3.1_genserver_introduction.livemd:
--------------------------------------------------------------------------------
1 | # GenServer Introduction
2 |
3 | ## Navigation
4 |
5 |
19 |
20 | ## What is a Genserver?
21 |
22 | In OTP(Open Telecom Platform), we have several behaviors that formalize common patterns in programming. Behaviors can be thought of as design patterns for processes. Over time, programmers have identified common patterns of using processes in OTP and designed standardized interfaces to cater to such use cases.
23 |
24 | One such behavior is the GenServer(Generic Server), which comes bundled with OTP. Other examples of behaviors include Supervisors and Applications.
25 |
26 | At its most basic level, **a GenServer is a single process that runs a loop and handles one message per iteration, passing along an updated state**. By using the GenServer behavior and implementing the necessary callbacks, we can easily implement a client-server relation.
27 |
28 | A GenServer process starts by initializing its state and then enters a waiting state, anticipating incoming messages. Upon receiving a message, the process handles it, updates its state, and returns to the waiting state (genserver loop).
29 |
30 | A process can only execute when it receives a message. After initialization, a process simply waits for messages, an **idle process doesn't consume any resources**.
31 |
32 | ## Genserver callbacks
33 |
34 | In order to create a genserver we must first use the genserver behaviour by adding the following line to our module `use Genserver`
35 |
36 | After this we can implement the Genserver callbacks, a genserver has the following callbacks..
37 |
38 | * `init/1`
39 | * `handle_continue/2`
40 | * `handle_call/3`
41 | * `handle_cast/2`
42 | * `handle_info/2`
43 | * `terminate/2`
44 | * `format_status/2`
45 | * `code_change/3`
46 |
47 | These callbacks are called at various points in the lifecycle of a genserver. Lets build a simple `counter` to go through these callbacks one by one...
48 |
49 | ```elixir
50 | defmodule Counter do
51 | use GenServer
52 |
53 | @impl true
54 | def init(state) do
55 | IO.inspect("init called, initial counter state: #{state}")
56 | {:ok, state}
57 | end
58 |
59 | @impl true
60 | def handle_cast({:inc, value}, state) do
61 | {:noreply, state + value}
62 | end
63 |
64 | @impl true
65 | def handle_cast({:dec, value}, state) do
66 | {:noreply, state - value}
67 | end
68 |
69 | @impl true
70 | def handle_call(:get_count, _from, state) do
71 | {:reply, "The count is #{state}", state}
72 | end
73 |
74 | @impl true
75 | def handle_info(message, state) do
76 | IO.inspect("Handle info called with message #{inspect(message)}")
77 | {:noreply, state}
78 | end
79 |
80 | @impl true
81 | def terminate(reason, _state) do
82 | IO.inspect("Genserver Terminating with reason #{reason}...")
83 | end
84 | end
85 | ```
86 |
87 | With the above counter genserver code let us try to understand the different callbacks.
88 | We will enable tracing genserver messages via the [:sys.trace/2](https://www.erlang.org/doc/man/sys.html#trace-2) function from the erlang `sys` module.
89 |
90 | ```elixir
91 | IO.inspect("Starting Genserver")
92 | {:ok, pid} = GenServer.start(Counter, 0)
93 | IO.inspect("Genserver Started")
94 |
95 | # Start tracing the genserver processes
96 | :sys.trace(pid, true)
97 |
98 | # Increment counter by 10
99 | :ok = GenServer.cast(pid, {:inc, 10})
100 | # Decrement counter by 5
101 | :ok = GenServer.cast(pid, {:dec, 5})
102 | # Increment counter by 5
103 | :ok = GenServer.cast(pid, {:inc, 2})
104 | current_count = GenServer.call(pid, :get_count)
105 | IO.puts("Current count = #{current_count}")
106 |
107 | # Send a message to the genserver process
108 | send(pid, "Hi genserver!")
109 |
110 | # Stop the genserver
111 | GenServer.stop(pid, :boom)
112 | ```
113 |
114 | Let's analyze the lifecycle of the GenServer by examining the output of the above code. Firstly notice that all of the functions are marked with `@impl true` to signify that they are implementing the GenServer behavior.
115 |
116 | Each GenServer callback receives the current state of the process and has the opportunity to update it. The callbacks can also return various values like `:noreply`, `:reply`, `:continue`, `:stop`, `:hibernate`, etc. These values govern the GenServer's lifecycle.
117 |
118 | #### Starting the GenServer - init/2
119 |
120 | To start the GenServer, we call `GenServer.start(Counter, 0)` which starts the GenServer process as an **unlinked process** we can use `GenServer.start_link/3` to start it as a linked process. We pass it the GenServer module name and the initial state of our Counter GenServer process. The output indicates that the `GenServer.start/2` call is **synchronous** and waits until the `init/2` GenServer callback. Once started, the GenServer process pid is returned.
121 |
122 | #### handle_cast/2
123 |
124 | We then send different cast messages like :inc and :dec to the GenServer to modify the process state, which, in our case, increments or decrements the counter. The `handle_cast/2` GenServer callback handles these cast calls. It's important to remember that cast messages are **asynchronous** and the `GenServer.cast/2` call does not wait for the cast message to be processed. Also, using cast, the GenServer **cannot send a reply** back to the caller process, so we only receive a `:ok` as the return value when calling `GenServer.cast/2`.
125 |
126 | #### handle_call/3
127 |
128 | We then use `GenServer.call/3` to fetch the current count, which is handled by the `handle_call/3` GenServer callback. Unlike `GenServer.cast/2`, this is a **synchronous operation**, meaning the `GenServer.call/3` function call must wait until the GenServer finishes processing the message. It also **allows the GenServer to return a reply to the caller**. In our case, the Counter GenServer returns the current count as a string like "The count is #{state}". It's worth noting that the `handle_call/3` receives a from parameter, which contains the pid of the caller process.
129 |
130 | ### handle_info/2
131 |
132 | Next, we send a message to the GenServer process using the `send/2` function. It's important to remember that a GenServer can also receive messages like any other elixir process. The `handle_info/2` GenServer callback handles such messages that are not calls or casts. In our case, we simply log the message "Hi genserver!".
133 |
134 | #### terminate/2
135 |
136 | Finally, we stop the GenServer process by calling `GenServer.stop/2`, which invokes the `terminate/2` GenServer callback, and the GenServer process is stopped.
137 |
138 |
139 |
140 | You might be wondering when the other GenServer callbacks are invoked, lets go through them one by one....
141 |
142 | #### handle_continue/2
143 |
144 | Most GenServer callbacks have the option to return a value containing a continue instruction like `{:continue, continue_arg}`. When such a value is returned, the `handle_continue/2` callback is invoked to handle the continue instruction. This is useful for splitting the work in a callback into multiple steps and updating the process state along the way, or for performing work after initialization.
145 |
146 | For example, to initialize a GenServer, we may need to perform a time-consuming task within the init/2 callback, which would block the caller and prevent the GenServer from starting. To avoid this, we can return a value like `{:ok, state, {:continue, continue_arg}}`, which allows the GenServer to start and unblocks the caller. The handle_continue/2 callback is then immediately invoked, where we can set the GenServer state.
147 |
148 | #### format_status/2
149 |
150 | This callback is infrequently used, but it can be helpful when inspecting a GenServer state with functions like `:sys.get_state/1`. It defines a formatted version of the status.
151 |
152 | #### code_change/3
153 |
154 | This callback is also rarely used. It handles changes to the GenServer's state when a new version of a module is loaded ([hot code swapping](https://medium.com/blackode/how-to-perform-hot-code-swapping-in-elixir-afc824860012)) and the term structure of the state needs to be updated.
155 |
156 | ## The terminate callback
157 |
158 | The `terminate/2` callback is triggered when a GenServer is about to exit, allowing for any necessary cleanup operations. However, it is important to note that `terminate/2` is not always guaranteed to be called.
159 |
160 | `terminate/2` is only called when the GenServer is trapping exits using the `Process.flag(:trap_exit, true)` OR if in a callback we return a `:stop` tuple or `raise` and exception. We will later study about process supervisors which can stop a genserver using a `:brutal_kill` strategy which also does not result in a call to `terminate/2`.
161 |
162 | Therefore it is *not guaranteed* that `terminate/2` is called when a GenServer exits and we should not rely on it and place critical logic in this callback.
163 |
164 | When using `GenServer.stop/2` the terminate/2 callback will be invoked before exiting even if the GenServer process is not trapping exits.
165 |
166 | For further information, see the discussion [here](https://stackoverflow.com/a/39775617).
167 |
168 | ## Lifecycle of a GenServer
169 |
170 | A simplified overview of the lifecycle of a GenServer is given below
171 |
172 |
173 |
174 | 
175 |
176 |
177 |
178 | Now that we have got an overview of the workings of a GenServer lets look at some gotachas and key points related to GenServers...
179 |
180 | ## GenServer Key Points to Remember
181 |
182 | * A GenServer is a **single elixir process** that operates in a loop, processing messages from its mailbox in the **order** they are received.
183 | * If a message takes a long time to process, calling synchronous functions such as `GenServer.call/2` may result in timeouts. You can specify a longer timeout (the default is 5 seconds) or use multiple GenServers to avoid overloading a single process.
184 | * GenServer functions fall into two categories: synchronous functions, like `GenServer.call/3`, which wait for a response, and asynchronous functions, like `GenServer.cast/2`, which do not wait for a reply.
185 | * Prefer using `GenServer.call/2` instead of `GenServer.cast/2` to apply backpressure and avoid overwhelming the `GenServer` process. `GenServer.call/2` blocks the caller process until a reply is received, ensuring controlled interactions and preventing message overload.
186 | * Implementing GenServer callbacks is optional, as Elixir provides default implementations. For example, if you don't define `handle_cast/2`, Elixir will use [a default implementation](https://github.com/elixir-lang/elixir/blob/a64d42f5d3cb6c32752af9d3312897e8cd5bb7ec/lib/elixir/lib/gen_server.ex#L809) that raises an error when the GenServer receives a cast message.
187 | GenServer callbacks can return different values to control the process's lifecycle. For instance:
188 | * Returning `{:continue, term()}` tells the GenServer to continue processing the message, triggering the `handle_continue/2` callback.
189 | * Returning `{:stop, reason, new_state}` terminates the GenServer process.
190 | * Returning `:hibernate` puts the GenServer process to sleep, freeing up resources.
191 |
192 | ## References
193 |
194 | * [HexDocs: GenServer](https://hexdocs.pm/elixir/GenServer.html#call/3)
195 | * [ElixirLang: GenServer](https://elixir-lang.org/getting-started/mix-otp/genserver.html)
196 | * [Exercism: GenServer](https://exercism.org/tracks/elixir/concepts/genserver)
197 | * https://github.com/DockYard-Academy/curriculum/blob/main/reading/genservers.livemd
198 |
199 | ## Navigation
200 |
201 |
215 |
--------------------------------------------------------------------------------
/chapters/ch_3.2_building_a_genserver.livemd:
--------------------------------------------------------------------------------
1 | # Buliding a GenServer
2 |
3 | ## Navigation
4 |
5 |
19 |
20 | ## Building a GenServer from scratch
21 |
22 | Let's delve into crafting a simplified GenServer-like implementation using Elixir's fundamental primitives such as `spawn_link` and `send`. This exercise will give us a clearer insight into the inner workings of GenServers.
23 |
24 | For the sake of simplicity, we will focus on implementing only the commonly used callbacks: `init/1`, `handle_call/3`, `handle_cast/2`, and `handle_info/2`.
25 |
26 | ```elixir
27 | defmodule MyGenServer do
28 | # Callbacks to implement
29 | @callback init(term()) :: {:ok, term()}
30 | @callback handle_call(term(), pid(), term()) :: {:reply, term(), term()}
31 | @callback handle_cast(term(), term()) :: {:noreply, term()}
32 | @callback handle_info(term(), term()) :: {:noreply, term()}
33 |
34 | # == Public API ==
35 | def start_link(module, args) do
36 | {:ok, spawn_link(__MODULE__, :server_init, [module, args])}
37 | end
38 |
39 | def call(server_pid, args) do
40 | send(server_pid, {:call, self(), args})
41 |
42 | receive do
43 | {:response, response} -> response
44 | end
45 | end
46 |
47 | def cast(server_pid, args) do
48 | send(server_pid, {:cast, args})
49 | end
50 |
51 | def stop(server_pid, reason \\ :normal) do
52 | send(server_pid, {:stop, reason})
53 | end
54 |
55 | # == Internal implementation ==
56 | def server_init(module, args) do
57 | {:ok, state} = module.init(args)
58 | genserver_loop(module, state)
59 | end
60 |
61 | # Recursively loop and wait for messages
62 | def genserver_loop(module, state) do
63 | receive do
64 | {:call, parent_pid, args} ->
65 | {:reply, response, new_state} = module.handle_call(args, parent_pid, state)
66 | send(parent_pid, {:response, response})
67 | genserver_loop(module, new_state)
68 |
69 | {:cast, args} ->
70 | {:noreply, new_state} = module.handle_cast(args, state)
71 | genserver_loop(module, new_state)
72 |
73 | {:stop, reason} ->
74 | module.terminate(reason, state)
75 | exit(reason)
76 |
77 | request ->
78 | {:noreply, new_state} = module.handle_info(request, state)
79 | genserver_loop(module, new_state)
80 | end
81 | end
82 | end
83 | ```
84 |
85 | ## Using our GenServer
86 |
87 | ```elixir
88 | defmodule Stack do
89 | @behaviour MyGenServer
90 |
91 | @impl true
92 | def init(args) do
93 | {:ok, args}
94 | end
95 |
96 | @impl true
97 | def handle_call(:get_stack, _from, state) do
98 | {:reply, state, state}
99 | end
100 |
101 | @impl true
102 | def handle_call(:pop, _from, [num | state]) do
103 | {:reply, num, state}
104 | end
105 |
106 | @impl true
107 | def handle_cast({:push, num}, state) do
108 | IO.inspect(num, label: "PUSH")
109 | {:noreply, [num | state]}
110 | end
111 |
112 | @impl true
113 | def handle_info(:stats, state) do
114 | IO.inspect("Stack length: #{length(state)}")
115 | {:noreply, state}
116 | end
117 | end
118 | ```
119 |
120 | ```elixir
121 | {:ok, stack_server_pid} = MyGenServer.start_link(Stack, [])
122 | MyGenServer.cast(stack_server_pid, {:push, 1})
123 | MyGenServer.cast(stack_server_pid, {:push, 2})
124 | MyGenServer.cast(stack_server_pid, {:push, 3})
125 | MyGenServer.call(stack_server_pid, :get_stack) |> IO.inspect(label: "STACK")
126 | MyGenServer.call(stack_server_pid, :pop) |> IO.inspect(label: "POP")
127 | MyGenServer.call(stack_server_pid, :get_stack) |> IO.inspect(label: "STACK")
128 | send(stack_server_pid, :stats)
129 | ```
130 |
131 | ## Navigation
132 |
133 |
147 |
--------------------------------------------------------------------------------
/chapters/ch_3.3_genserver_examples.livemd:
--------------------------------------------------------------------------------
1 | # GenServer examples
2 |
3 | ## Navigation
4 |
5 |
19 |
20 | ## Password Manager
21 |
22 | Our objective is to create a straightforward password manager GenServer that can save user's passwords in its state.
23 |
24 | Notice how we have developed an API with functions like `save_password/3`, `get_password/1`, and `delete_password/1`. This API facilitates easy communication with the GenServer without needing to directly call GenServer functions like `GenServer.call/3` or `GenServer.cast/2`.
25 |
26 | Also notice how we have ensured that the GenServer code is kept to a minimum and have placed our password validation logic in a separate module. This approach of separating the logic into a purely functional module makes testing easier since the buissness logic can be tested in isolation without dealing with the GenServer.
27 |
28 | ```elixir
29 | defmodule Password do
30 | defstruct url: nil, username: nil, password: nil, inserted_at: nil
31 |
32 | @doc """
33 | Check if a password entry is valid
34 | """
35 | def validate_entry(%Password{url: url, username: username, password: password}) do
36 | with {:ok, _url} <- URI.new(url),
37 | {:ok, _username} <- validate_username(username),
38 | {:ok, _password} <- validate_password(password) do
39 | {:ok,
40 | %Password{
41 | url: url,
42 | username: username,
43 | password: password,
44 | inserted_at: DateTime.utc_now()
45 | }}
46 | end
47 | end
48 |
49 | # Helper functions
50 | defp validate_username(username) do
51 | cond do
52 | not is_binary(username) -> {:error, "Invalid username"}
53 | String.length(username) == 0 -> {:error, "Username is empty"}
54 | true -> {:ok, username}
55 | end
56 | end
57 |
58 | defp validate_password(password) do
59 | cond do
60 | not is_binary(password) -> {:error, "Invalid password"}
61 | String.length(password) < 3 -> {:error, "Password must be atleast 3 character long"}
62 | true -> {:ok, password}
63 | end
64 | end
65 | end
66 |
67 | defmodule PasswordManager do
68 | use GenServer
69 |
70 | # Public APIs
71 |
72 | def start_link(_opts) do
73 | GenServer.start_link(
74 | __MODULE__,
75 | %{},
76 | # Use the module name as the name of the GenServer Process
77 | name: __MODULE__
78 | )
79 | end
80 |
81 | def save_password(url, username, password) do
82 | entry = %Password{
83 | url: url,
84 | username: username,
85 | password: password
86 | }
87 |
88 | GenServer.call(__MODULE__, {:save_password, entry})
89 | end
90 |
91 | def get_password(url) do
92 | GenServer.call(__MODULE__, {:get_password, url})
93 | end
94 |
95 | def delete_password(url) do
96 | GenServer.cast(__MODULE__, {:delete_password, url})
97 | end
98 |
99 | def stop(), do: GenServer.stop(__MODULE__)
100 |
101 | # Callbacks
102 |
103 | @impl true
104 | def init(state) do
105 | {:ok, state}
106 | end
107 |
108 | @impl true
109 | def handle_call({:save_password, new_password}, _from, state) do
110 | case Password.validate_entry(new_password) do
111 | {:ok, entry} -> {:reply, :saved, Map.put(state, entry.url, entry)}
112 | {:error, reason} -> {:reply, {:error, reason}, state}
113 | end
114 | end
115 |
116 | @impl true
117 | def handle_call({:get_password, url}, _from, state) do
118 | case Map.get(state, url) do
119 | nil -> {:reply, :not_found, state}
120 | entry -> {:reply, entry, state}
121 | end
122 | end
123 |
124 | @impl true
125 | def handle_cast({:delete_password, url}, state) do
126 | state = Map.delete(state, url)
127 | {:noreply, state}
128 | end
129 | end
130 | ```
131 |
132 | ```elixir
133 | # Start the Password Manager Genserver
134 | {:ok, _pid} = PasswordManager.start_link(nil)
135 |
136 | PasswordManager.save_password("gmail.com", "john_doe@gmail.com", "12345")
137 | |> IO.inspect(label: "Saving Gmail creds")
138 |
139 | PasswordManager.save_password("spotify.com", "music4life", "ab")
140 | |> IO.inspect(label: "Saving Spotify creds")
141 |
142 | PasswordManager.save_password("apple.com", "iuser", "ilife")
143 | |> IO.inspect(label: "Saving Apple creds")
144 |
145 | PasswordManager.get_password("gmail.com") |> IO.inspect(label: "Gmail creds")
146 | PasswordManager.get_password("spotify.com") |> IO.inspect(label: "Spotify creds")
147 |
148 | PasswordManager.delete_password("gmail.com") |> IO.inspect(label: "Deleting Gmail")
149 | PasswordManager.get_password("gmail.com") |> IO.inspect(label: "Gmail creds")
150 |
151 | PasswordManager.stop()
152 | ```
153 |
154 | ### Testing GenServer
155 |
156 |
157 |
158 | Now lets try to write some tests for the above GenServer.
159 |
160 | ```elixir
161 | ExUnit.start()
162 |
163 | defmodule PasswordManagerTest do
164 | use ExUnit.Case
165 |
166 | describe "save_password/3" do
167 | test "saves password if password entry is valid" do
168 | {:ok, _pid} = PasswordManager.start_link(nil)
169 | assert :saved == PasswordManager.save_password("gmail.com", "john_doe@gmail.com", "12345")
170 |
171 | assert %Password{
172 | url: "gmail.com",
173 | username: "john_doe@gmail.com",
174 | password: "12345",
175 | inserted_at: _
176 | } = PasswordManager.get_password("gmail.com")
177 | end
178 |
179 | test "does not save password if password entry is invalid" do
180 | {:ok, _pid} = PasswordManager.start_link(nil)
181 |
182 | assert {:error, "Password must be atleast 3 character long"} ==
183 | PasswordManager.save_password("gmail.com", "john_doe@gmail.com", "12")
184 |
185 | assert :not_found == PasswordManager.get_password("gmail.com")
186 | end
187 | end
188 |
189 | describe "delete_password/3" do
190 | test "deletes password if password found" do
191 | {:ok, _pid} = PasswordManager.start_link(nil)
192 | assert :saved == PasswordManager.save_password("gmail.com", "john_doe@gmail.com", "12345")
193 | assert :ok == PasswordManager.delete_password("gmail.com")
194 |
195 | assert :not_found = PasswordManager.get_password("gmail.com")
196 | end
197 | end
198 | end
199 |
200 | ExUnit.run()
201 | ```
202 |
203 | Here the password validation logic can be tested independently, without having to start the GenServer in the tests. This method of testing is preferable since testing pure functions is generally much easier than testing async GenServer code.
204 |
205 | ## Cron Job
206 |
207 | Lets see another example of building a GenServer. In Elixir, you can easily create a basic CRON job using GenServers to execute a task periodically.
208 |
209 | ```elixir
210 | defmodule CronJob do
211 | use GenServer
212 |
213 | # Every 10 seconds
214 | @interval :timer.seconds(10)
215 |
216 | def start_link(_opts) do
217 | GenServer.start_link(__MODULE__, %{})
218 | end
219 |
220 | def init(state) do
221 | schedule_work()
222 | {:ok, state}
223 | end
224 |
225 | def handle_info(:work, state) do
226 | work()
227 | schedule_work()
228 | {:noreply, state}
229 | end
230 |
231 | defp schedule_work() do
232 | Process.send_after(self(), :work, @interval)
233 | end
234 |
235 | defp work() do
236 | IO.inspect("Working...")
237 | end
238 | end
239 |
240 | CronJob.start_link(nil)
241 | ```
242 |
243 | This works fine for simple use cases however, if you require more advanced functionality consider using a library such as [Quantum](https://github.com/quantum-elixir/quantum-core).
244 |
245 | ## Resources
246 |
247 | * https://hexdocs.pm/elixir/1.14.4/GenServer.html#reply/2
248 | * https://medium.com/blackode/2-unique-use-cases-of-genserver-reply-deep-insights-elixir-expert-31e7abbd42d1
249 |
250 | ## Navigation
251 |
252 |
266 |
--------------------------------------------------------------------------------
/chapters/ch_3.4_other_genserver_functions.livemd:
--------------------------------------------------------------------------------
1 | # Other GenServer functions
2 |
3 | ## Navigation
4 |
5 |
19 |
20 | ## GenServer.reply/2
21 |
22 | In the previous chapters, we learned that a GenServer is a single process that processes messages from its mailbox one at a time. When using `GenServer.call/3`, the calling process waits until the GenServer sends a reply.
23 |
24 | However, in some cases, a GenServer may receive a message that requires a time-consuming task, which can block the GenServer and prevent it from processing new messages while it handles the lengthy task.
25 |
26 | To avoid this issue, we can delegate the time-consuming task to a separate process, which allows the GenServer to continue handling new messages without being blocked. Once the time-consuming task is completed, the GenServer can reply back to the caller using `GenServer.reply/2`.
27 |
28 | **To put it simply,`GenServer.reply/2` can be used to send a reply back to a client that has called `GenServer.call/3`. This is especially useful when the reply cannot be specified in the return value of `handle_call/3`**
29 |
30 | ---
31 |
32 | To illustrate this concept, let's walk through an example.
33 |
34 | We will build a *FoodOrderingServer* which allows users to order for a food item or list past orders. Lets suppose the call to list the past order is a fast one however the call to place an order is a slow operation.
35 |
36 | Ideally we don't want to block other calls to the GenServer while its busy placing an order.
37 |
38 | ```elixir
39 | defmodule FoodOrderingServer do
40 | use GenServer
41 |
42 | # Public APIs
43 |
44 | def start_link(_opts) do
45 | GenServer.start_link(__MODULE__, %{}, name: __MODULE__)
46 | end
47 |
48 | def place_order(user, item) do
49 | # We specify a timeout of 10 seconds to avoid timeout errors,
50 | # since placing an order takes a lot of time
51 | GenServer.call(__MODULE__, {:place_order, user, item}, 10000)
52 | end
53 |
54 | def list_orders(user) do
55 | GenServer.call(__MODULE__, {:list_orders, user})
56 | end
57 |
58 | # Callbacks
59 |
60 | @impl true
61 | def init(_args) do
62 | {:ok, %{}}
63 | end
64 |
65 | @impl true
66 | def handle_call({:place_order, user, item}, from, state) do
67 | IO.puts("Recieved new order request from #{inspect(from)}")
68 |
69 | spawn(fn ->
70 | # Simulate placing order which takes 6 seconds
71 | :timer.sleep(6000)
72 | send(__MODULE__, {:order_placed, user, item, from})
73 | end)
74 |
75 | # Notice how we return :noreply here
76 | # (the caller process will be blocked and waiting since we did not reply)
77 | {:noreply, state}
78 | end
79 |
80 | @impl true
81 | def handle_call({:list_orders, user}, _from, state) do
82 | {:reply, Map.get(state, user, []), state}
83 | end
84 |
85 | @impl true
86 | def handle_info({:order_placed, user, item, from}, state) do
87 | state =
88 | Map.update(
89 | state,
90 | user,
91 | [item],
92 | fn existing_orders -> [item | existing_orders] end
93 | )
94 |
95 | IO.puts("Order #{item} ready for #{user}")
96 | # Send reply to the caller who is waiting for the order
97 | GenServer.reply(from, {:ok, :order_placed})
98 | {:noreply, state}
99 | end
100 | end
101 | ```
102 |
103 | ```elixir
104 | # Stop the Server if its already running
105 | Process.whereis(FoodOrderingServer) |> GenServer.stop()
106 |
107 | {:ok, _pid} = FoodOrderingServer.start_link(nil)
108 |
109 | # This two lines of code will execute one by one synchornously since the current process will be
110 | # waiting untill the order is placed and the GenServer replies back
111 | FoodOrderingServer.place_order("Jhon", "sandwich")
112 | FoodOrderingServer.place_order("Tom", "pizza")
113 | ```
114 |
115 | ```elixir
116 | # Ordering simultaneously from different processes
117 |
118 | spawn(fn -> FoodOrderingServer.place_order("Jhon", "burger") end)
119 |
120 | spawn(fn ->
121 | FoodOrderingServer.place_order("Tom", "ice cream")
122 |
123 | FoodOrderingServer.list_orders("Tom")
124 | |> IO.inspect(label: "Toms orders")
125 | end)
126 |
127 | spawn(fn ->
128 | FoodOrderingServer.list_orders("Tom")
129 | |> IO.inspect(label: "Toms orders")
130 | end)
131 | ```
132 |
133 | #### Code Breakdown
134 |
135 | In the given code, we initially place orders one by one and observe that each call to `FoodOrderingServer.place_order/2` waits for the GenServer's reply before proceeding.
136 |
137 | To simulate multiple users placing and listing their orders simultaneously, we spawn two processes and place orders from each of them concurrently. Since the GenServer delegates the order placing task to another process, it is not blocked and can immediately respond to both orders. Once the orders are processed, the processes send a message back to the GenServer via `send(__MODULE__, {:order_placed, user, item, from})` which is handled by the `handle_info/2` callback, after which the GenServer replies back to the callers who were awaiting the reply using `GenServer.reply(from, {:ok, :order_placed})`.
138 |
139 | This design unblocks the GenServer, allowing it to always respond to messages promptly.
140 |
141 | It's important to note that `GenServer.reply/2` can be invoked from any process, not just from within the GenServer process. In our example, we could have called `GenServer.reply(from, {:ok, :order_placed})` from the spawned processes.
142 |
143 | This is possible because the `from` parameter holds the PID of the caller along with a `reference` that enables the caller to recognize that the message came as a reply for the `GenServer.call/2` that it was waiting for.
144 |
145 | ---
146 |
147 |
148 |
149 | (Note: There are 2 other functions `GenServer.abcast/3` and `GenServer.multi_call/4` which allows us to cast and call multiple GenServers at a time and can be useful in a distributed environment.)
150 |
151 | ## Navigation
152 |
153 |
167 |
--------------------------------------------------------------------------------
/chapters/ch_4.0_the_registry_module.livemd:
--------------------------------------------------------------------------------
1 | # The Registry module
2 |
3 | ## Navigation
4 |
5 |
19 |
20 | ## What is Registry?
21 |
22 | From the official documentation
23 |
24 | > A local, decentralized and scalable key-value process storage. It allows developers to lookup one or more processes with a given key.
25 |
26 | Lets go through some important points about registries...
27 |
28 | * The Registry in Elixir is a **process store** that stores **key-value** pairs, allowing us to register a process under a specific name.
29 | * There are two types of Registries: **unique and duplicate**. A unique Registry only permits one process to be registered under a given name, while a duplicate Registry permits multiple processes to be registered under the same name.
30 | * Each entry in the Registry is associated with the process that registered it. If the process crashes, the Registry automatically removes the keys associated with that process.
31 | * The Registry **compares keys using the match operation (===/2)**.
32 | * **Partitioning** the Registry is possible, allowing for more scalable behavior in highly concurrent environments with thousands or millions of entries.
33 | * The Registry **uses ETS tables** to store data under the hood.
34 | * Registries can **only be run locally** and donot support distributed access.
35 |
36 | ## Where to use Registry?
37 |
38 | The most common use of Registry is to name process. The `:via`is frequently used to specify the process name when using the Registry.
39 |
40 | In addition to process naming, the Registry offers other useful features such as a dispatch mechanism that enables developers to implement custom logic for request initiation. With this dispatching mechanism, developers can build scalable and highly efficient systems, such as a local PubSub, by utilizing the `dispatch/3` function.
41 |
42 | ## Naming processes using Registry
43 |
44 | The most common use of Registry is in naming processes.
45 |
46 | First we start the Registry process
47 |
48 | ```elixir
49 | # We start a Registry process and name it "Registry.ProcessStore"
50 | # Notice we use `keys: :unique` option which means every key in the Registry
51 | # will point to a single process
52 | {:ok, _} = Registry.start_link(keys: :unique, name: Registry.ProcessStore)
53 | ```
54 |
55 | Now lets use this registry to name a GenServer
56 |
57 | ```elixir
58 | defmodule Stack do
59 | use GenServer
60 |
61 | # Callbacks
62 |
63 | @impl true
64 | def init(stack) do
65 | {:ok, stack}
66 | end
67 |
68 | @impl true
69 | def handle_cast({:push, e}, stack) do
70 | {:noreply, [e | stack]}
71 | end
72 |
73 | # Other Callbacks ....
74 | end
75 | ```
76 |
77 | Now that we have a simple GenServer lets try to start 2 instances of this GenServer and name each of them using the Registry.
78 |
79 | ```elixir
80 | # Start the Stack GenServer and register it under the "Registry.ProcessStore"
81 | # with a key named "stack_server_1"
82 | GenServer.start_link(
83 | Stack,
84 | [],
85 | name: {:via, Registry, {Registry.ProcessStore, "stack_server_1"}}
86 | )
87 |
88 | # Start another instance of the Stack server with the name "stack_server_2"
89 | # Notice how we also store an optional value associated with this process `:second_stack`
90 | GenServer.start_link(
91 | Stack,
92 | [],
93 | name: {:via, Registry, {Registry.ProcessStore, "stack_server_2", :second_stack}}
94 | )
95 | ```
96 |
97 | When we register a process under a Registry, we have the option to store an associated metadata with that entry. In the second example mentioned above, we not only registered an instance of our Stack GenServer process under the registry but also stored the value `:second_stack` along with its corresponding entry.
98 |
99 | Now lets call our Stack GenServer using its Registered name, we can use the `lookup/2` function that returns a list like `[{pid(), value()}]`. For Registries that allow duplicate entries a lookup can return multiple entries in this list.
100 |
101 | ```elixir
102 | # Since we use an unique Registry, its guaranteed we will only get atmost
103 | # one process under the name "stack_server"
104 | [{stack_server_one_pid, nil}] = Registry.lookup(Registry.ProcessStore, "stack_server_1")
105 |
106 | GenServer.cast(stack_server_one_pid, {:push, "stack1"})
107 |
108 | [{stack_server_two_pid, value}] = Registry.lookup(Registry.ProcessStore, "stack_server_2")
109 | IO.inspect(value, label: "Stack server 2 value")
110 | GenServer.cast(stack_server_two_pid, {:push, "stack2"})
111 |
112 | IO.inspect(:sys.get_state(stack_server_one_pid), label: "Stack Server1 state")
113 | IO.inspect(:sys.get_state(stack_server_two_pid), label: "Stack Server2 state")
114 | ```
115 |
116 | Let us now explore how a Registry operates when we permit the storage of duplicate entries.
117 |
118 | When utilizing duplicate registries, it is not possible to use the :via option. To illustrate how duplicate registries function, let us attempt to register the current process twice using the `register/3` function.
119 |
120 | ```elixir
121 | {:ok, _} = Registry.start_link(keys: :duplicate, name: Registry.DupProcessStore)
122 |
123 | {:ok, _} = Registry.register(Registry.DupProcessStore, "async_city", :hello)
124 |
125 | {:ok, _} = Registry.register(Registry.DupProcessStore, "async_city", :world)
126 |
127 | Registry.lookup(Registry.DupProcessStore, "async_city")
128 | ```
129 |
130 | Observe how the invocation of `Registry.lookup/2` resulted in a list containing 2 tuples, each representing a process along with its associated metadata. These two processes were registered under the identical name, "async_city".
131 |
132 | ## Dispatching using Registry
133 |
134 | Dispatching allows us to fetch all entries for all processes registered under a given key. We pass a callback function which would receive the list of `{pid, value}` for every entry registered under the given key.
135 |
136 | It is worth noting that dispatching takes place in the process that initiates the `dispatch/3` call, either serially or concurrently in the case of multiple partitions.
137 |
138 | To better understand the concept of dispatching, let us take a look at an example.
139 |
140 | ```elixir
141 | # Start a Registry which allows duplicates
142 | {:ok, _} = Registry.start_link(keys: :duplicate, name: Registry.Numbers)
143 |
144 | # Register the current process 3 times under the same key "odd"
145 | # Save a value along with registration that is 1, "3" and fn -> 5
146 | {:ok, _} = Registry.register(Registry.Numbers, "odd", 1)
147 | {:ok, _} = Registry.register(Registry.Numbers, "odd", "3")
148 | {:ok, _} = Registry.register(Registry.Numbers, "odd", fn -> 5 end)
149 |
150 | # Register the current process 3 times under another key "even"
151 | {:ok, _} = Registry.register(Registry.Numbers, "even", 2)
152 | {:ok, _} = Registry.register(Registry.Numbers, "even", "4")
153 | {:ok, _} = Registry.register(Registry.Numbers, "even", fn -> 6 end)
154 |
155 | # Dispatching on processes registered under the key "odd"
156 | Registry.dispatch(Registry.Numbers, "odd", fn entries ->
157 | for {_pid, num} <- entries do
158 | cond do
159 | is_number(num) -> num
160 | is_binary(num) -> String.to_integer(num)
161 | is_function(num) -> num.()
162 | end
163 | |> IO.inspect(label: "ODD")
164 | end
165 | end)
166 |
167 | # Dispatching on processes registered under the key "even"
168 | Registry.dispatch(Registry.Numbers, "even", fn entries ->
169 | for {_pid, num} <- entries do
170 | cond do
171 | is_number(num) -> num
172 | is_binary(num) -> String.to_integer(num)
173 | is_function(num) -> num.()
174 | end
175 | |> IO.inspect(label: "EVEN")
176 | end
177 | end)
178 | ```
179 |
180 | ### Building a pubsub system with Registry
181 |
182 | We can also use this `dispatch/3` function to implement a local, non-distributed PubSub.
183 | This works by registering multiple processes under a given key which acts like a pubsub topic.
184 |
185 | We can then send a message to all processes registered under a key to emulate a pubsub broadcast. Here we also set the number of partitions to the number of schedulers online, which will make the registry more performant on highly concurrent environments.
186 |
187 | Lets see this in action.
188 |
189 | ```elixir
190 | {:ok, _} =
191 | Registry.start_link(
192 | keys: :duplicate,
193 | name: Registry.ChatPubSub,
194 | # The number of schedulers available in the VM
195 | partitions: System.schedulers_online()
196 | )
197 |
198 | # Register the current process under the "Registry.ChatPubSub" registery with a key "chat_room:1"
199 | {:ok, _} = Registry.register(Registry.ChatPubSub, "chat_room:1", [])
200 |
201 | # Dispatching by looking up all process registered with the key "chat_room:1" in the
202 | # "Registry.ChatPubSub" registry and then sending them a message.
203 | Registry.dispatch(Registry.ChatPubSub, "chat_room:1", fn entries ->
204 | for {pid, _} <- entries, do: send(pid, {:broadcast, "hello world"})
205 | end)
206 |
207 | # Receive any broadcasted messages
208 | receive do
209 | {:broadcast, message} -> IO.inspect(message, label: "Received broadcast")
210 | end
211 | ```
212 |
213 | By using this approach, we can register multiple processes under a single key within a Registry and subsequently dispatch messages to all the processes associated with that key.
214 |
215 | ## Other registry functions and match specs
216 |
217 | Apart from the `register/3` and `lookup/2` functions, the Registry module has several other useful functions which allows us to find and manipulate data inside the Registry. Most of these functions are straightforward to understand.
218 |
219 | However, its worth noting that some functions use match specs to find matching entries from the Registry let us look at some examples to understand how match specs work.
220 |
221 | From the official documentation
222 |
223 | > A match spec is a pattern that must be an atom or a tuple that will match the structure of the value stored in the registry. The atom `:_` can be used to ignore a given value or tuple element, while the atom `:"$1"` can be used to temporarily assign part of pattern to a variable for a subsequent comparison.
224 |
225 | > Optionally, it is possible to pass a list of guard conditions for more precise matching. Each guard is a tuple, which describes checks that should be passed by assigned part of pattern. For example the `$1 > 1` guard condition would be expressed as the `{:>, :"$1", 1}` tuple. Please note that guard conditions will work only for assigned variables like :"$1", :"$2", and so forth.
226 |
227 | Lets consider the `match/4` functions in the Registry module that returns entries from the Registry that matches the match spec passed.
228 |
229 | ```elixir
230 | Registry.start_link(keys: :duplicate, name: Registry.MatchSpec)
231 |
232 | # Register the current process multiple times with different values under the key "my_key"
233 | {:ok, _} = Registry.register(Registry.MatchSpec, "my_key", 1)
234 | {:ok, _} = Registry.register(Registry.MatchSpec, "my_key", "one")
235 | {:ok, _} = Registry.register(Registry.MatchSpec, "my_key", {1, 2})
236 | {:ok, _} = Registry.register(Registry.MatchSpec, "my_key", {2, 1})
237 | {:ok, _} = Registry.register(Registry.MatchSpec, "my_key", {2, 2})
238 |
239 | # Use different match specs to find matching entries from the Registry under the key "my_key"
240 | Registry.match(Registry.MatchSpec, "my_key", 1)
241 | |> IO.inspect(label: "* match spec: 1 returned")
242 |
243 | Registry.match(Registry.MatchSpec, "my_key", :_)
244 | |> IO.inspect(label: "* match spec: :_ returned")
245 |
246 | Registry.match(Registry.MatchSpec, "my_key", {2, :_})
247 | |> IO.inspect(label: "* match spec: {2, :_} returned")
248 |
249 | Registry.match(Registry.MatchSpec, "my_key", {:"$1", :"$1"})
250 | |> IO.inspect(label: ~s(* match spec: {:"$1", :"$1"} returned))
251 |
252 | # Also using guards along with match specs
253 | Registry.match(Registry.MatchSpec, "my_key", {:"$1", :"$2"}, [{:>, :"$1", :"$2"}])
254 | |> IO.inspect(label: ~s(* match spec: {:"$1", :"$2"} with guard [{:>, :"$1", :"$2"}] returned))
255 |
256 | Registry.match(Registry.MatchSpec, "my_key", :"$1", [{:is_binary, :"$1"}])
257 | |> IO.inspect(label: ~s(* match spec: :"$1" with guard [{:is_binary, :"$1"}] returned"))
258 | ```
259 |
260 | Other functions like `count_match/4`, `select/2`, etc in the Registry module also use match specs for filtering entries in the Registry.
261 |
262 | ## Resources
263 |
264 | * The official Registry documentition: https://hexdocs.pm/elixir/1.14.4/Registry.html#content
265 | * Guards in elixir: https://hexdocs.pm/elixir/1.14/patterns-and-guards.html#guards
266 |
267 | ## Navigation
268 |
269 |
283 |
--------------------------------------------------------------------------------
/chapters/ch_5.1_supervisors_introduction.livemd:
--------------------------------------------------------------------------------
1 | # Introduction to Supervisors
2 |
3 | ## Navigation
4 |
5 |
19 |
20 | ## What is a Supervisor?
21 |
22 | In our previous lesson on OTP, we explored the Genserver behavior. However, there's another critical behavior in OTP that deserves our attention: the **supervisor**.
23 |
24 | Supervisors fulfill the role of overseeing other processes, often referred to as child processes, and contribute to the creation of a hierarchical process structure called a **supervision tree**. This tree not only ensures fault-tolerance but also governs the application's startup and shutdown processes.
25 |
26 | Supervisors are the driving force behind the Elixir developer's inclination towards embracing the "let it crash" or "fail fast" philosophy. This approach allows supervisors to automatically restart crashed processes, facilitating a more robust system.
27 |
28 |
29 |
30 | To better understand how supervisors work, let's examine a simple example. We'll create a Stack Genserver module with a bug that causes it to crash when attempting to pop an element from an empty stack. Since our Genserver is supervised, we can observe how the supervisor automatically restarts the failed Genserver process when it crashes.
31 |
32 | ```elixir
33 | defmodule Stack do
34 | use GenServer
35 |
36 | def start_link(%{initial_value: value, name: name}) do
37 | GenServer.start_link(__MODULE__, value, name: name)
38 | end
39 |
40 | ## Callbacks
41 |
42 | @impl true
43 | def init(arg) do
44 | IO.puts("Stack GenServer starting up!")
45 | {:ok, [arg]}
46 | end
47 |
48 | @impl true
49 | def handle_call({:push, element}, _from, stack) do
50 | IO.puts("Pushed #{inspect(element)}")
51 | {:reply, :pushed, [element | stack]}
52 | end
53 |
54 | @impl true
55 | def handle_cast(:pop, [popped | stack]) do
56 | IO.puts("Popped #{inspect(popped)}")
57 | {:noreply, stack}
58 | end
59 | end
60 | ```
61 |
62 | ```elixir
63 | children = [
64 | %{
65 | id: :stack_1,
66 | # The Stack is a child porcess started via Stack.start_link/1
67 | start: {Stack, :start_link, [%{initial_value: 0, name: :stack_1}]}
68 | }
69 | ]
70 |
71 | # Now we start the supervisor process and pass it the list of child specs (child processes to supervise)
72 | # On starting the supervisor, it automatically starts all the child processes and supervises them
73 | {:ok, supervisor_pid} = Supervisor.start_link(children, strategy: :one_for_one)
74 |
75 | # After the supervisor starts, we can query the supervisor for information regarding all child processes supervised under it
76 | Supervisor.which_children(supervisor_pid) |> IO.inspect(label: "Supervisor's children")
77 | ```
78 |
79 | Now lets see what happens if our Stack Genserver process crashes
80 |
81 | ```elixir
82 | GenServer.whereis(:stack_1) |> IO.inspect(label: "Stack Genserver Process pid")
83 |
84 | :sys.get_state(GenServer.whereis(:stack_1))
85 | |> IO.inspect(label: "Intial Genserver state")
86 |
87 | GenServer.call(:stack_1, {:push, 10})
88 | GenServer.call(:stack_1, {:push, 20})
89 | GenServer.cast(:stack_1, :pop)
90 | GenServer.cast(:stack_1, :pop)
91 | GenServer.cast(:stack_1, :pop)
92 |
93 | :sys.get_state(GenServer.whereis(:stack_1))
94 | |> IO.inspect(label: "Genserver state just before crash")
95 |
96 | # Boom! Stack genserver crashes..
97 | GenServer.cast(:stack_1, :pop)
98 |
99 | # wait for the supervisor to restart the Stack Server process
100 | Process.sleep(200)
101 | GenServer.whereis(:stack_1) |> IO.inspect(label: "Restarted stack Genserver Process pid")
102 | :sys.get_state(GenServer.whereis(:stack_1)) |> IO.inspect(label: "Genserver state after crash")
103 | ```
104 |
105 | ## Child Specs
106 |
107 | When starting a supervisor, we have the option to provide a list of child specifications that dictate how the supervisor should handle starting, stopping, and restarting each child process.
108 |
109 | A supervisor can supervise two types of processes: workers and other supervisor processes. The former is commonly known as a `worker`, while the latter is referred to as a `supervisor`, typically forming a supervision tree.
110 |
111 | A child specification is represented as a map with up to six elements. The first two elements are mandatory, while the remaining ones are optional.
112 |
113 | Lets go through the different options that we can specify in the supervisor child spec
114 |
115 | * `:id` - This key is **required** and serves as an internal identifier used by the supervisor to identify the child specification. It should be unique among the workers within the same supervisor.
116 |
117 | * `:start` - This key is **required** and contains a tuple specifying the module, function, and arguments used to start the child process.
118 |
119 | * `:restart` - This optional key, defaulted to `:permanent`, is an atom that determines when a terminated child process should be restarted.
120 |
121 | * `:shutdown` - This optional key, defaulted to 5_000 (5 seconds) for workers and `:infinity` for supervisors, specifies how a child process should be terminated, either by an integer representing a timeout or the atom `:infinity`.
122 |
123 | * `:type` - This optional key, defaulted to `:worker`, specifies whether the child process is a `:worker` or a `:supervisor`.
124 |
125 | * `:modules` - This optional key contains a list of modules used by hot code upgrade mechanisms to identify processes using specific modules.
126 |
127 | A child specification can be defined in one of three ways:
128 |
129 | 1. As a map representing the child specification itself.
130 |
131 | ```elixir
132 | children = [
133 | %{
134 | id: :stack_1,
135 | start: {Stack, :start_link, [%{initial_value: 0, name: :stack_1}]}
136 | }
137 | ]
138 | ```
139 |
140 | The above example defines a child with `:id` of `:stack_1`, which is started by invoking `Stack.start_link(%{initial_value: 0, name: :stack_1})`.
141 |
142 | 2. As a tuple with the module name as the first element and the start argument as the second.
143 |
144 | ```elixir
145 | children = [
146 | {Stack, %{initial_value: 0, name: :stack_1}}
147 | ]
148 | ```
149 |
150 | When using this shorthand notation, the supervisor calls `Stack.child_spec(%{initial_value: 0, name: :stack_1})` to retrieve the child specification. The `Stack` module is responsible for defining its own `child_spec/1` function.
151 |
152 | The `Stack` module can define its child specification as follows:
153 |
154 | ```elixir
155 | def child_spec(arg) do
156 | %{
157 | id: Stack,
158 | start: {Stack, :start_link, [arg]}
159 | }
160 | end
161 | ```
162 |
163 | In this case, since `GenServer` already defines `Stack.child_spec/1`, we can leverage the automatically generated `child_spec/1` function and customize it by passing options directly to `use GenServer`. We will see examples of this in later chapters
164 |
165 | 3. Alternatively, a child specification can be specified by providing only the module name.
166 |
167 | ```elixir
168 | children = [Stack]
169 | ```
170 |
171 | This is equivalent to `{Stack, []}`. However, in our case, it would be invalid since `Stack.start_link/1` requires an initial value, and passing an empty list wouldn't work.
172 |
173 | ### The `Supervisor.child_spec/2` function
174 |
175 | When using the shorthand notations mentioned above, such as the `{module, arg}` tuple or a module name only as a child specification, we can modify the generated child specifications using the `Supervisor.child_spec/2` function.
176 |
177 | * When a two-element tuple of the form `{module, arg}` is provided, the child specification is retrieved by calling `module.child_spec(arg)`.
178 |
179 | * When only a module is given, the child specification is retrieved by calling `module.child_spec([])`.
180 |
181 | After retrieving the child specification, any overrides specified in the function argument are applied directly to the child spec.
182 |
183 | For example, we can use the shorthand notation `{Stack, %{initial_value: 0, name: :stack_1}}`, but this would set `id: Stack` as the child's identifier since it is the default behavior of `module.child_spec(arg)`. However, we can override this behavior as shown below:
184 |
185 |
186 |
187 | ```elixir
188 | children = [
189 | Supervisor.child_spec({Stack, %{initial_value: 0, name: :stack_1}}, id: :special_stack)
190 | ]
191 | ```
192 |
193 | ## Resources
194 |
195 | * https://hexdocs.pm/elixir/1.14.4/Supervisor.html
196 | * https://elixir-lang.org/getting-started/mix-otp/supervisor-and-application.html
197 |
198 | ## Navigation
199 |
200 |
214 |
--------------------------------------------------------------------------------
/chapters/ch_5.2_supervision_strategies.livemd:
--------------------------------------------------------------------------------
1 | # Supervision strategies
2 |
3 | ```elixir
4 | Mix.install([
5 | {:kino, "~> 0.9.0"}
6 | ])
7 | ```
8 |
9 | ## Navigation
10 |
11 |
25 |
26 | ## Supervision strategies
27 |
28 | When starting a supervisor, we have the ability to specify a supervision strategy. This strategy determines the actions taken by the supervisor when one of its child processes crashes.
29 |
30 | In the previous chapter, we started the supervisor for our Stack GenServer process using the `Supervisor.start_link(children, strategy: :one_for_one)` function call.
31 |
32 | Here, the `:strategy` option passed to the supervisor refers to the supervision strategy being used.
33 |
34 | Now, let's explore each of the supervision strategies in detail.
35 |
36 | To illustrate the different strategies, we'll consider a simple GenServer that crashes if we send it a `:boom` message. This GenServer stores a random positive integer in its state.
37 |
38 | ```elixir
39 | defmodule CrashDummyServer do
40 | use GenServer
41 |
42 | def start_link(name) do
43 | random_state = System.unique_integer([:positive])
44 | GenServer.start_link(__MODULE__, {random_state, name}, name: name)
45 | end
46 |
47 | ## Callbacks
48 |
49 | @impl true
50 | def init({random_value, name}) do
51 | IO.inspect("#{name} starting up!")
52 | {:ok, random_value}
53 | end
54 |
55 | @impl true
56 | def handle_cast(:boom, state) do
57 | process_pid = self() |> inspect()
58 | raise "BOOM! CrashDummyServer process: #{process_pid} crashed!"
59 | {:noreply, state}
60 | end
61 | end
62 | ```
63 |
64 | In the examples so far, we started a supervisor by directly calling the `Supervisor.start_link/2` function with the required options. However we can also define the supervisor as a module instead.
65 |
66 | To do so we have to use the `Supervisor` otp behavior in our module.
67 |
68 | ```elixir
69 | defmodule CrashDummySupervisor do
70 | # Using this behaviour we will automatically define a child_spec/1 function
71 | use Supervisor
72 |
73 | def start_link(strategy) do
74 | Supervisor.start_link(__MODULE__, strategy, name: __MODULE__)
75 | end
76 |
77 | # We have to implement this `init/1` callback when using the "Supervisor" behaviour
78 | @impl true
79 | def init(strategy) do
80 | # Supervision tree
81 | children = [
82 | child_spec(:dummy1),
83 | child_spec(:dummy2),
84 | child_spec(:dummy3)
85 | ]
86 |
87 | # Notice the supervision strategy
88 | Supervisor.init(children, strategy: strategy)
89 | end
90 |
91 | defp child_spec(name) do
92 | Supervisor.child_spec({CrashDummyServer, name}, id: name)
93 | end
94 | end
95 | ```
96 |
97 | In the above code snippet, we define multiple instances of our "CrashDummyServer" GenServer within the supervision tree. When the supervisor is started, it automatically starts three instances (processes) of the CrashDummyServer with the names `:dummy1`, `:dummy2`, and `:dummy3`.
98 |
99 | Since we want to start three processes of the same GenServer, we cannot use the `{CrashDummyServer, name}` child specification because it would assign the module name as the `:id`, resulting in the same `:id` being given to all three processes. To avoid this, we use the `Supervisor.child_spec/2` function and explicitly pass a separate `:id` to each process.
100 |
101 | The supervision strategy is passed as an argument to the `start_link/1` function and `init/1` callback so that we can restart the same supervisor with a different supervision strategy.
102 |
103 | ## :one_for_one
104 |
105 | With the "one_for_one" supervision strategy, if a child process terminates, only that specific process is restarted. In other words, if there are multiple child processes supervised by our supervisor and one of them crashes, only the crashed process is restarted while the other supervised processes continue running unaffected.
106 |
107 |
108 |
109 | 
110 |
111 |
112 |
113 | To observe the behavior of this strategy, we can start the supervisor and then intentionally crash one of the supervised processes to see the restart in action.
114 |
115 | We will use [Kino](https://hexdocs.pm/kino/) to draw the supervision tree before and after the crash.
116 |
117 | ```elixir
118 | {:ok, supervisor_pid} = CrashDummySupervisor.start_link(:one_for_one)
119 |
120 | Process.info(supervisor_pid, :links) |> IO.inspect(label: "Supervisors links")
121 |
122 | Supervisor.which_children(supervisor_pid) |> IO.inspect(label: "Supervisors Children")
123 |
124 | :sys.get_state(GenServer.whereis(:dummy1)) |> IO.inspect(label: "Dummy 1 state")
125 | :sys.get_state(GenServer.whereis(:dummy2)) |> IO.inspect(label: "Dummy 2 state")
126 | :sys.get_state(GenServer.whereis(:dummy3)) |> IO.inspect(label: "Dummy 3 state")
127 |
128 | Kino.Process.render_sup_tree(supervisor_pid)
129 | ```
130 |
131 | ```elixir
132 | # Makes the dummy2 child crash
133 | GenServer.cast(:dummy2, :boom)
134 | # Wait for the process to crash and be restarted
135 | Process.sleep(200)
136 |
137 | Supervisor.which_children(supervisor_pid) |> IO.inspect(label: "Supervisors Children")
138 |
139 | :sys.get_state(GenServer.whereis(:dummy1)) |> IO.inspect(label: "Dummy 1 state")
140 | :sys.get_state(GenServer.whereis(:dummy2)) |> IO.inspect(label: "Dummy 2 state")
141 | :sys.get_state(GenServer.whereis(:dummy3)) |> IO.inspect(label: "Dummy 3 state")
142 |
143 | Kino.Process.render_sup_tree(supervisor_pid)
144 | ```
145 |
146 | Based on the example, we can confirm that when using the `:one_for_one` supervision strategy, only the `:dummy2` GenServer process crashed and was subsequently restarted. As a result, the restarted process obtained a new process ID and its state was reset. On the other hand, the `:dummy1` and `:dummy3` processes continued to run without any interruption, maintaining their respective process IDs and states unchanged.
147 |
148 | ## :one_for_all
149 |
150 | Upon restarting the CrashDummySupervisor with the `:one_for_all` restart strategy, if any child process terminates, all other child processes will be terminated as well. Following that, all child processes, including the terminated one, will be restarted.
151 |
152 |
153 |
154 | 
155 |
156 |
157 |
158 | Let's proceed with restarting the `CrashDummySupervisor` using the `:one_for_all` strategy.
159 |
160 | ```elixir
161 | # Stop the existing supervisor process
162 | # We used the module name as the Supervisor process name so we can use the module name to stop
163 | # the supervisor process.
164 | # This will also terminate the supervision tree and all process running under our supervisor
165 | Supervisor.stop(CrashDummySupervisor)
166 |
167 | {:ok, supervisor_pid} = CrashDummySupervisor.start_link(:one_for_all)
168 |
169 | Supervisor.which_children(supervisor_pid) |> IO.inspect(label: "Supervisors Children")
170 |
171 | :sys.get_state(GenServer.whereis(:dummy1)) |> IO.inspect(label: "Dummy 1 state")
172 | :sys.get_state(GenServer.whereis(:dummy2)) |> IO.inspect(label: "Dummy 2 state")
173 | :sys.get_state(GenServer.whereis(:dummy3)) |> IO.inspect(label: "Dummy 3 state")
174 |
175 | Kino.Process.render_sup_tree(supervisor_pid)
176 | ```
177 |
178 | ```elixir
179 | # Makes the dummy2 child crash
180 | GenServer.cast(:dummy2, :boom)
181 | # Wait for the process to crash and be restarted
182 | Process.sleep(200)
183 |
184 | Supervisor.which_children(supervisor_pid) |> IO.inspect(label: "Supervisors Children")
185 |
186 | :sys.get_state(GenServer.whereis(:dummy1)) |> IO.inspect(label: "Dummy 1 state")
187 | :sys.get_state(GenServer.whereis(:dummy2)) |> IO.inspect(label: "Dummy 2 state")
188 | :sys.get_state(GenServer.whereis(:dummy3)) |> IO.inspect(label: "Dummy 3 state")
189 |
190 | Kino.Process.render_sup_tree(supervisor_pid)
191 | ```
192 |
193 | This time we can see that when the `:dummy_2` process crashed the supervisor restarted all the child processes. So the all processes now have a different pid.
194 |
195 | ## :rest_for_one
196 |
197 | With the `:rest_for_one` strategy, if a child process terminates, not only the terminated child process but also the **subsequent child processes** that were started after it will be terminated and restarted.
198 |
199 | This strategy is useful when you want to restart only a portion of your supervision tree. In this case, when a process crashes, only the processes dependent on the crashed process will be restarted.
200 |
201 | #### Note:
202 |
203 | The order in which child processes are specified in a supervision tree is crucial. A supervisor will attempt to start the child processes in the exact order specified in the supervisor child specification. Similarly, when a process crashes, the supervisor will restart the child processes in the same order.
204 |
205 | When a supervisor shuts down, it terminates all children in the reverse order in which they are listed.
206 |
207 |
208 |
209 | 
210 |
211 |
212 |
213 | Let's see this strategy in action with our example. When the `:dummy2` process crashes, only the `:dummy2` and `:dummy3` processes will be restarted, while the `:dummy1` process will continue running.
214 |
215 | ```elixir
216 | Supervisor.stop(CrashDummySupervisor)
217 |
218 | {:ok, supervisor_pid} = CrashDummySupervisor.start_link(:rest_for_one)
219 |
220 | Supervisor.which_children(supervisor_pid) |> IO.inspect(label: "Supervisors Children")
221 |
222 | :sys.get_state(GenServer.whereis(:dummy1)) |> IO.inspect(label: "Dummy 1 state")
223 | :sys.get_state(GenServer.whereis(:dummy2)) |> IO.inspect(label: "Dummy 2 state")
224 | :sys.get_state(GenServer.whereis(:dummy3)) |> IO.inspect(label: "Dummy 3 state")
225 |
226 | Kino.Process.render_sup_tree(supervisor_pid)
227 | ```
228 |
229 | ```elixir
230 | # Makes the dummy2 child crash
231 | GenServer.cast(:dummy2, :boom)
232 | # Wait for the process to crash and be restarted
233 | Process.sleep(200)
234 |
235 | Supervisor.which_children(supervisor_pid) |> IO.inspect(label: "Supervisors Children")
236 |
237 | :sys.get_state(GenServer.whereis(:dummy1)) |> IO.inspect(label: "Dummy 1 state")
238 | :sys.get_state(GenServer.whereis(:dummy2)) |> IO.inspect(label: "Dummy 2 state")
239 | :sys.get_state(GenServer.whereis(:dummy3)) |> IO.inspect(label: "Dummy 3 state")
240 |
241 | Kino.Process.render_sup_tree(supervisor_pid)
242 | ```
243 |
244 | ### Resources
245 |
246 |
247 |
248 | * The images for the different restart stragies are taken from the [erlang documentation](https://www.erlang.org/doc/design_principles/sup_princ.html#restart-strategy)
249 |
250 | ## Navigation
251 |
252 |
266 |
--------------------------------------------------------------------------------
/chapters/ch_5.3_restart_strategies.livemd:
--------------------------------------------------------------------------------
1 | # Supervisor restart strategies
2 |
3 | ## Navigation
4 |
5 |
19 |
20 | ## Restart Strategies
21 |
22 | In the previous chapter, we learned about supervision strategies that determine whether a supervisor should restart its child processes when one of them crashes. However, it's important to consider when a process should be considered "crashed." Processes can gracefully terminate, in which case we might not want to restart them, or they can crash due to errors.
23 |
24 | To address this, restart strategies come into play. Unlike supervision strategies that apply to the entire supervision tree, restart strategies can be defined for each individual child process, allowing for more fine-grained control over their behavior.
25 |
26 |
27 |
28 | Restart values can be specified either in the child spec or when creating a GenServer.
29 |
30 | In the child spec, the restart value can be set as follows:
31 |
32 |
33 |
34 | ```elixir
35 | children = [
36 | %{
37 | id: :stack_1,
38 | start: {Stack, :start_link, []},
39 | restart: :temporary # <================= Here
40 | }
41 | ]
42 | ```
43 |
44 | #### Modifying default child spec
45 |
46 | Alternatively, when creating a GenServer, the restart value can be specified using the `use GenServer` macro:
47 |
48 |
49 |
50 | ```elixir
51 | use GenServer, restart: :transient
52 | ```
53 |
54 | As we learned earlier, GenServers provide a default `child_spec/1` function that automatically generates the child specification. By passing options directly to `use GenServer`, we can customize the `child_spec/1` function.
55 |
56 | To understand the behavior of different restart options, let's create a simple GenServer and add it to the supervision tree with various restart options.
57 |
58 | ```elixir
59 | defmodule CrashDummyServer do
60 | use GenServer
61 |
62 | def start_link(name) do
63 | random_state = System.unique_integer([:positive])
64 | GenServer.start_link(__MODULE__, {random_state, name}, name: name)
65 | end
66 |
67 | ## Callbacks
68 |
69 | @impl true
70 | def init({random_value, name}) do
71 | IO.inspect("#{name} starting up!")
72 | {:ok, random_value}
73 | end
74 |
75 | @impl true
76 | def handle_cast(:stop_gracefully, state) do
77 | # Returning this value makes the GenServer stop gracefully with :normal reason
78 | # If reason is neither :normal, :shutdown, nor {:shutdown, term} an error is logged.
79 | {:stop, :normal, state}
80 | end
81 |
82 | @impl true
83 | def handle_cast(:crash, state) do
84 | process_pid = self() |> inspect()
85 | raise "BOOM! CrashDummyServer process: #{process_pid} crashed!"
86 | {:noreply, state}
87 | end
88 | end
89 | ```
90 |
91 | ```elixir
92 | defmodule CrashDummySupervisor do
93 | use Supervisor
94 |
95 | def start_link() do
96 | Supervisor.start_link(__MODULE__, :noop, name: __MODULE__)
97 | end
98 |
99 | @impl true
100 | def init(_) do
101 | # Supervision tree, start multiple instances of our genserver with different restart options
102 | children = [
103 | child_spec(:permanent_dummy, :permanent),
104 | child_spec(:temporary_dummy, :temporary),
105 | child_spec(:transient_dummy, :transient)
106 | ]
107 |
108 | Supervisor.init(children, strategy: :one_for_one)
109 | end
110 |
111 | defp child_spec(name, restart_strategy) do
112 | Supervisor.child_spec(
113 | {CrashDummyServer, name},
114 | id: name,
115 | # Specifying the restart strategy
116 | restart: restart_strategy
117 | )
118 | end
119 | end
120 | ```
121 |
122 | In the code snippet above, we have created a simple GenServer that crashes when receiving the `:boom` message and gracefully stops when receiving the `:stop_gracefully` message.
123 |
124 | Within the Supervisor, we start three instances of this GenServer with three different restart strategies:
125 |
126 | * `:permanent`: This is the default restart strategy, where the child process is always restarted regardless of whether it crashes or is gracefully shut down.
127 | * `:temporary`: With this restart strategy, the child process is never restarted, even in the case of abnormal termination such as a crash. Any termination, even if it is abnormal, is considered successful.
128 | * `:transient`: The child process is restarted only if it terminates abnormally, meaning it exits with an exit reason other than `:normal`, `:shutdown`, or `{:shutdown, term}`.
129 |
130 | Now, let's test these restart strategies in action.
131 |
132 |
133 |
134 | ---
135 |
136 | ### `:permanent` restart strategy
137 |
138 | ```elixir
139 | {:ok, supervisor_pid} = CrashDummySupervisor.start_link()
140 | Supervisor.which_children(supervisor_pid)
141 | ```
142 |
143 | ```elixir
144 | # Test graceful termination of child with `:permanent` restart strategy
145 | # Notice how the GenServer is restarted
146 | GenServer.cast(:permanent_dummy, :stop_gracefully)
147 | ```
148 |
149 | ```elixir
150 | # Test abnormal termination of child with `:permanent` restart strategy
151 | # Notice how the GenServer is restarted
152 | GenServer.cast(:permanent_dummy, :crash)
153 | ```
154 |
155 | ---
156 |
157 | ### `:temporary` restart strategy
158 |
159 | ```elixir
160 | # Test graceful termination of child with `:temporary` restart strategy
161 | # Notice how the GenServer is NOT restarted
162 | GenServer.cast(:temporary_dummy, :stop_gracefully)
163 | ```
164 |
165 | ```elixir
166 | # Notice how temporary_dummy is no longer present in the list of children
167 | Supervisor.which_children(supervisor_pid) |> IO.inspect(label: "Supervisors Children")
168 | ```
169 |
170 | ```elixir
171 | # Restart the Supervisor so that all child processes are start again
172 | Supervisor.stop(supervisor_pid)
173 | {:ok, supervisor_pid} = CrashDummySupervisor.start_link()
174 | ```
175 |
176 | ```elixir
177 | # Test abnormal termination of child with `:temporary` restart strategy
178 | # Notice how the GenServer is NOT restarted
179 | GenServer.cast(:temporary_dummy, :crash)
180 | ```
181 |
182 | ```elixir
183 | # Notice how temporary_dummy is no longer present in the list of children
184 | Supervisor.which_children(supervisor_pid) |> IO.inspect(label: "Supervisors Children")
185 | ```
186 |
187 | ---
188 |
189 | ### `:transient` restart strategy
190 |
191 | ```elixir
192 | # Restart the Supervisor so that all child processes are start again
193 | Supervisor.stop(supervisor_pid)
194 | {:ok, supervisor_pid} = CrashDummySupervisor.start_link()
195 | ```
196 |
197 | ```elixir
198 | Supervisor.which_children(supervisor_pid) |> IO.inspect(label: "Supervisors Children")
199 |
200 | # Test graceful termination of child with `:transient` restart strategy
201 | # Notice how the GenServer is NOT restarted since it was stopped gracefully
202 | GenServer.cast(:transient_dummy, :stop_gracefully)
203 | ```
204 |
205 | ```elixir
206 | # Notice how the transient child has a pid "undefined` since its no longer running
207 | Supervisor.which_children(supervisor_pid)
208 | ```
209 |
210 | ```elixir
211 | # Restart the Supervisor so that all child processes are start again
212 | Supervisor.stop(supervisor_pid)
213 | {:ok, supervisor_pid} = CrashDummySupervisor.start_link()
214 | ```
215 |
216 | ```elixir
217 | # Test abnormal termination of child with `:transient` restart strategy
218 | # Notice how the GenServer is restarted since it was stopped abnormally
219 | GenServer.cast(:transient_dummy, :crash)
220 | ```
221 |
222 | ```elixir
223 | # Notice how transient_dummy was restarted and all children are running
224 | Supervisor.which_children(supervisor_pid) |> IO.inspect(label: "Supervisors Children")
225 | ```
226 |
227 | ## The Max Restarts option
228 |
229 | So far, we have covered various options in the supervisor's child specifications, such as `:id`, `:strategy`, `:name`, and `:restart`. Now, let's explore the remaining options that the child specification supports.
230 |
231 | Two important options are:
232 |
233 | * `:max_restarts`: This option sets the maximum number of restarts allowed within a specified time frame. By default, it is set to 3.
234 |
235 | * `:max_seconds`: This option defines the time frame in which the `:max_restarts` limit applies. The default value is 5 seconds.
236 |
237 | These options determine the maximum **restart intensity** of a supervisor, controlling the number of restarts that can occur within a given time interval. It is part of the supervisor's built-in mechanism to manage restarts effectively.
238 |
239 | **If the number of restarts exceeds the `:max_restarts` limit within the last `:max_seconds` seconds, the supervisor terminates all its child processes and itself**. In this case, the termination reason for the supervisor is `:shutdown`.
240 |
241 | When a supervisor terminates, the next higher-level supervisor takes action. It either restarts the terminated supervisor or terminates itself.
242 |
243 | The restart mechanism is designed to prevent a scenario where a process repeatedly crashes for the same reason, only to be restarted again and again.
244 |
245 | ## Shutdown strategy
246 |
247 | When defining child specifications for a supervisor, we have the option to include the `:shutdown` option, which determines how the supervisor shuts down its child processes.
248 |
249 | The `:shutdown` option has three possible values:
250 |
251 | * *An integer greater than or equal to 0*: This specifies the amount of time in milliseconds that the supervisor will wait for its children to terminate after sending a `Process.exit(child, :shutdown)` signal. If the child process does not trap exits, it will be terminated immediately upon receiving the `:shutdown` signal. If the child process traps exits, it has the specified amount of time to terminate. If it fails to terminate within the specified time, the supervisor will forcefully terminate the child process using `Process.exit(child, :kill)`.
252 |
253 | * `:brutal_kill`: This option causes the child process to be unconditionally and immediately terminated using `Process.exit(child, :kill)`. That is the supervisor will not wait for the child process to terminate gracefully but will immediately kill the process.
254 |
255 | * `:infinity`: With this option, the supervisor will wait indefinitely for the child process to terminate.
256 |
257 | By default, the `:shutdown` option is set to `5_000` (5 seconds), which means the supervisor will wait for a maximum of 5 seconds for the child process to shut down gracefully. If the child process does not terminate within this time frame, the supervisor will forcefully terminate it using `Process.exit(child, :kill)`.
258 |
259 | These options provide flexibility in managing the shutdown behavior of child processes in a supervisor.
260 |
261 | ## Key points
262 |
263 | * During startup, a supervisor processes all child specifications and starts each child in **the order they are defined**. This is achieved by invoking the function specified under the `:start` key in the child specification, typically `start_link/1`.
264 |
265 | * When a supervisor initiates shutdown, it terminates its children in the **reverse order of their listing**. This termination process involves sending a shutdown exit signal, `Process.exit(child_pid, :shutdown)`, to each child process and waiting for a specified time interval for them to terminate. The default interval is 5000 milliseconds.
266 |
267 | * If a child process is not trapping exits, it will immediately shut down upon receiving the first exit signal. On the other hand, if a child process is trapping exits, it will invoke the terminate callback and must terminate within a reasonable time before the supervisor forcefully terminates it.
268 |
269 | * When an Elixir application exits, the termination propagates down the supervision tree. *Supervisors always trap exits* for various reasons, so they attempt to stop all their children upon receiving an exit signal. This is achieved by sending an exit signal to each child individually, allowing a timeout period before resorting to a brutal termination with `:kill`. The duration of this timeout is determined by the `shutdown` option specified in the child specification.
270 |
271 | * Once all children are stopped, the supervisor itself stops as well, resulting in the orderly shutdown of the supervision tree.
272 |
273 | These points highlight the startup and shutdown behavior of supervisors, the termination process for child processes, and the flow of exit signals within the supervision tree.
274 |
275 | ## Navigation
276 |
277 |
291 |
--------------------------------------------------------------------------------
/chapters/ch_5.4_introduction_to_dynamic_supervisor.livemd:
--------------------------------------------------------------------------------
1 | # Introduction to Dynamic Supervisor
2 |
3 | ```elixir
4 | Mix.install([
5 | {:kino, "~> 0.9.0"}
6 | ])
7 | ```
8 |
9 | ## Navigation
10 |
11 |
25 |
26 | ## The Dynamic Supervisor
27 |
28 | In the previous chapter, we learned about the Supervisor behavior, which enables us to supervise processes and restart them in case of failures, ensuring fault tolerance. However, the Supervisor behavior requires us to specify all the child processes it will supervise in advance as child specifications. In other words, **the Supervisor module was primarily designed to handle static children.**
29 |
30 | When the supervisor starts, it creates and starts the child processes in the specified order, and when the supervisor is stopped, it terminates the processes in the reverse order.
31 |
32 | On the other hand, a DynamicSupervisor **starts with no children** initially. Instead, children are started **on demand** using the `start_child/2` function, and there is **no specific ordering** between the children. This provides a lot of flexibility as we can dynamically add and remove child processes to be supervised. The DynamicSupervisor can efficiently handle a large number of children by utilizing optimized data structures and perform certain operations, such as shutting down, concurrently.
33 |
34 |
35 |
36 | ### Key points
37 |
38 | * DynamicSupervisor is a specialized type of Supervisor designed to handle dynamic children. Note that in Erlang we have only one supervisor. These behaviors like DynamicSupervisor and PartitionSupervisor are abstraction built on top of the basic Supervisor to address common use cases more conveniently.
39 |
40 | * Dynamic supervisors start without any children initially, and there is no predefined ordering between the children. Children can be added to the supervisor dynamically as needed, without any specific sequence or arrangement.
41 |
42 | * The only available supervision strategy for DynamicSupervisor is `:one_for_one`.
43 |
44 | * The `id` of a child in a DynamicSupervisor is always `:undefined`. This is because dynamically supervised children are created from the same child specification, and assigning a specific id to each child would result in conflicts.
45 |
46 |
47 |
48 | ### Supervisor.start_child/2 vs DynamicSupervisor.start_child/2
49 |
50 | It may appear confusing that both the Supervisor and DynamicSupervisor modules provide a function called `start_child/2` to dynamically start supervised child processes. This raises the question of what distinguishes the two and why we have a dedicated DynamicSupervisor for dynamic child management.
51 |
52 | While it is possible to dynamically start and stop children from a standard Supervisor, the DynamicSupervisor is specifically designed to excel in this use case. There are differences in how a DynamicSupervisor handles its children compared to a regular supervisor. For instance, a DynamicSupervisor does not impose an inherent ordering among its children.
53 |
54 | On restart a DynamicSupervisor starts empty while a regular Supervisor typically starts along with all the child process defined in its child specifications. A DynamicSupervisor can concurrently shuts down all children when restarted unlike a standard supervisor which follows a specific restart order.
55 |
56 | Furthermore, the DynamicSupervisor module provides additional options, such as `:max_children`, which allows setting a limit on the maximum number of dynamically supervised children.
57 |
58 | Therefore it just more idiomatic and optimal to use a DynamicSupervisor instead of the regular Supervisor module when trying to dynamically start/stop supervised processes.
59 |
60 | ## Usage
61 |
62 | Just like the regular supervisor module the DynamicSupervisor can either be started directly or defined as a module.
63 |
64 | Lets look at some examples...
65 |
66 | ```elixir
67 | children = [{DynamicSupervisor, name: MyTestDynamicSupervisor}]
68 |
69 | # Th only possible strategy with DynamicSupervisor is :one_for_one
70 | {:ok, supervisor_pid} = Supervisor.start_link(children, strategy: :one_for_one)
71 | ```
72 |
73 | We will now create a simple GenServer that we can start under this supervisor
74 |
75 | ```elixir
76 | defmodule TestServer do
77 | use GenServer
78 |
79 | def start_link(name) do
80 | GenServer.start_link(__MODULE__, :noop, name: name)
81 | end
82 |
83 | ## Callbacks
84 |
85 | @impl true
86 | def init(_arg) do
87 | {:ok, :noop}
88 | end
89 |
90 | @impl true
91 | def handle_call({:echo, arg}, _from, state) do
92 | {:reply, arg, state}
93 | end
94 | end
95 | ```
96 |
97 | Now we can use the `DynamicSupervisor.start_child(supervisor, child_spec)` function to dynamically start a child process under the supervisor. Notice how we need to pass a child spec to the function.
98 |
99 | ```elixir
100 | {:ok, echo_1} = DynamicSupervisor.start_child(MyTestDynamicSupervisor, {TestServer, :echo1})
101 | {:ok, echo_2} = DynamicSupervisor.start_child(MyTestDynamicSupervisor, {TestServer, :echo2})
102 | ```
103 |
104 | ```elixir
105 | # Lets visualize the supervision tree
106 | Kino.Process.render_sup_tree(supervisor_pid)
107 | ```
108 |
109 | Notice how we dynamically started 2 instances of our `TestServer` GenServer process under the `MyTestDynamicSupervisor` DynamicSupervisor.
110 |
111 | ```elixir
112 | DynamicSupervisor.count_children(MyTestDynamicSupervisor)
113 | ```
114 |
115 | ```elixir
116 | # Notice how the id is undefined for a DynamicSupervisor
117 | DynamicSupervisor.which_children(MyTestDynamicSupervisor)
118 | ```
119 |
120 | We can easily terminate a dynamically started child
121 |
122 | ```elixir
123 | DynamicSupervisor.terminate_child(MyTestDynamicSupervisor, echo_2)
124 | ```
125 |
126 | ```elixir
127 | DynamicSupervisor.count_children(MyTestDynamicSupervisor)
128 | ```
129 |
130 | ```elixir
131 | DynamicSupervisor.which_children(MyTestDynamicSupervisor)
132 | ```
133 |
134 | ---
135 |
136 | ### Module based DynamicSupervisor
137 |
138 | Now lets use a module based DynamicSupervisor. Just like the regular Supervisor behaviour the DynamicSupervisor behaviour only has one callback that we must implement that is the `init/1` callback.
139 |
140 | Also similar to the regular Supervisor module when starting a DynamicSupervisor we can pass options like `:name`, `:strategy`, `:max_restarts` and `:max_seconds`.
141 |
142 | Two new options that are available with DynamicSupervisors are
143 |
144 | * `:max_children` - the maximum amount of children to be running under this supervisor at the same time. When `:max_children` is exceeded, `start_child/2` returns `{:error, :max_children}`. Defaults to `:infinity`.
145 |
146 | * `:extra_arguments` - arguments that are prepended to the arguments specified in the child spec given to `start_child/2`. Defaults to an empty list.
147 |
148 | To understand this better lets look at an example:
149 |
150 | ```elixir
151 | # A simple GenServer module which we would start under our supervisor
152 | defmodule TestServerV2 do
153 | use GenServer
154 |
155 | def start_link(extra_arg, name, arg) do
156 | GenServer.start_link(__MODULE__, [extra_arg, arg], name: name)
157 | end
158 |
159 | ## Callbacks
160 |
161 | @impl true
162 | def init([extra_arg, arg]) do
163 | IO.inspect(
164 | "New TestServerV2 started with extra_arg = #{inspect(extra_arg)} and arg = #{inspect(arg)}"
165 | )
166 |
167 | {:ok, :noop}
168 | end
169 |
170 | @impl true
171 | def handle_call({:echo, arg}, _from, state) do
172 | {:reply, arg, state}
173 | end
174 | end
175 | ```
176 |
177 | ```elixir
178 | defmodule MyTestDynamicSupervisorV2 do
179 | # The DynamicSupervisor behaviour that defines a default child_spec/1
180 | use DynamicSupervisor
181 |
182 | def start_link(init_arg) do
183 | DynamicSupervisor.start_link(__MODULE__, init_arg, name: __MODULE__)
184 | end
185 |
186 | # A public api to easily start child process under this supervisor
187 | def start_child(name, arg) do
188 | child_spec = %{id: TestServerV2, start: {TestServerV2, :start_link, [name, arg]}}
189 |
190 | # This will start an child process of the TestServerV2 by calling
191 | # TestServerV2.start_link(init_arg, name, arg)
192 | DynamicSupervisor.start_child(__MODULE__, child_spec)
193 | end
194 |
195 | @impl true
196 | def init(init_arg) do
197 | # Returns a tuple containing the supervisor initialization options.
198 | DynamicSupervisor.init(
199 | strategy: :one_for_one,
200 | max_children: 2,
201 | extra_arguments: [init_arg]
202 | )
203 | |> IO.inspect(label: "DynamicSupervisor initialized with")
204 | end
205 | end
206 | ```
207 |
208 | Few things to note in the above code snippets:
209 |
210 | * We are using the `DynamicSupervisor.init/1` helper function to generate a tuple that initializes the dynamic supervisor with proper options in its `init/1` callback.
211 |
212 | * We have added a helper function `MyTestDynamicSupervisorV2.start_child/2` to dynamically start supervised child processes under our dynamic supervisor.
213 |
214 | * We have passed additional options like the `max_children` to limit the number of children the dynamic supervisor can start.
215 |
216 | * The `extra_arguments: [init_arg]` option will automatically prepend the `init_arg` argument to every child process started under this supervisor. This is especially useful if we want to always send a specific argument to every child process that is started under this supervisor.
217 |
218 | [Note: Similar to the regular supervisor module the DynamicSupervisor module also defines a default `child_spec/1` function so we can use shorthand syntax when defining child specs to pass to `DynamicSupervisor.start_child/2`]
219 |
220 | ```elixir
221 | {:ok, supervisor_pid} = MyTestDynamicSupervisorV2.start_link("Elixir is ❤")
222 | ```
223 |
224 | Now let us dynamically start and stop child process under our supervisor.
225 |
226 | ```elixir
227 | {:ok, echo_1} = MyTestDynamicSupervisorV2.start_child(:echov2_1, :yolo)
228 | {:ok, echo_2} = MyTestDynamicSupervisorV2.start_child(:echov2_2, :awesome_elixir)
229 | ```
230 |
231 | Notice how the child processes that were started have received the "Elixir is ❤" specified as `:extra_arguments` along with the arguments that were passed.
232 |
233 | ```elixir
234 | DynamicSupervisor.count_children(MyTestDynamicSupervisor) |> IO.inspect()
235 | DynamicSupervisor.which_children(MyTestDynamicSupervisor)
236 | ```
237 |
238 | ```elixir
239 | DynamicSupervisor.start_child(MyTestDynamicSupervisorV2, {TestServerV2, :echo3})
240 | ```
241 |
242 | ```elixir
243 | # Lets visualize the supervision tree
244 | Kino.Process.render_sup_tree(supervisor_pid)
245 | ```
246 |
247 | ```elixir
248 | DynamicSupervisor.terminate_child(MyTestDynamicSupervisorV2, echo_1)
249 | DynamicSupervisor.count_children(MyTestDynamicSupervisor) |> IO.inspect()
250 | DynamicSupervisor.which_children(MyTestDynamicSupervisor)
251 | ```
252 |
253 | In future chapters, we will delve into the topic of scaling a DynamicSupervisor by utilizing a PartitionSupervisor. We will also go through more examples of how to use dynamic supervisors in real use cases.
254 |
255 | ## Resources
256 |
257 | * https://hexdocs.pm/elixir/DynamicSupervisor.html
258 | * https://elixirforum.com/t/different-between-supervisor-start-child-and-dynamicsupervisor-start-child/14585/3
259 |
260 | ## Navigation
261 |
262 |
276 |
--------------------------------------------------------------------------------
/chapters/ch_5.5_partition_supervisor.ex.livemd:
--------------------------------------------------------------------------------
1 | # The Partition Supervisor
2 |
3 | ```elixir
4 | Mix.install([
5 | {:kino, "~> 0.9.0"}
6 | ])
7 | ```
8 |
9 | ## Navigation
10 |
11 |
25 |
26 | ## Introduction
27 |
28 | A PartitionSupervisor functions similarly to a regular supervisor, but with the added capability of creating partitions.
29 |
30 | When a PartitionSupervisor is started, it *will create multiple partitions and will start a process under each of the partitions*.
31 |
32 | This feature becomes particularly valuable when certain processes within a system have the potential to become bottlenecks. If these processes can easily partition their state without any interdependencies, the PartitionSupervisor can be used.
33 |
34 | By starting multiple instances of such processes across different partitions, the workload can be distributed and potential bottlenecks can be avoided.
35 |
36 | ## Usage
37 |
38 | Once a PartitionSupervisor is started, we can dispatch messages to its children using the `{:via, PartitionSupervisor, {name, key}}`. Here, `name` refers to the name of the PartitionSupervisor, and `key` is used for routing the message.
39 |
40 | The PartitionSupervisor uses a routing strategy to determine the appropriate partition to which a message should be dispatched. When sending a message to a child process under a PartitionSupervisor, we provide a `key`. Depending on the routing strategy in place, the PartitionSupervisor will utilize this key to select the specific partition to which the message should be sent.
41 |
42 | Let's explore an example to gain a better understanding of this concept.
43 |
44 |
45 |
46 | Lets create a simple GenServer which we can start under the partition supervisor
47 |
48 | ```elixir
49 | defmodule EchoServer do
50 | use GenServer
51 |
52 | def start_link(args) do
53 | GenServer.start_link(__MODULE__, args)
54 | end
55 |
56 | @impl true
57 | def init(args) do
58 | IO.inspect("EchoServer #{inspect(self())} started with args: #{inspect(args)}")
59 | {:ok, :noop}
60 | end
61 |
62 | @impl true
63 | def handle_call({:echo, msg}, _from, state) do
64 | IO.inspect("EchoServer(#{inspect(self())}) echoing: #{inspect(msg)}")
65 | {:reply, msg, state}
66 | end
67 | end
68 | ```
69 |
70 | Nows lets start a partition supervisor
71 |
72 | ```elixir
73 | {:ok, supervisor_pid} =
74 | PartitionSupervisor.start_link(
75 | name: EchoServerPartitionSupervisor,
76 | # Use the default child_spec/1 function of the GenServer
77 | child_spec: EchoServer.child_spec(:test_arg)
78 | )
79 | ```
80 |
81 | Now lets visualize the supervision tree.
82 |
83 | ```elixir
84 | Kino.Process.render_sup_tree(supervisor_pid)
85 | ```
86 |
87 | From the above output we can now see that multiple processes of the `EchoServer` GenServer were started by the Partition Supervisor. A separate instance of the `EchoServer` was started for each partition that was created.
88 |
89 | By default the number of partitions a PartitionSupervisor will create is equal to `System.schedulers_online()`(typically the number of CPU cores).
90 |
91 | ```elixir
92 | System.schedulers_online()
93 | ```
94 |
95 | The number of processes(partitions) we see in the supervision tree must match the above output returned from `System.schedulers_online()`.
96 |
97 |
98 |
99 | The PartitionSupervisor provides additional options that can be passed during its initialization:
100 |
101 | * `:partitions` - This option accepts a positive integer value that represents the number of partitions to create. By default, it is set to `System.schedulers_online()`, which corresponds to the number of online schedulers in the system.
102 |
103 | * `:with_arguments` - A two-argument anonymous function that allows the partition to be given to the child starting function.
104 |
105 | In addition to these specific options, other common options such as `:name`, `:child_spec`, `:max_restarts`, and `:max_seconds` can be used with the PartitionSupervisor, and they function as they do in regular supervisors.
106 |
107 | Now lets restart our PartitionSupervisor with some of these options to customize its behaviour...
108 |
109 | ```elixir
110 | # Defining a new echo server GenServer with a start_link/2 function
111 | # to also receive the partition number as an argument.
112 | defmodule EchoServerV2 do
113 | use GenServer
114 |
115 | def start_link(args, partition_number) do
116 | GenServer.start_link(__MODULE__, [args, partition_number])
117 | end
118 |
119 | @impl true
120 | def init([args, partition_number]) do
121 | IO.inspect(
122 | "EchoServer #{inspect(self())} started on partition #{partition_number} with args: #{inspect(args)}"
123 | )
124 |
125 | # We save the partition number in the GenServer state
126 | {:ok, partition_number}
127 | end
128 |
129 | @impl true
130 | def handle_call({:echo, msg}, _from, partition_number) do
131 | IO.inspect(
132 | "EchoServer(#{inspect(self())})(partition=#{partition_number}) echoing: #{inspect(msg)}"
133 | )
134 |
135 | {:reply, msg, partition_number}
136 | end
137 | end
138 | ```
139 |
140 | ```elixir
141 | # Stop the existing supervisor
142 | :ok = PartitionSupervisor.stop(EchoServerPartitionSupervisor)
143 |
144 | # Start the EchoServerPartitionSupervisor again with added options
145 | {:ok, supervisor_pid} =
146 | PartitionSupervisor.start_link(
147 | name: EchoServerPartitionSupervisor,
148 | child_spec: EchoServerV2.child_spec(:test_arg),
149 |
150 | # We explicitly specify the number of partitions to create
151 | partitions: 3,
152 | with_arguments: fn [existing_args], partition ->
153 | # Inject the partition number into the args given to the child process
154 | # This will be passed to the child process when it is started via the
155 | # `start_link(args, partition_number)` function.
156 | [existing_args, partition]
157 | end
158 | )
159 | ```
160 |
161 | ```elixir
162 | Kino.Process.render_sup_tree(supervisor_pid)
163 | ```
164 |
165 | Notice that this time only 3 partitions were created and 3 child processes were started.
166 | Also notice how the partition number was passed as an argument to every child process, this is due to the use of the `with_arguments` option.
167 |
168 | The `with_arguments` option allows us to customize the arguments passed to child processes in a partitioned supervision setup. By providing a two-argument anonymous function, we can include the partition number in the arguments used to start each child process. This **allows each process to have knowledge of the partition_number on which it is running**.
169 |
170 |
171 |
172 | ### Sending messages
173 |
174 | To send a message to a child process under a PartitionSupervisor, we can use the `{:via, PartitionSupervisor, {name, key}}` tuple. Here key is used for routing the message to the appropriate partition.
175 |
176 | By using this message dispatching method, we can effectively send messages to specific child processes running under the PartitionSupervisor based on the key that we pass.
177 |
178 | ```elixir
179 | # Send a message to the EchoServer running on partition 0
180 | :hi =
181 | GenServer.call(
182 | {:via, PartitionSupervisor, {EchoServerPartitionSupervisor, 0}},
183 | {:echo, :hi}
184 | )
185 |
186 | # Send a message to the EchoServer running on partition 1
187 | :ola =
188 | GenServer.call(
189 | {:via, PartitionSupervisor, {EchoServerPartitionSupervisor, 1}},
190 | {:echo, :ola}
191 | )
192 |
193 | # Send a message to the EchoServer running on partition 2
194 | :adios =
195 | GenServer.call(
196 | {:via, PartitionSupervisor, {EchoServerPartitionSupervisor, 2}},
197 | {:echo, :adios}
198 | )
199 |
200 | # Send a message to the EchoServer running on partition 1
201 | # (the routing key 1000 results in partition 1 to be selected)
202 | GenServer.call(
203 | {:via, PartitionSupervisor, {EchoServerPartitionSupervisor, 1000}},
204 | {:echo, :boom}
205 | )
206 | ```
207 |
208 | When using integer keys with the PartitionSupervisor, the routing strategy is determined by the formula `rem(abs(key), partitions)`. In the example we provided, the message with the key `1000` was sent to partition 1 because `rem(abs(1000), 3) = rem(1000, 3) = 1`.
209 |
210 | However, if the routing key is not an integer, the `:erlang.phash2(key, partitions)` hash function is used as the routing strategy. This function calculates a hash value based on the key and the number of partitions, resulting in the selection of the appropriate partition to which the message should be dispatched.
211 |
212 | ```elixir
213 | :erlang.phash2("1000", 3) |> IO.inspect(label: "Partition")
214 |
215 | GenServer.call(
216 | {:via, PartitionSupervisor, {EchoServerPartitionSupervisor, "1000"}},
217 | {:echo, :hello_world}
218 | )
219 | ```
220 |
221 | If we want to retrieve the PID of the process running on a partition for a certain key, we can use `GenServer.whereis({:via, PartitionSupervisor, {name, key}})`
222 |
223 | ```elixir
224 | # Get the PID of the process running in the partition that would be
225 | # selected when using "1000" as the key
226 | GenServer.whereis({:via, PartitionSupervisor, {EchoServerPartitionSupervisor, "1000"}})
227 | ```
228 |
229 | ### Implementation detail
230 |
231 | The PartitionSupervisor uses either an ETS table or a `Registry` to manage all of the partitions. Under the hood, the PartitionSupervisor generates a child spec for each partition and then acts as a regular supervisor. The ID of each child spec is the partition number.
232 |
233 | ## Navigation
234 |
235 |
249 |
--------------------------------------------------------------------------------
/chapters/ch_5.6_scaling_dynamic_supervisor.livemd:
--------------------------------------------------------------------------------
1 | # Scaling Dynamic Supervisors
2 |
3 | ```elixir
4 | Mix.install([
5 | {:kino, "~> 0.9.0"}
6 | ])
7 | ```
8 |
9 | ## Navigation
10 |
11 |
25 |
26 | ## The scalability problem with DynamicSupervisors
27 |
28 | In previous chapters, we learned about the DynamicSupervisor, which is effective for dynamically spawning and supervising child processes. However, in certain scenarios, the DynamicSupervisor can become a bottleneck.
29 |
30 | The DynamicSupervisor operates as a single process responsible for starting other processes. In high-demand situations where there are numerous requests to start new child processes, the DynamicSupervisor may struggle to keep up. Additionally, if a child process experiences delays during initialization (e.g., being stuck in the `init/1` callback), it can block the DynamicSupervisor and prevent it from starting new child processes.
31 |
32 | Lets simulate such a situation with an example...
33 |
34 | ```elixir
35 | # A minimal GenServer which takes 2 seconds to initialize
36 | defmodule SlowGenServer do
37 | use GenServer
38 |
39 | def start_link(args) do
40 | GenServer.start_link(__MODULE__, args)
41 | end
42 |
43 | @impl true
44 | def init(_args) do
45 | # Simulate slow start of a GenServer by sleeping for 1 second
46 | :timer.sleep(1000)
47 | IO.inspect("Started new SlowGenServer #{inspect(self())}")
48 | {:ok, :noop}
49 | end
50 | end
51 | ```
52 |
53 | Note: In real scenarios, it's important to avoid performing time-consuming tasks in the `init/1` callback of a GenServer. Instead, we should leverage the `handle_continue/2` callback to handle long-running tasks and prevent them from blocking the GenServer startup process. However, for the purpose of this example, let's proceed with trying it out.
54 |
55 | ```elixir
56 | # Start a DynamicSupervisor named "MySlowDynamicSupervisor"
57 | {:ok, supervisor_pid} =
58 | DynamicSupervisor.start_link(
59 | name: MySlowDynamicSupervisor,
60 | # Use the default child_spec/1 function of the GenServer
61 | child_spec: DynamicSupervisor.child_spec([])
62 | )
63 | ```
64 |
65 | Now lets try to simultaneously add 5 child processes under our DynamicSupervisor.
66 |
67 | ```elixir
68 | # Lets start 5 instances of the SlowGenServer process under the DynamicSupervisor
69 | for _i <- 1..5 do
70 | # Start a new process that in turn starts a new child process under the dynamic supervisor
71 | spawn(fn -> DynamicSupervisor.start_child(MySlowDynamicSupervisor, SlowGenServer) end)
72 | end
73 | ```
74 |
75 | Notice how the DynamicSupervisor starts each child process one by one and is blocked until the previous child process is started. In real-world scenarios, this can cause significant delays in starting child processes under a single DynamicSupervisor, resulting in potential bottlenecks and performance issues.
76 |
77 |
78 |
79 | Lets visualize the resulting supervision tree
80 |
81 | ```elixir
82 | Kino.Process.render_sup_tree(supervisor_pid)
83 | ```
84 |
85 | Notice how all the 5 instances of the `SlowGenServer` process are spawned under the same `MySlowDynamicSupervisor` instance.
86 |
87 | ## Using a PartitionSupervisor to scale DynamicSupervisors
88 |
89 | To address the aforementioned problem, we can use a PartitionSupervisor to start multiple instances of the DynamicSupervisor. The **PartitionSupervisor acts as a supervisor for multiple DynamicSupervisor processes, each running in a separate partition**.
90 |
91 | When a new child process needs to be started, the PartitionSupervisor selects one of the DynamicSupervisor processes to handle the request. This distribution of child process creation across multiple DynamicSupervisors helps distribute the workload and prevents bottlenecks that can occur when relying on a single DynamicSupervisor.
92 |
93 | Lets see this in action...
94 |
95 | ```elixir
96 | # Stop the existing dynamic supervisor
97 | Supervisor.stop(MySlowDynamicSupervisor)
98 |
99 | # Start a partition supervisor with a dynamic supervisor as the child process for each partition
100 | {:ok, supervisor_pid} =
101 | PartitionSupervisor.start_link(
102 | name: MySlowPartitionSupervisor,
103 | # Create 6 partitions
104 | partitions: 6,
105 | # Use the default child_spec/1 function of DynamicSupervisor
106 | child_spec: DynamicSupervisor.child_spec([])
107 | )
108 | ```
109 |
110 | In the code above, we start a partition supervisor that will by create six partitions and will start a dynamic supervisor for each partition.
111 |
112 | ```elixir
113 | Kino.Process.render_sup_tree(supervisor_pid)
114 | ```
115 |
116 | Now, instead of directly calling the DynamicSupervisor by its name, we access it through the PartitionSupervisor using the `{:via, PartitionSupervisor, {partition_supervisor_name, key}}` format.
117 |
118 | Now lets try again to start 6 child processes under the supervisor
119 |
120 | ```elixir
121 | for i <- 1..5 do
122 | # Start a new process that in turn starts a new child process under one of the
123 | # dynamic supervisors via the partition supervisor
124 | spawn(fn ->
125 | DynamicSupervisor.start_child(
126 | {:via, PartitionSupervisor, {MySlowPartitionSupervisor, i}},
127 | SlowGenServer
128 | )
129 | end)
130 | end
131 | ```
132 |
133 | In the provided code, we spawn five new processes, and each process starts a new child process under one of the dynamic supervisors via the partition supervisor.
134 |
135 | We use the numbers 1 to 5 as the routing keys for each child process. With six partitions available, each child process will be started under a separate dynamic supervisor.
136 |
137 |
138 |
139 | Lets visualize the resulting supervision tree
140 |
141 | ```elixir
142 | Kino.Process.render_sup_tree(supervisor_pid)
143 | ```
144 |
145 | In the provided supervision tree, we can observe that five instances of the DynamicSupervisor were started under the `MySlowPartitionSupervisor` PartitionSupervisor. Each of these dynamic supervisors represents a separate partition.
146 |
147 | Furthermore, under each dynamic supervisor, a separate instance of the `SlowGenServer` process was started.
148 |
149 |
150 |
151 | ---
152 |
153 | By leveraging the PartitionSupervisor as the entry point, we can abstract away the details of the individual dynamic supervisors and rely on the routing strategy to handle the selection of the appropriate dynamic supervisor for starting the child processes. This approach allows for efficient distribution of child processes across multiple dynamic supervisors, reducing the load on any single supervisor and avoiding potential bottlenecks.
154 |
155 | As a result, the child processes are started much faster compared to the previous example, where we relied on a single dynamic supervisor.
156 |
157 |
158 |
159 | Note: In most real-world scenarios, the supervisor and partition supervisor are typically started as part of the application's supervision tree. Instead of manually calling start_link/1, we can define the supervisors and their child specifications in the application module.
160 |
161 | Here's an example of how we can start the partition supervisor under a supervision tree:
162 |
163 |
164 |
165 | ```elixir
166 | defmodule MyApp.Application do
167 | use Application
168 |
169 | def start(_type, _args) do
170 | children = [
171 | {PartitionSupervisor,
172 | child_spec: DynamicSupervisor,
173 | name: MySlowPartitionSupervisor}
174 | ]
175 |
176 | opts = [strategy: :one_for_one]
177 | Supervisor.start_link(children, opts)
178 | end
179 | end
180 | ```
181 |
182 |
183 |
184 | ### Resources:
185 |
186 | * https://hexdocs.pm/elixir/1.15.0-rc.0/PartitionSupervisor.html#content
187 | * https://blog.appsignal.com/2022/09/20/fix-process-bottlenecks-with-elixir-1-14s-partition-supervisor.html
188 |
189 | ## Navigation
190 |
191 |
205 |
--------------------------------------------------------------------------------
/chapters/ch_7.1_intro_to_tasks.livemd:
--------------------------------------------------------------------------------
1 | # Introduction to Tasks
2 |
3 | ## Navigation
4 |
5 |
19 |
20 | ## Introduction
21 |
22 | In the previous chapters, we explored various methods of starting processes, including `spawn/1` and `spawn_link/1`. Now, let's dive into the Task module, which provides a more convenient approach to spawning processes for performing tasks.
23 |
24 | The Task module offers a wide range of convenience functions to effectively manage launched tasks. Unlike plain processes started with `spawn/1`, tasks provide additional capabilities such as monitoring metadata and error logging.
25 |
26 | With the Task module, we gain access to a many functions tailored to common use cases. We can easily await the completion of spawned tasks, launch supervised tasks, and execute multiple tasks concurrently. The abstraction and convenience functions provided by the Task module make working with processes a breeze, eliminating the need to delve into low-level details.
27 |
28 | ## Basics usage
29 |
30 | Let's explore some basic examples of launching tasks:
31 |
32 | ```elixir
33 | Task.start(fn -> IO.puts("Hello from the first task!") end)
34 |
35 | # Same as Task.start/1, but accepts a module, function, and arguments instead.
36 | Task.start(IO, :puts, ["Hello from the second task!"])
37 | ```
38 |
39 | As you can see, launching a task is very similar to spawning a process using `spawn/1`. In this case, the process spawned by `Task.start/1` is not linked to the caller process. It is primarily used for performing side effects where we don't need to wait for the result or handle failures.
40 |
41 | ## The Task struct
42 |
43 | Under the hood, a task in Elixir is essentially a regular Elixir process. When we spawn a task using one of the functions provided by the Task module, we receive a `Task` struct in return. This struct contains additional information about the task, and it can be utilized with various functions from both the Task and Task.Supervisor modules (which we will explore in greater detail in the upcoming chapters).
44 |
45 | Now, let's take a closer look at the information encapsulated within the Task struct. To do this, we will start a task using the `Task.async/1` function and analyze the resulting struct.
46 |
47 | ```elixir
48 | Task.async(fn -> :empty_task end)
49 | ```
50 |
51 | We get back a structure like so
52 |
53 |
54 |
55 | ```elixir
56 | %Task{
57 | mfa: {module, function, arrity},
58 | owner: owner_pid,
59 | pid: task_process_pid,
60 | ref: #Reference
61 | }
62 | ```
63 |
64 | * `:mfa` - a three-element tuple containing the module, function name, and arity invoked to start the task in async/1 and async/3
65 | * `:owner` - the PID of the process that started the task
66 | * `:pid` - the PID of the task process; nil if there is no process specifically assigned for the task
67 | * `:ref` - an opaque term used as the task monitor reference
68 |
69 | In the case of the `ref` field in the Task struct, it represents a monitor reference. When a task is spawned, the caller process monitors the task process using this reference. This monitoring enables the caller process to receive a `{:DOWN, , :process, , }` message when the task exits. The monitor reference is particularly useful when awaiting tasks to [receive exit messages in case of crashes](https://github.com/elixir-lang/elixir/blob/7f7a8bca99fa306a41a985df0018ba642e577d4d/lib/elixir/lib/task.ex#L841).
70 |
71 |
72 |
73 | In the upcoming chapters, we will dive deeper into the capabilities of tasks. We will explore how to await task completion, supervise tasks, and uncover many more exciting features. Stay tuned!
74 |
75 | ## Navigation
76 |
77 |
91 |
--------------------------------------------------------------------------------
/chapters/ch_7.2_awaiting_tasks.livemd:
--------------------------------------------------------------------------------
1 | # Awaiting Tasks
2 |
3 | ```elixir
4 | Mix.install([])
5 | ```
6 |
7 | ## Navigation
8 |
9 |
23 |
24 | ## Introduction
25 |
26 | Till now we have seen examples were we spawn a process to do something concurrently however sometimes we might need the value returned by the process. In such situations it is important to wait for the process to complete and then fetch the result.
27 |
28 | The Task module provides the `async` and `await` functions to handle this common use case. With `Task.async/1`, a new process is created, **linked, and monitored** by the caller. Once the task action finishes, a message containing the result is sent to the caller. The `Task.await/2` function is then used to read this message and obtain the result.
29 |
30 | Here are some key points to note about `async` and `await`:
31 |
32 | * Async tasks establish a link between the caller and the spawned process. If either the caller or the task crashes, the other process will crash as well. This intentional linkage ensures that the computation is not carried out if the process meant to receive the result no longer exists.
33 |
34 | * When using async tasks, it is important to await a reply as they are always sent. If you don't expect a reply but still want to launch a linked process, consider using `Task.start_link/1` instead.
35 |
36 | Lets look at some code to understand this better...
37 |
38 | ```elixir
39 | my_task =
40 | Task.async(fn ->
41 | # Sleep for 2 seconds
42 | :timer.sleep(2000)
43 | IO.puts("Done sleeping")
44 | DateTime.utc_now()
45 | end)
46 | |> IO.inspect()
47 |
48 | # Notice how the above task process is linked to the caller process
49 | Process.info(self(), :links)
50 | |> IO.inspect(label: "Parent process #{inspect(self())} links")
51 |
52 | Task.await(my_task) |> IO.inspect(label: "Task returned")
53 | ```
54 |
55 | Lets do this again, but check the process mailbox to see the message returned by the spawned task process.
56 |
57 | ```elixir
58 | my_task = Task.async(fn -> "return value" end)
59 | IO.inspect(my_task)
60 | # Wait for process to complete
61 | :timer.sleep(100)
62 | :erlang.process_info(self(), :messages) |> IO.inspect(label: "Messages in mailbox")
63 | Task.await(my_task)
64 | ```
65 |
66 | Here notice how the spawed task process sends a message back to the caller in the format
67 | `{, }`. The `Task.await/1` call basically [awaits this message](https://github.com/elixir-lang/elixir/blob/9fd85b06dcb74217108cd0bdf4164b6cd7f9e667/lib/elixir/lib/task.ex#L827) in a recieve block like so...
68 |
69 |
70 |
71 | ```elixir
72 | receive do
73 | # The reply message from the task
74 | {^ref, reply} ->
75 | # Stop monitoring the task since th task has sent a reply so must have completed successfully so we no longer monitor the process for crashes
76 | demonitor(ref)
77 | reply
78 |
79 | # This is the message received from the task monitor, if this happens it means we received the :DOWN message without getting the reply message first, which means the task crashed
80 | {:DOWN, ^ref, _, proc, reason} ->
81 | # Exit the linked caller process that is awaiting since the task process crashed
82 | exit({reason(reason, proc), {__MODULE__, :await, [task, timeout]}})
83 |
84 | # more code
85 | end
86 | ```
87 |
88 |
89 |
90 | The other message returned is `{:DOWN, ref, :process, pid, reason}` - since all tasks are also monitored, you will also receive the `:DOWN` message delivered by `Process.monitor/1`. If you receive the :DOWN message without getting the reply message, it means the task crashed.
91 |
92 |
93 |
94 | At any point we can ignore a linked task by calling `Task.ignore/1` which means the task will continue running, but it will be unlinked and we can no longer yield, await or shut it down. Also this means if the task fails the owner process will be unaffected. Lets look at and example...
95 |
96 | ```elixir
97 | time_bomb_task =
98 | Task.async(fn ->
99 | :timer.sleep(2000)
100 | raise "BOOOOM!"
101 | end)
102 | |> IO.inspect()
103 |
104 | IO.inspect(Process.info(self(), :links), label: "Parent process #{inspect(self())} links")
105 |
106 | # Unlink the spawned task
107 | Task.ignore(time_bomb_task)
108 |
109 | IO.inspect(Process.info(self(), :links), label: "Parent process #{inspect(self())} links")
110 |
111 | :timer.sleep(2100)
112 |
113 | IO.puts("Parent process survived!")
114 | ```
115 |
116 | Lets see another example were we launch 3 tasks using the `Task.async/3` function that takes mfa(module function args) as arguments. Each of tasks generating a random number.
117 |
118 | We then await there results and return the sum of the random numbers
119 |
120 | ```elixir
121 | my_task1 = Task.async(Enum, :random, [0..10])
122 | my_task2 = Task.async(Enum, :random, [10..20])
123 | my_task3 = Task.async(Enum, :random, [20..30])
124 |
125 | Task.await(my_task1) + Task.await(my_task2) + Task.await(my_task3)
126 | ```
127 |
128 | In cases were wee need to await multiple tasks the Task module provides a better apporach using the `Task.await_many/1` that awaits replies from multiple tasks and returns them as a list.
129 |
130 | For example we could rewrite the above example like so
131 |
132 | ```elixir
133 | my_task1 = Task.async(Enum, :random, [0..10])
134 | my_task2 = Task.async(Enum, :random, [10..20])
135 | my_task3 = Task.async(Enum, :random, [20..30])
136 |
137 | results = Task.await_many([my_task1, my_task2, my_task3])
138 | IO.inspect(results, label: "Results from await_many")
139 | Enum.sum(results)
140 | ```
141 |
142 | Some important points to note about `Task.await_many/1` are...
143 |
144 | * If any of the task processes dies, the caller process will exit with the same reason as that task.
145 | * It returns a list of the results, in the **same order** as the tasks supplied in the tasks input argument.
146 | * A timeout, in milliseconds or :infinity, can be given with a default value of 5000. If the timeout is exceeded, then the caller process will exit. Any task processes that are linked to the caller process (which is the case when a task is started with async) will also exit. Any task processes that are trapping exits or not linked to the caller process will continue to run.
147 |
148 | ## Task await timeouts
149 |
150 | When calling `Task.await/1` by default the await timeout is 5 seconds after which the caller process will exit. If the task process is linked to the caller process which is the case when a task is started with async, then the task process will also exit. If the task process is trapping exits or not linked to the caller process, then it will continue to run.
151 |
152 | Lets look at an example...
153 |
154 | ```elixir
155 | dont_await_me = Task.async(fn -> :timer.sleep(:infinity) end)
156 | Task.await(dont_await_me)
157 | ```
158 |
159 | The `Task.await/1` function can only be called once for any given task.
160 |
161 | If we want to check if a task has completed or not and not risk the caller process exiting we must use `Task.yield/2`.
162 |
163 | ## Yielding tasks
164 |
165 | Sometimes we only wish to check if a Task is completed within a given timeout, if not we want the caller process to continue. Unlike `Task.await/1` were the caller process exits in cases of timeouts with `Task.yield/2` the caller process will continue to run if the Task has not yet completed within the timeout. Therefore `Task.yield/2` can be called multiple times on the same task.
166 |
167 | Just like await the yield function will also block the caller process until the task completes or the timeout is reached.
168 |
169 | These are the different scenarios when calling `Task.yield/1`
170 |
171 | * When the task process finishes within the yield timeout - Returns `{:ok, result}` were `result` is the value returned by the task.
172 |
173 | * When the task process does not reply within the yield timeout - Returns `nil`. This can happen if the timeout expires OR if the message from the task has already been consumed by the caller.
174 |
175 | * When the task process has already exited OR if the task is not linked to the calling process - Returns `{:exit, reason}`
176 |
177 | Now lets look at some code...
178 |
179 | ```elixir
180 | heavy_task =
181 | Task.async(fn ->
182 | :timer.sleep(5000)
183 | :finished_heavy_task
184 | end)
185 |
186 | Task.yield(heavy_task, 1000) |> IO.inspect(label: "after 1 second")
187 | Task.yield(heavy_task, 1000) |> IO.inspect(label: "after 2 second")
188 | Task.yield(heavy_task, 1000) |> IO.inspect(label: "after 3 second")
189 | Task.yield(heavy_task, 1000) |> IO.inspect(label: "after 4 second")
190 | :timer.sleep(1500)
191 | :erlang.process_info(self(), :messages) |> IO.inspect(label: "Messages in mailbox")
192 | Task.yield(heavy_task, 1000) |> IO.inspect(label: "After task finished")
193 | :erlang.process_info(self(), :messages) |> IO.inspect(label: "Messages in mailbox")
194 | Task.yield(heavy_task, 1000) |> IO.inspect(label: "After message from task was consumed")
195 | ```
196 |
197 | Similar to `Task.await_many/2` we also have `Task.yield_many/2`
198 |
199 | This function receives a list of tasks and waits for their replies in the given time interval. It returns a list of two-element tuples, with the task as the first element and the yielded result as the second. The tasks in the returned list will be in the same order as the tasks supplied in the tasks input argument.
200 |
201 | Similarly to yield/2, each task's result will be `{:ok, term}` if the task has successfully reported its result back in the given time interval or `{:exit, reason}` if the task has died
202 | nil if the task keeps running past the timeout
203 |
204 | ```elixir
205 | tasks =
206 | for i <- 1..10 do
207 | Task.async(fn ->
208 | Process.sleep(i * 1000)
209 | i
210 | end)
211 | end
212 |
213 | tasks_with_results = Task.yield_many(tasks)
214 |
215 | results =
216 | Enum.map(tasks_with_results, fn {task, res} ->
217 | # Shut down the tasks that did not reply or exit
218 | res || Task.shutdown(task, :brutal_kill)
219 | end)
220 |
221 | # Here we are matching only on {:ok, value} and
222 | # ignoring {:exit, _} (crashed tasks) and `nil` (no replies)
223 | for {:ok, value} <- results do
224 | IO.inspect(value)
225 | end
226 | ```
227 |
228 | In the example above, we create tasks that sleep from 1 up to 10 seconds and return the number of seconds they slept for. If you execute the code all at once, you should see 1 up to 4 printed, as those were the tasks that have replied in the default timeout (5 seconds) of `Task.yield_many/1`. All other tasks will have been shut down using the `Task.shutdown/2` call.
229 |
230 | As a convenience, you can achieve a similar behaviour to above by specifying the `:on_timeout` option to be `:kill_task` (or `:ignore`).
231 |
232 | For example to kill all tasks which do not yield within 7 seconds we can write
233 | `Task.yield_many(tasks_list, timeout: 7000, on_timeout: :kill_task)` (this option is available from elixir 1.15.0+)
234 |
235 | ## References
236 |
237 | * https://hexdocs.pm/elixir/1.12/Task.html#content
238 |
239 | ## Navigation
240 |
241 |
255 |
--------------------------------------------------------------------------------
/chapters/ch_7.3_task_async_stream.livemd:
--------------------------------------------------------------------------------
1 | # Task.async_stream/3
2 |
3 | ```elixir
4 | Mix.install([
5 | {:httpoison, "~> 2.1"},
6 | {:jason, "1.4.0"},
7 | {:nimble_csv, "~> 1.2"}
8 | ])
9 | ```
10 |
11 | ## Navigation
12 |
13 |
27 |
28 | ## Introduction
29 |
30 | [Streams](https://hexdocs.pm/elixir/1.12/Stream.html) are a valuable feature in Elixir that allow for lazy emission of elements. Any [enumerable](https://hexdocs.pm/elixir/1.12/Enumerable.html) that generates elements one by one during enumeration is considered a stream. Streams are particularly useful when dealing with large datasets that could consume excessive memory if loaded all at once. With streams, we can manage data lazily, processing elements as needed. To learn more about Stream module check out the documentation [here](https://hexdocs.pm/elixir/1.15.0/Stream.html).
31 |
32 | Now that we have a basic understanding of streams, let's explore the exciting `Task.async_stream/3` function. This function enables **concurrent processing of each element in a enumerable**, unlocking significant potential for parallel execution.
33 |
34 | Since `Task.async_stream/3` works on enumerables it can work on both Streams and Enums.
35 |
36 | Lets look at an example...
37 |
38 | ```elixir
39 | # A function to get a chuk norris joke by calling an api
40 | get_chuknorris_joke = fn ->
41 | HTTPoison.get!("https://api.chucknorris.io/jokes/random")
42 | |> Map.get(:body)
43 | |> Jason.decode!()
44 | |> Map.get("value")
45 | end
46 | ```
47 |
48 | Lets see how much time it takes to make 10 api calls one by one
49 |
50 | ```elixir
51 | Enum.map(1..10, fn _ -> get_chuknorris_joke.() end)
52 | ```
53 |
54 | Now lets try the same using `Task.async_stream/3`
55 |
56 | ```elixir
57 | 1..10
58 | |> Task.async_stream(fn _ -> get_chuknorris_joke.() end)
59 | |> Enum.to_list()
60 | ```
61 |
62 | Observe the significant improvement in the function's execution speed this time. This is because `Task.async_stream/3` launched a separate process to handle each item in the enumerable. In our case, it spawned a separate process for each API call.
63 |
64 | It's important to note that `Task.async_stream/3` returns a stream, which is lazy and won't execute until we consume it. A common way to consume a stream is by using one of the `Enum` functions, such as `Enum.to_list/1` in this case, or by invoking `Stream.run/1`.
65 |
66 | `Task.async_stream/3` also provides various options to customize its behavior. One such option is `:max_concurrency`, which allows us to control the number of tasks running simultaneously. By default, it is set to `System.schedulers_online/0`.
67 |
68 | Another consideration is the ordering of results from `Task.async_stream/3`. By default, Elixir buffers the results to emit them in the original order, as the spawned processes may finish in random order. However, setting the `:ordered` option to `false` removes the need for buffering at the expense of removing ordering.
69 |
70 | For a complete list of options, refer to the [documentation](https://hexdocs.pm/elixir/1.12/Task.html#async_stream/3).
71 |
72 | ## A practical example
73 |
74 | Now, let's explore another practical example of using `Task.async_stream/3`. In this example, we'll read a CSV file containing the top 100 websites.
75 |
76 | The csv file has data in the following format...
77 |
78 | ```csv
79 | 1,"fonts.googleapis.com",10
80 | 2,"facebook.com",10
81 | 3,"twitter.com",10
82 | 4,"google.com",10
83 | 5,"youtube.com",10
84 | ...
85 | ```
86 |
87 | Our goal is to check the reachability of each website by sending an HTTP request to it.
88 |
89 | ```elixir
90 | "#{Path.absname(__DIR__)}/sample_data/top_websites.csv"
91 | |> File.stream!()
92 | |> NimbleCSV.RFC4180.parse_stream()
93 | # Map out the website information from every row in the csv file
94 | |> Stream.map(fn [_, website, _] -> website end)
95 | |> Task.async_stream(&HTTPoison.get/1, timeout: :infinity, ordered: false, max_concurrency: 4)
96 | # Filter out reachable websites
97 | |> Stream.filter(fn
98 | {:ok, _} -> true
99 | _ -> false
100 | end)
101 | |> Enum.count()
102 | |> IO.inspect(label: "Reachable website count")
103 | ```
104 |
105 | Here's a breakdown of the code:
106 |
107 | First, we use `File.stream!/1` to read the CSV file. This function provides a stream that allows us to access the file lazily, avoiding the need to load the entire file into memory.
108 |
109 | Next, we parse the file using the [parse_stream/1](https://hexdocs.pm/nimble_csv/NimbleCSV.html#c:parse_stream/2) function from the [nimble_csv](https://github.com/dashbitco/nimble_csv) library. This gives us a parsed stream of the CSV data.
110 |
111 | We then leverage `Task.async_stream/3` to make a GET request to each website concurrently. Since the order of the responses doesn't matter, we specify `ordered: false`. Additionally, we limit the concurrency to 4 requests at a time using the `max_concurrency: 4` option.
112 |
113 | Finally, we filter out the reachable websites and then count the elements by consuming the stream using the `Enum.count/1` function.
114 |
115 |
116 |
117 | By using `Task.async_stream/3` Elixir enables us to perform concurrent data processing with just a few lines of code. This simplicity and power of concurrent programming in Elixir is truly amazing 🚀
118 |
119 | ## References
120 |
121 | * https://hexdocs.pm/elixir/1.12/Task.html#async_stream/3
122 |
123 | ## Navigation
124 |
125 |
139 |
--------------------------------------------------------------------------------
/chapters/ch_8.0_agents.livemd:
--------------------------------------------------------------------------------
1 | # Agents
2 |
3 | ## Navigation
4 |
5 |
19 |
20 | ## Introduction
21 |
22 | When it comes to storing state in Elixir, we have several options at our disposal. We can utilize the process dictionary for local access, employ GenServers, utilize [ETS tables](https://elixirschool.com/en/lessons/storage/ets), and more. However, one straightforward and convenient approach is to use Agents. Agents provide a simple abstraction for storing state in a process and offer a straightforward API for accessing and updating the stored state. Internally, Agents are implemented as GenServer processes.
23 |
24 | ## Usage
25 |
26 | Using an agent is incredibly straightforward. Let's take a look at a simple example:
27 |
28 | ```elixir
29 | {:ok, my_counter_pid} = Agent.start_link(fn -> 0 end)
30 |
31 | # Retrieve the stored state
32 | Agent.get(my_counter_pid, fn state -> state end)
33 | |> IO.inspect(label: "GET")
34 |
35 | # Perform a GenServer.cast/2 operation on the agent state
36 | # The caller process will not wait for the operation to complete
37 | Agent.cast(my_counter_pid, fn state -> state + 1 end)
38 | |> IO.inspect(label: "CAST")
39 |
40 | # Update the Agent state
41 | # Implemented as a GenServer.call/2 the caller process will wait until the agent process replies
42 | Agent.update(my_counter_pid, fn state -> state + 1 end)
43 | |> IO.inspect(label: "UPDATE")
44 |
45 | # Get and update the state in a single call
46 | # Implemented as a GenServer.call/2
47 | Agent.get_and_update(my_counter_pid, fn state -> {state, state + 1} end)
48 | |> IO.inspect(label: "get_and_update")
49 |
50 | # Retrieve the stored state
51 | Agent.get(my_counter_pid, fn state -> state end)
52 | |> IO.inspect(label: "GET")
53 |
54 | # Stop the agent process
55 | Agent.stop(my_counter_pid)
56 | ```
57 |
58 | A agent can also be implemented as a module.
59 |
60 | ```elixir
61 | defmodule FibonacciStore do
62 | use Agent
63 |
64 | @doc "Starts the FibonacciStore agent"
65 | def start_link(_) do
66 | Agent.start_link(fn -> [0, 1] end, name: __MODULE__)
67 | end
68 |
69 | @doc "Gets the next number in the Fibonacci series"
70 | def next_fibonacci() do
71 | Agent.get_and_update(__MODULE__, fn [num1, num2] ->
72 | next_fibonacci = num1 + num2
73 | {next_fibonacci, [num2, next_fibonacci]}
74 | end)
75 | end
76 |
77 | @doc "Gets the last generated number in the Fibonacci series"
78 | def current_fibonacci() do
79 | Agent.get(__MODULE__, fn [_num1, num2] -> num2 end)
80 | end
81 | end
82 | ```
83 |
84 | Now lets take our agent server for a spin.
85 |
86 | ```elixir
87 | # Stop the agent process if already running
88 | if Process.whereis(FibonacciStore), do: Agent.stop(FibonacciStore)
89 |
90 | # Start the agent process
91 | FibonacciStore.start_link(:noop)
92 |
93 | # Print the current Fibonacci number in the Agent state
94 | FibonacciStore.current_fibonacci() |> IO.inspect(label: "Current Fibonacci")
95 |
96 | # Print the next 10 Fibonacci numbers
97 | Enum.each(1..10, fn _ -> IO.puts(FibonacciStore.next_fibonacci()) end)
98 |
99 | # Print the current Fibonacci number in the Agent state
100 | FibonacciStore.current_fibonacci() |> IO.inspect(label: "Current Fibonacci")
101 |
102 | # Stop the agent process
103 | FibonacciStore
104 | |> Process.whereis()
105 | |> Agent.stop()
106 | ```
107 |
108 | ## Supervising Agents
109 |
110 | Typically, agents are included in a supervision tree, much like GenServers. When we use `use Agent` in our module, it automatically creates a `child_spec/1` function, which enables us to start the agent directly under a supervisor.
111 |
112 | The process of adding an agent to a supervision tree closely resembles that of a GenServer. To illustrate, let's consider starting our FibonacciStore agent under a supervisor:
113 |
114 | ```elixir
115 | # Same as {FibonacciStore, []}
116 | children = [FibonacciStore]
117 | Supervisor.start_link(children, strategy: :one_for_all)
118 |
119 | # Generate 5 numbers in the Fibonacci series
120 | Enum.each(1..5, fn _ -> FibonacciStore.next_fibonacci() end)
121 |
122 | FibonacciStore.current_fibonacci() |> IO.inspect(label: "State before restart")
123 |
124 | # Simulate termination of the agent server to check if the supervisor restarts it
125 | FibonacciStore
126 | |> Process.whereis()
127 | |> Process.exit(:boom)
128 |
129 | # Wait for the supervisor to restart the agent process
130 | :timer.sleep(200)
131 |
132 | # Notice that the agent process was restarted and its state is now set to its initial state
133 | FibonacciStore.current_fibonacci() |> IO.inspect(label: "State after restart")
134 | ```
135 |
136 | In addition, similar to GenServers, the `use Agent` macro also accepts a list of options to customize the child specification and determine its behavior under a supervisor. By providing these options, we can tailor how the agent operates within the supervision tree. The following options can be passed:
137 |
138 | * `:id` - Specifies the identifier for the child specification. By default, it is set to the current module's name.
139 | * `:restart` - Determines the restart strategy for the child. The default value is `:permanent`, which restarts the child process regardless of whether it crashes or is gracefully terminated.
140 |
141 | Here's an example of using the `use Agent` macro with customized options:
142 |
143 |
144 |
145 | ```elixir
146 | use Agent, restart: :transient, shutdown: 10_000
147 | ```
148 |
149 | In the above code, the agent child specification is configured to have a restart strategy of `:transient`, meaning that the child process will only be restarted if it terminates abnormally. Additionally, the shutdown strategy is set to allow a grace period of 10,000 milliseconds for the child to shut down before forcefully terminating it.
150 |
151 | ## Navigation
152 |
153 |
167 |
--------------------------------------------------------------------------------
/chapters/ch_9.0_gotchas.livemd:
--------------------------------------------------------------------------------
1 | # Gotchas
2 |
3 | ## Navigation
4 |
5 |
15 |
16 | ## Loss of sharing
17 |
18 | [Ref](https://medium.com/@johnjocoo/debugging-memory-issues-in-elixir-601c8a0a607d#2e85)
19 |
20 | In Elixir, each process manages its own memory, meaning no data is shared between processes. All data sent between processes is fully copied, which includes data written to or read from an ETS table. During this copying, data is flattened, losing any internal sharing of terms.
21 |
22 | Within a single process, however, [data can be shared](ch_1.2_immutability_and_memory_management.livemd#persistent-datastructures). For instance, if you have a list assigned to a variable, then prepend an element and assign it to another variable, the tail of the original list is shared between both variables, maintaining the same memory allocation.
23 |
24 | Consider preloading associations in Ecto, like Posts and Comments, where a post has many comments. If you fetch 1000 comments and preload their 100 associated posts, Ecto shares these posts among the comments. However, when full copying occurs, each post is duplicated for each comment, resulting in 1000 separate post entries. This process, known as flattening or "loss of sharing," leads to significant memory duplication.
25 |
26 | ## Navigation
27 |
28 |
38 |
--------------------------------------------------------------------------------
/chapters/images/download_manager_architecture.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Arp-G/async-elixir/e4bf46da6816ab54f4359f4593d1a0e3a59e9159/chapters/images/download_manager_architecture.png
--------------------------------------------------------------------------------
/chapters/images/one_for_all.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Arp-G/async-elixir/e4bf46da6816ab54f4359f4593d1a0e3a59e9159/chapters/images/one_for_all.png
--------------------------------------------------------------------------------
/chapters/images/one_for_one.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Arp-G/async-elixir/e4bf46da6816ab54f4359f4593d1a0e3a59e9159/chapters/images/one_for_one.png
--------------------------------------------------------------------------------
/chapters/images/rest_for_one.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Arp-G/async-elixir/e4bf46da6816ab54f4359f4593d1a0e3a59e9159/chapters/images/rest_for_one.png
--------------------------------------------------------------------------------
/chapters/sample_data/top_websites.csv:
--------------------------------------------------------------------------------
1 | 1,"fonts.googleapis.com",10
2 | 2,"facebook.com",10
3 | 3,"twitter.com",10
4 | 4,"google.com",10
5 | 5,"youtube.com",10
6 | 6,"s.w.org",10
7 | 7,"instagram.com",10
8 | 8,"googletagmanager.com",10
9 | 9,"linkedin.com",10
10 | 10,"ajax.googleapis.com",10
11 | 11,"plus.google.com",10
12 | 12,"gmpg.org",10
13 | 13,"pinterest.com",9.63
14 | 14,"fonts.gstatic.com",9.6
15 | 15,"wordpress.org",9.54
16 | 16,"en.wikipedia.org",9.54
17 | 17,"youtu.be",9.47
18 | 18,"maps.google.com",9.3
19 | 19,"itunes.apple.com",9.21
20 | 20,"github.com",9.18
21 | 21,"bit.ly",9.11
22 | 22,"play.google.com",9.07
23 | 23,"goo.gl",9.03
24 | 24,"docs.google.com",9.02
25 | 25,"cdnjs.cloudflare.com",8.99
26 | 26,"vimeo.com",8.98
27 | 27,"support.google.com",8.87
28 | 28,"google-analytics.com",8.8
29 | 29,"maps.googleapis.com",8.79
30 | 30,"flickr.com",8.76
31 | 31,"vk.com",8.74
32 | 32,"t.co",8.72
33 | 33,"reddit.com",8.69
34 | 34,"amazon.com",8.66
35 | 35,"medium.com",8.64
36 | 36,"sites.google.com",8.57
37 | 37,"drive.google.com",8.51
38 | 38,"creativecommons.org",8.47
39 | 39,"microsoft.com",8.47
40 | 40,"developers.google.com",8.46
41 | 41,"adobe.com",8.44
42 | 42,"soundcloud.com",8.41
43 | 43,"theguardian.com",8.38
44 | 44,"apis.google.com",8.35
45 | 45,"ec.europa.eu",8.33
46 | 46,"lh3.googleusercontent.com",8.3
47 | 47,"chrome.google.com",8.28
48 | 48,"cloudflare.com",8.27
49 | 49,"nytimes.com",8.26
50 | 50,"maxcdn.bootstrapcdn.com",8.25
51 | 51,"support.microsoft.com",8.25
52 | 52,"blogger.com",8.25
53 | 53,"forbes.com",8.24
54 | 54,"s3.amazonaws.com",8.23
55 | 55,"code.jquery.com",8.23
56 | 56,"dropbox.com",8.19
57 | 57,"translate.google.com",8.15
58 | 58,"paypal.com",8.14
59 | 59,"apps.apple.com",8.14
60 | 60,"tinyurl.com",8.12
61 | 61,"etsy.com",8.1
62 | 62,"theatlantic.com",8.09
63 | 63,"m.facebook.com",8.08
64 | 64,"archive.org",8.05
65 | 65,"amzn.to",8.04
66 | 66,"cnn.com",8.04
67 | 67,"policies.google.com",8.02
68 | 68,"commons.wikimedia.org",8.02
69 | 69,"issuu.com",8.01
70 | 70,"i.imgur.com",8
71 | 71,"wordpress.com",8
72 | 72,"wp.me",7.99
73 | 73,"businessinsider.com",7.98
74 | 74,"yelp.com",7.98
75 | 75,"mail.google.com",7.98
76 | 76,"support.apple.com",7.97
77 | 77,"t.me",7.94
78 | 78,"apple.com",7.92
79 | 79,"washingtonpost.com",7.92
80 | 80,"bbc.com",7.92
81 | 81,"gstatic.com",7.92
82 | 82,"imgur.com",7.91
83 | 83,"amazon.de",7.91
84 | 84,"bbc.co.uk",7.9
85 | 85,"googleads.g.doubleclick.net",7.9
86 | 86,"mozilla.org",7.89
87 | 87,"eventbrite.com",7.89
88 | 88,"slideshare.net",7.88
89 | 89,"w3.org",7.87
90 | 90,"forms.gle",7.86
91 | 91,"platform.twitter.com",7.85
92 | 92,"accounts.google.com",7.84
93 | 93,"telegraph.co.uk",7.82
94 | 94,"messenger.com",7.82
95 | 95,"web.archive.org",7.81
96 | 96,"secure.gravatar.com",7.81
97 | 97,"usatoday.com",7.79
98 | 98,"huffingtonpost.com",7.78
99 | 99,"stackoverflow.com",7.78
100 | 100,"fb.com",7.78
101 |
--------------------------------------------------------------------------------