Skip to content
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 40 commits into from
Apr 11, 2022
Merged
Show file tree
Hide file tree
Changes from 24 commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
32e05fd
Create boilerplate
angelikatyborska Mar 6, 2022
bbb7688
First draft of an example solution
angelikatyborska Mar 6, 2022
759057f
First draft of tests
angelikatyborska Mar 6, 2022
3b957a7
Simplify by removing "currently serving"
angelikatyborska Mar 6, 2022
70cd1bb
Remove unnecessary alias
angelikatyborska Mar 6, 2022
309b342
Add passing user input
angelikatyborska Mar 6, 2022
9a2f021
Fix boilerplate
angelikatyborska Mar 6, 2022
96e04f9
Extend the story
angelikatyborska Mar 6, 2022
5ee9ae4
Update exercises/concept/take-a-number-deluxe/.docs/instructions.md
angelikatyborska Mar 27, 2022
875e56e
Pass module name to @impl
angelikatyborska Mar 27, 2022
900f38d
Add typespecs to state
angelikatyborska Mar 27, 2022
5e13b0c
Use erlang's :queue
angelikatyborska Mar 27, 2022
12fbbcb
Add jie as contributor
angelikatyborska Mar 27, 2022
fc50f25
Merge branch 'main' into add-genserver-exercise
angelikatyborska Mar 27, 2022
0a450e6
Set up configs
angelikatyborska Mar 27, 2022
556827d
Add missing task id
angelikatyborska Apr 2, 2022
03adc6d
Implement auto shutdown with timeouts
angelikatyborska Apr 2, 2022
b2d9ffa
Add specs to the boilerplate
angelikatyborska Apr 2, 2022
2171b9a
Write instructions
angelikatyborska Apr 2, 2022
7d3dd34
Write GenServer introduction
angelikatyborska Apr 2, 2022
eea8c65
Write hints
angelikatyborska Apr 2, 2022
9c311f4
Write concept blurb
angelikatyborska Apr 2, 2022
121c98a
Add concept to practice exercises
angelikatyborska Apr 2, 2022
a44260f
Fix spec mistakes
angelikatyborska Apr 2, 2022
1477627
Build our own queue module
jiegillet Apr 3, 2022
dca2d36
Add queue to editor files
jiegillet Apr 3, 2022
0f1d4bc
Fix warning
jiegillet Apr 3, 2022
ee0276e
Remove difficulty warning
angelikatyborska Apr 4, 2022
c302cde
Add explanation comment to queue
angelikatyborska Apr 4, 2022
740cd0d
Merge branch 'main' into add-genserver-exercise
angelikatyborska Apr 10, 2022
2ec9990
Merge branch 'main' into add-genserver-exercise
angelikatyborska Apr 10, 2022
c9f9363
Configlet fmt
angelikatyborska Apr 10, 2022
ba73c1f
Update exercises/concept/take-a-number-deluxe/.docs/instructions.md
angelikatyborska Apr 11, 2022
334ad00
Update exercises/concept/take-a-number-deluxe/.docs/hints.md
angelikatyborska Apr 11, 2022
bc89504
Remove erl libs from requirements
angelikatyborska Apr 11, 2022
961700b
Add genserver to circular-buffer
angelikatyborska Apr 11, 2022
b6ddbdf
Update exercises/concept/take-a-number-deluxe/.docs/instructions.md
angelikatyborska Apr 11, 2022
6fc13a8
Fix spec of start_link
angelikatyborska Apr 11, 2022
c257071
Use cond in example
angelikatyborska Apr 11, 2022
6274f17
Copy intro to concept intro and about
angelikatyborska Apr 11, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions concepts/genserver/.meta/config.json
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"
]
}
3 changes: 3 additions & 0 deletions concepts/genserver/about.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# About

TODO: copy exercise introduction
Copy link
Member Author

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.

3 changes: 3 additions & 0 deletions concepts/genserver/introduction.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Introduction

TODO: copy exercise introduction
Copy link
Member Author

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.

14 changes: 14 additions & 0 deletions concepts/genserver/links.json
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"
}
]
32 changes: 30 additions & 2 deletions config.json
Original file line number Diff line number Diff line change
Expand Up @@ -614,6 +614,26 @@
"enum"
],
"status": "active"
},
{
"slug": "take-a-number-deluxe",
"name": "Take-A-Number Deluxe",
"uuid": "93e8ef96-0c76-4466-bd02-f16c4fb29c12",
"concepts": [
"genserver"
],
"prerequisites": [
"processes",
"pids",
"agent",
"behaviours",
"structs",
"tuples",
"keyword-lists",
"typespecs",
"erlang-libraries"
],
"status": "beta"
}
],
"practice": [
Expand Down Expand Up @@ -1962,7 +1982,8 @@
"if",
"processes",
"pids",
"agent"
"agent",
"genserver"
],
"practices": [
"if",
Expand Down Expand Up @@ -2714,10 +2735,12 @@
"structs",
"pids",
"processes",
"genserver",
"recursion"
],
"practices": [
"processes"
"processes",
"genserver"
],
"difficulty": 8
},
Expand Down Expand Up @@ -2990,6 +3013,11 @@
"slug": "floating-point-numbers",
"name": "Floating Point Numbers"
},
{
"uuid": "23ac8ee4-382d-4297-a7a0-bf72dca94150",
"slug": "genserver",
"name": "GenServer"
},
{
"uuid": "5dd6841b-3875-499c-9de7-4cf4d033e68f",
"slug": "guards",
Expand Down
81 changes: 81 additions & 0 deletions exercises/concept/take-a-number-deluxe/.docs/hints.md
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}`.
- 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 exercises/concept/take-a-number-deluxe/.docs/instructions.md
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.
- 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: {[], []},
# 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
```
Loading