-
-
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
Conversation
The tests, boilerplate, and exemplar solution are ready for review. I want to get those approved before I continue with detailed instructions and hints. It was really hard to find something that:
Overall the exemplar solution is pretty big and this task will require more work than usual, but I don't see a way of making it smaller without compromising on the learning goals. |
@impl true | ||
def handle_info({:take_a_number, sender_pid}, state) do | ||
send(sender_pid, {:error, :basic_model_message_received_by_deluxe_model_server}) | ||
{:noreply, state} | ||
end | ||
|
||
@impl true | ||
def handle_info(_, state) do | ||
{:noreply, state} | ||
end |
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.
Running use GenServer
already creates a catch-all handle_info
, so I had to get creative to push students to implement it themselves and actually be able to assert they did. At first I wanted just a normal catch-all that calls IO.inspect
but I couldn't figure out how to assert on the IO. The normal capture IO in ExUnit doesn't work because the IO happens in a different process.
So I came up with this, and I'm going to justify it with something like: "when testing the initial prototype of the new machine, somebody did something very weird in the client code and ended up sending the old messages meant for the basic Take-A-Number machine to the new GenServer based one instead. They were really annoyed that they didn't receive any responses or any errors, and asked you to add this response for them so they realize what's going on the next time". A bit far-fetched but I was out of ideas.
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.
If you want another idea for justifying handle_info
, Elixir in Action uses the example of sending messages at regular intervals
def init(_) do
:timer.send_interval(5000, :cleanup)
...
end
You could say that the number queue should be reset once a day.
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.
Another way to push students to implement the handle_info
callback is to use GenServer's own timeout. You can say in history that for "economy" reasons the machine should be turned off after a period of inactivity.
Example:
@timeout 1500
def init(_), do: {:ok, ..., @timeout}
def handle_call(..., ..., ...), do: {:reply, ..., ..., @timeout}
def handle_cast(..., ...), do: {:noreply, ..., @timeout}
def handle_info(:timeout, ...), do: exit(:normal) # energy_is_expensive
That way, if the student doesn't implement this callback, the process will keep running and the tests should fail, since the match all handle_info
just calls a :logger.error
Process.sleep(1000)
assert Process.alive?(pid)
Process.sleep(1000)
refute Process.alive?(pid)
This behavior should be easy to test, but these timers in the tests scares me
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.
@Tyyagoo That sounds brilliant! I totally forgot about the "timeouts" feature. I will try to incorporate your idea.
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.
Great work!
I think the exercise makes sense in a "real world" kind of way, and I don't think it will be that hard for students to complete, because all of the heavy lifting is done in TakeANumberDeluxe.State
, all they have to do it call the right functions and check for errors.
I would suggest adding some @doc
s to the TakeANumberDeluxe.State
functions, because it's important to quickly grasp what the functions do to pick the right one, but the implementations are not trivial.
exercises/concept/take-a-number-deluxe/lib/take_a_number_deluxe/state.ex
Show resolved
Hide resolved
genserver
take-a-number-deluxe
(genserver
)
assert TakeANumberDeluxe.serve_next_queued_number(pid) == {:ok, 2} | ||
|
||
queue = | ||
:queue.delete(2, :queue.delete(1, :queue.in(3, :queue.in(2, :queue.in(1, :queue.new()))))) |
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.
This looks ridiculous, it's not equal to from_list([3])
and the pipe cannot be used 😂
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.
That's one reason why we code in Elixir and not in Erlang :)
Thank you for contributing to Based on the files changed in this PR, it would be good to pay attention to the following details when reviewing the PR:
Automated comment created by PR Commenter 🤖. |
~~~~exercism/caution | ||
This is an advanced topic. | ||
~~~~ |
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.
I added this as a warning because I'm feeling a bit nervous about how big this exercise ended up being. Do you think this is a good idea?
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.
Honestly, I don't think it's that advanced.
I first implemented a GenServer
on my own in the bank-account
exercise, and it was very very smooth, the official documentation is very clear for anyone who needs more help.
It may have been complicated to implement TakeANumberDeluxe.State
but the students don't have to, all they have to do is connect the dots, there is basically no logic to implement.
I think top-secret
was more complex.
{:ok, next_number, %{state | queue: new_queue}} | ||
|
||
:queue.member(priority_number, state.queue) -> | ||
{:ok, priority_number, %{state | queue: :queue.delete(priority_number, state.queue)}} |
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.
Looks like :queue.delete
is only available from OTP 24. @jiegillet what would you propose as a fix? Back to my weird list-based queue? Or add a check with function_exported?(:queue, :delete, 2)
and use to_list
, Enum.filter
, and from_list
to fake delete
?
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.
None of these suggestions sparked joy, so I implemented and pushed a queue module. Feel free to revert if it's not to your liking :)
I figured this exercise is already complicated, so this won't make a real difference :)
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.
Thank you! I'm glad you didn't make me implement a queue myself 😁. I just added an explanation comment at the top of the queue module.
Something vague is broken with Elixir 1.9 + OTP 20 according to CI:
It will take me a while to debug because installing OTP below 22 doesn't work (either out of the box or at all?) on my new mac. I will have to debug via Docker. |
I had to build my own Docker image for OTP 20 because the highest Elixir version on DockerHub with OTP 20 is 1.6. For some reason, We can either add a hack to the test like this: otp_version = String.to_integer(System.otp_release())
if otp_version <= 20, do: Process.flag(:trap_exit, true)
# assert stuff
if otp_version <= 20, do: Process.flag(:trap_exit, false) Or drop OTP 20 support and pair 1.8 and 1.9 with OTP 21. Or try OTP 20.3 instead of 20.0 on CI, hoping that this was a bug that they somehow fixed at some point. According to https://www.erlang.org/patches/otp-21.0 OTP 21 was first released on 2018-06-19, almost 4 years ago, and OTP 20 was last released on 2020-02-28, 2 years ago. |
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.
Great work! You had to jump through so many hoops on this one :)
I left a bunch of small comments before we merge.
Co-authored-by: Jie <[email protected]>
Co-authored-by: Jie <[email protected]>
Co-authored-by: Jie <[email protected]>
@jiegillet Big thanks for the two huge reviews 🙏 I am so happy we will finally fill the GenServer shaped hole in the syllabus... |
Resolves #559
Heavily WIP!I'm opening this PR to signal that I'm working on this.Now ready for final review.
Requires #1103 to be merged first to resolve configlet errors and to be able to unlock this exercise.