-
-
Notifications
You must be signed in to change notification settings - Fork 402
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
New concept exercise: take-a-number-deluxe
(genserver
)
#1076
Merged
Merged
Changes from 24 commits
Commits
Show all changes
40 commits
Select commit
Hold shift + click to select a range
32e05fd
Create boilerplate
angelikatyborska bbb7688
First draft of an example solution
angelikatyborska 759057f
First draft of tests
angelikatyborska 3b957a7
Simplify by removing "currently serving"
angelikatyborska 70cd1bb
Remove unnecessary alias
angelikatyborska 309b342
Add passing user input
angelikatyborska 9a2f021
Fix boilerplate
angelikatyborska 96e04f9
Extend the story
angelikatyborska 5ee9ae4
Update exercises/concept/take-a-number-deluxe/.docs/instructions.md
angelikatyborska 875e56e
Pass module name to @impl
angelikatyborska 900f38d
Add typespecs to state
angelikatyborska 5e13b0c
Use erlang's :queue
angelikatyborska 12fbbcb
Add jie as contributor
angelikatyborska fc50f25
Merge branch 'main' into add-genserver-exercise
angelikatyborska 0a450e6
Set up configs
angelikatyborska 556827d
Add missing task id
angelikatyborska 03adc6d
Implement auto shutdown with timeouts
angelikatyborska b2d9ffa
Add specs to the boilerplate
angelikatyborska 2171b9a
Write instructions
angelikatyborska 7d3dd34
Write GenServer introduction
angelikatyborska eea8c65
Write hints
angelikatyborska 9c311f4
Write concept blurb
angelikatyborska 121c98a
Add concept to practice exercises
angelikatyborska a44260f
Fix spec mistakes
angelikatyborska 1477627
Build our own queue module
jiegillet dca2d36
Add queue to editor files
jiegillet 0f1d4bc
Fix warning
jiegillet ee0276e
Remove difficulty warning
angelikatyborska c302cde
Add explanation comment to queue
angelikatyborska 740cd0d
Merge branch 'main' into add-genserver-exercise
angelikatyborska 2ec9990
Merge branch 'main' into add-genserver-exercise
angelikatyborska c9f9363
Configlet fmt
angelikatyborska ba73c1f
Update exercises/concept/take-a-number-deluxe/.docs/instructions.md
angelikatyborska 334ad00
Update exercises/concept/take-a-number-deluxe/.docs/hints.md
angelikatyborska bc89504
Remove erl libs from requirements
angelikatyborska 961700b
Add genserver to circular-buffer
angelikatyborska b6ddbdf
Update exercises/concept/take-a-number-deluxe/.docs/instructions.md
angelikatyborska 6fc13a8
Fix spec of start_link
angelikatyborska c257071
Use cond in example
angelikatyborska 6274f17
Copy intro to concept intro and about
angelikatyborska File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
{ | ||
"blurb": "GenServer is a behaviour that abstracts common client-server interactions between Elixir processes.", | ||
"authors": [ | ||
"angelikatyborska" | ||
] | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
# About | ||
|
||
TODO: copy exercise introduction | ||
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
# Introduction | ||
|
||
TODO: copy exercise introduction | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. TODO before merge, after exercise introduction is approved. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
[ | ||
{ | ||
"url": "https://elixir-lang.org/getting-started/mix-otp/genserver.html", | ||
"description": "Getting Started - GenServer" | ||
}, | ||
{ | ||
"url": "https://hexdocs.pm/elixir/GenServer.html", | ||
"description": "Documentation - Genserver" | ||
}, | ||
{ | ||
"url": "https://en.wikipedia.org/wiki/Actor_model", | ||
"description": "Actor model" | ||
} | ||
] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,81 @@ | ||
# Hints | ||
|
||
## General | ||
|
||
- Read about `GenServer` in the official [Getting Started guide][getting-started-genserver]. | ||
- Read about `GenServer` on [elixirschool.com][elixir-school-genserver]. | ||
- Read about the `GenServer` behaviour [in the documentation][genserver]. | ||
|
||
## 1. Start the machine | ||
|
||
- Remember to [use][use] the [`GenServer` behaviour][genserver]. | ||
- There is [a built-in function][start-link] that starts a linked `GenServer` process. The only thing that `TakeANumberDeluxe.start_link/2` needs to do is call that function with the right arguments. | ||
- `__MODULE__` is a special variable that holds the name of the current module. | ||
- Implement the [`GenServer` callback used when starting the process][init]. | ||
- The callback should return either `{:ok, state}` or `{:error, reason}`. | ||
angelikatyborska marked this conversation as resolved.
Show resolved
Hide resolved
|
||
- Read the options from the `init_arg` keyword list. | ||
- There is [a built-in function][keyword-get] to get a value from a keyword list. | ||
- Use `TakeANumberDeluxe.State.new/2` to get the initial state. | ||
- Use `@impl` above your callback implementation to mark which behaviour this callback comes from. | ||
|
||
## 2. Report machine state | ||
|
||
- There is [a built-in function][call] that sends a message to a `GenServer` process and receives a reply. The only thing that `TakeANumberDeluxe.report_state/1` needs to do is call that function with the right arguments. | ||
- The messages sent to a server can be anything, but atoms are best. | ||
- Implement the [`GenServer` callback used when handling messages that need a reply][handle-call]. | ||
- The callback should return `{:reply, reply, state}`. | ||
- Pass the state as the reply. | ||
- Use `@impl` above your callback implementation to mark which behaviour this callback comes from. | ||
|
||
## 3. Queue new numbers | ||
|
||
- There is [a built-in function][call] that sends a message to a `GenServer` process and receives a reply. The only thing that `TakeANumberDeluxe.queue_new_number/1` needs to do is call that function with the right arguments. | ||
- The messages sent to a server can be anything, but atoms are best. | ||
- Implement the [`GenServer` callback used when handling messages that need a reply][handle-call]. | ||
- The callback should return `{:reply, reply, state}`. | ||
- Get the reply and the new state by calling `TakeANumberDeluxe.State.queue_new_number/1`. Use a [`case`][case] expression to pattern match the return value. | ||
- The reply should be either `{:ok, new_number}` or `{:error, error}`. | ||
- Use `@impl` above your callback implementation to mark which behaviour this callback comes from. | ||
|
||
## 4. Serve next queued number | ||
|
||
- There is [a built-in function][call] that sends a message to a `GenServer` process and receives a reply. The only thing that `TakeANumberDeluxe.serve_next_queued_number/2` needs to do is call that function with the right arguments. | ||
- The messages sent to a server can be anything, but tuples are best if an argument needs to be sent in the message. Use a message like this: `{:my_message_name, some_argument}`. | ||
- Implement the [`GenServer` callback used when handling messages that need a reply][handle-call]. | ||
- The callback should return `{:reply, reply, state}`. | ||
- Get the reply and the new state by calling `TakeANumberDeluxe.State.serve_next_queued_number/2`. Use a [`case`][case] expression to pattern match the return value. | ||
- The reply should be either `{:ok, next_number}` or `{:error, error}`. | ||
- Use `@impl` above your callback implementation to mark which behaviour this callback comes from. | ||
|
||
## 5. Reset state | ||
|
||
- There is [a built-in function][cast] that sends a message to a `GenServer` process and does not wait for a reply. The only thing that `TakeANumberDeluxe.reset_state/1` needs to do is call that function with the right arguments. | ||
- The messages sent to a server can be anything, but atoms are best. | ||
- Implement the [`GenServer` callback used when handling messages that do not need a reply][handle-cast]. | ||
- The callback should return `{:noreply, state}`. | ||
- Use `TakeANumberDeluxe.State.new/2` to get the new state, just like in `init/1`. | ||
- Use `@impl` above your callback implementation to mark which behaviour this callback comes from. | ||
|
||
## 6. Implement auto shutdown | ||
|
||
- Extend all `init/1` and `handle_*` callbacks to return one extra element in their tuples. Its value should be `state.auto_shutdown_timeout`. | ||
- Implement the [`GenServer` callback used when handling messages that weren't sent in the usual `GenServer` way][handle-info]. | ||
- This callback needs to handle `:timeout` messages and exit the process, but also catch and ignore any other messages. | ||
- There is [a built-in function][exit] that exits the current process. | ||
- The exit reason should be `:normal`. | ||
|
||
[getting-started-genserver]: https://elixir-lang.org/getting-started/mix-otp/genserver.html | ||
[elixir-school-genserver]: https://elixirschool.com/en/lessons/advanced/otp_concurrency | ||
[genserver]: https://hexdocs.pm/elixir/GenServer.html | ||
[use]: https://hexdocs.pm/elixir/Kernel.html#use/2 | ||
[impl]: https://hexdocs.pm/elixir/Module.html#module-impl | ||
[start-link]: https://hexdocs.pm/elixir/GenServer.html#start_link/3 | ||
[call]: https://hexdocs.pm/elixir/GenServer.html#call/2 | ||
[cast]: https://hexdocs.pm/elixir/GenServer.html#cast/2 | ||
[init]: https://hexdocs.pm/elixir/GenServer.html#c:init/1 | ||
[handle-call]: https://hexdocs.pm/elixir/GenServer.html#c:handle_call/3 | ||
[handle-cast]: https://hexdocs.pm/elixir/GenServer.html#c:handle_cast/2 | ||
[handle-info]: https://hexdocs.pm/elixir/GenServer.html#c:handle_info/2 | ||
[keyword-get]: https://hexdocs.pm/elixir/Keyword.html#get/3 | ||
[case]: https://hexdocs.pm/elixir/Kernel.SpecialForms.html#case/2 | ||
[exit]: https://hexdocs.pm/elixir/Process.html#exit/2 |
128 changes: 128 additions & 0 deletions
128
exercises/concept/take-a-number-deluxe/.docs/instructions.md
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,128 @@ | ||
# Instructions | ||
|
||
The basic Take-A-Number machine was selling really well, but some users were complaining about its lack of advanced features compared to other models available on the market. | ||
|
||
The manufacturer listened to user feedback and decided to release a deluxe model with more features, and you once again were tasked with writing the software for this machine. | ||
|
||
The new features added to the deluxe model include: | ||
- Keeping track of currently queued numbers. | ||
- Setting the minimum and maximum number. This will allow using multiple deluxe Take-A-Number machines for queueing customers to different departments at the same facility, and to tell apart the departments by the number range. | ||
angelikatyborska marked this conversation as resolved.
Show resolved
Hide resolved
|
||
- Allowing certain numbers to skip the queue to provide priority service to pregnant women and the elderly. | ||
- Auto shutdown to prevent accidentally leaving the machine on for the whole weekend and wasting energy. | ||
|
||
The business logic of the machine was already implemented by your colleague and can be found in the module `TakeANumberDeluxe.State`. Now your task is to wrap it in a `GenServer`. | ||
|
||
## 1. Start the machine | ||
|
||
Use the `GenServer` behaviour in the `TakeANumberDeluxe` module. | ||
|
||
Implement the `start_link/1` function and the necessary `GenServer` callback. | ||
|
||
The argument passed to `start_link/1` is a keyword list. It contains the keys `:min_number` and `:max_number`. The values under those keys need to be passed to the function `TakeANumberDeluxe.State.new/2`. | ||
|
||
If `TakeANumberDeluxe.State.new/2` returns an `{:ok, state}` tuple, the machine should start, using the returned state as its state. If it returns an `{:error, error}` tuple instead, the machine should stop, giving the returned error as the reason for stopping. | ||
|
||
```elixir | ||
TakeANumberDeluxe.start_link(min_number: 1, max_number: 9) | ||
# => {:ok, #PID<0.174.0>} | ||
|
||
TakeANumberDeluxe.start_link(min_number: 9, max_number: 1) | ||
# => {:error, :invalid_configuration} | ||
``` | ||
|
||
You might have noticed that the function `TakeANumberDeluxe.State.new/2` also takes an optional third argument, `auto_shutdown_timeout`. We will use it in the last step of this exercise. | ||
|
||
## 2. Report machine state | ||
|
||
Implement the `report_state/1` function and the necessary `GenServer` callback. The machine should reply to the caller with its current state. | ||
|
||
```elixir | ||
{:ok, machine} = TakeANumberDeluxe.start_link(min_number: 1, max_number: 10) | ||
TakeANumberDeluxe.report_state(machine) | ||
# => %TakeANumberDeluxe.State{ | ||
# max_number: 10, | ||
# min_number: 1, | ||
# queue: {[], []}, | ||
angelikatyborska marked this conversation as resolved.
Show resolved
Hide resolved
|
||
# auto_shutdown_timeout: :infinity, | ||
# } | ||
``` | ||
|
||
## 3. Queue new numbers | ||
|
||
Implement the `queue_new_number/1` function and the necessary `GenServer` callback. | ||
|
||
It should call the `TakeANumberDeluxe.State.queue_new_number/1` function with the current state of the machine. | ||
|
||
If `TakeANumberDeluxe.State.queue_new_number/1` returns an `{:ok, new_number, new_state}` tuple, the machine should reply to the caller with the new number and set the new state as its state. If it returns a `{:error, error}` tuple instead, the machine should reply to the caller with the error and not change its state. | ||
|
||
```elixir | ||
{:ok, machine} = TakeANumberDeluxe.start_link(min_number: 1, max_number: 2) | ||
TakeANumberDeluxe.queue_new_number(machine) | ||
# => {:ok, 1} | ||
|
||
TakeANumberDeluxe.queue_new_number(machine) | ||
# => {:ok, 2} | ||
|
||
TakeANumberDeluxe.queue_new_number(machine) | ||
# => {:error, :all_possible_numbers_are_in_use} | ||
``` | ||
|
||
## 4. Serve next queued number | ||
|
||
Implement the `serve_next_queued_number/2` function and the necessary `GenServer` callback. | ||
|
||
It should call the `TakeANumberDeluxe.State.serve_next_queued_number/2` function with the current state of the machine and its second optional argument, `priority_number`. | ||
|
||
If `TakeANumberDeluxe.State.serve_next_queued_number/2` returns an `{:ok, next_number, new_state}` tuple, the machine should reply to the caller with the next number and set the new state as its state. If it returns a `{:error, error}` tuple instead, the machine should reply to the caller with the error and not change its state. | ||
|
||
```elixir | ||
{:ok, machine} = TakeANumberDeluxe.start_link(min_number: 1, max_number: 10) | ||
TakeANumberDeluxe.queue_new_number(machine) | ||
# => {:ok, 1} | ||
|
||
TakeANumberDeluxe.serve_next_queued_number(machine) | ||
# => {:ok, 1} | ||
|
||
TakeANumberDeluxe.serve_next_queued_number(machine) | ||
# => {:error, :empty_queue} | ||
``` | ||
|
||
## 5. Reset state | ||
|
||
Implement the `reset_state/1` function and the necessary `GenServer` callback. | ||
|
||
It should call the `TakeANumberDeluxe.State.new/2` function to create a new state using the current state's `min_number` and `max_number`. The machine should set the new state as its state. It should not reply to the caller. | ||
|
||
```elixir | ||
{:ok, machine} = TakeANumberDeluxe.start_link(min_number: 1, max_number: 10) | ||
|
||
TakeANumberDeluxe.reset_state(machine) | ||
# => :ok | ||
``` | ||
|
||
## 6. Implement auto shutdown | ||
|
||
Modify starting the machine. It should read the value under the key `:auto_shutdown_timeout` in the keyword list passed as `init_arg` and pass it as the third argument to `TakeANumberDeluxe.State.new/3`. | ||
|
||
Modify resetting the machine state to also pass `auto_shutdown_timeout` to `TakeANumberDeluxe.State.new/3`. | ||
|
||
Modify the return values of all implemented callbacks (`init/1` and all `handle_*` callbacks) to set a timeout. Use the the value under the key `:auto_shutdown_timeout` in the current machine state. | ||
|
||
Implement a `GenServer` callback to handle the `:timeout` message that will be sent to the machine if it doesn't receive any other messages within the given timeout. It should exit the process with reason `:normal`. | ||
|
||
Make sure to also handle any unexpected messages by ignoring them. | ||
|
||
```elixir | ||
{:ok, machine} = TakeANumberDeluxe.start_link( | ||
min_number: 1, | ||
max_number: 10, | ||
auto_shutdown_timeout: :timer.hours(2) | ||
) | ||
|
||
# after 3 hours... | ||
|
||
TakeANumberDeluxe.queue_new_number(machine) | ||
# => ** (exit) exited in: GenServer.call(#PID<0.171.0>, :queue_new_number, 5000) | ||
# ** (EXIT) no process: the process is not alive or there's no process currently associated with the given name, possibly because its application isn't started | ||
# (elixir 1.13.0) lib/gen_server.ex:1030: GenServer.call/3 | ||
``` |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
TODO before merge, after exercise introduction is approved.