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

Conversation

angelikatyborska
Copy link
Member

@angelikatyborska angelikatyborska commented Mar 6, 2022

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.

@angelikatyborska angelikatyborska added x:size/large Large amount of work x:module/concept-exercise Work on Concept Exercises x:type/content Work on content (e.g. exercises, concepts) labels Mar 6, 2022
@angelikatyborska
Copy link
Member Author

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:

  • makes sense in the real world,
  • uses all three handle_call, handle_cast, and handle_info, and uses handle_call both without user input and with user input.

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.

Comment on lines 72 to 81
@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
Copy link
Member Author

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.

Copy link
Contributor

@jiegillet jiegillet Mar 7, 2022

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.

Copy link
Contributor

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

Copy link
Member Author

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.

Copy link
Contributor

@jiegillet jiegillet left a 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 @docs 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.

@angelikatyborska angelikatyborska changed the title New concept exercise: genserver New concept exercise: take-a-number-deluxe (genserver) Mar 27, 2022
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())))))
Copy link
Member Author

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 😂

Copy link
Contributor

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 :)

@github-actions
Copy link
Contributor

Thank you for contributing to exercism/elixir 💜 🎉. This is an automated PR comment 🤖 for the maintainers of this repository that helps with the PR review process. You can safely ignore it and wait for a maintainer to review your changes.

Based on the files changed in this PR, it would be good to pay attention to the following details when reviewing the PR:

  • General steps

    • 🏆 Does this PR need to receive a label with a reputation modifier (x:size/{tiny,small,medium,large,massive})? (A medium reputation amount is awarded by default, see docs)
  • Any exercise changed

    • 👤 Does the author of the PR need to be added as an author or contributor in <exercise>/.meta/config.json (see docs)?
    • 🔬 Do the analyzer and the analyzer comments exist for this exercise? Do they need to be changed?
    • 📜 Does the design file (<exercise>/.meta/design.md) need to be updated to document new implementation decisions?
  • Concept exercise changed

    • 🌲 Do prerequisites and practices in config.json need to be updated?
    • 📖 Does the concept introduction provide all necessary information to solve this exercise?
  • Concept exercise tests changed

    • ⚪️ Are all tests un-skipped?
    • 🔢 Are all tests annotated with @tag task_id?
    • 🐈 Can all tests be understood by reading the test's block only (e.g. no module attributes, no setup functions)?
  • Concept changed

    • 👤 Does the author of the PR need to be added as an author or contributor in <concept>/.meta/config.json (see docs)?
    • 📖 Does the concept introduction provide all necessary information to solve this exercise?

Automated comment created by PR Commenter 🤖.

Comment on lines 5 to 7
~~~~exercism/caution
This is an advanced topic.
~~~~
Copy link
Member Author

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?

Copy link
Contributor

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)}}
Copy link
Member Author

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?

Copy link
Contributor

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 :)

Copy link
Member Author

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.

@angelikatyborska angelikatyborska marked this pull request as ready for review April 2, 2022 18:32
@angelikatyborska
Copy link
Member Author

angelikatyborska commented Apr 4, 2022

Something vague is broken with Elixir 1.9 + OTP 20 according to CI:

  1) test start_link/1 min and max numbers get validated (TakeANumberDeluxeTest)
     test/take_a_number_deluxe_test.exs:24
     ** (EXIT from #PID<0.122.0>) :invalid_configuration

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.

@angelikatyborska
Copy link
Member Author

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, start_link sends an exit signal in OTP 20 🤷 that shouldn't happen as far as I understand the docs.

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.

Copy link
Contributor

@jiegillet jiegillet left a 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.

@angelikatyborska
Copy link
Member Author

@jiegillet Big thanks for the two huge reviews 🙏 I am so happy we will finally fill the GenServer shaped hole in the syllabus...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
x:module/concept-exercise Work on Concept Exercises x:size/large Large amount of work x:type/content Work on content (e.g. exercises, concepts)
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Implement new Concept Exercise: genserver
3 participants