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

Add test for race condition in community gardens #1368

Conversation

cshintov
Copy link
Contributor

If the register function uses get and update instead of get_and_update concurrent operations results in non-unique ids.

@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)?

Automated comment created by PR Commenter 🤖.

If the `register` function uses `get` and `update` instead of
`get_and_update` concurrent operations results in non-unique ids.
@cshintov cshintov force-pushed the add-test-for-race-condition-in-community-gardens branch from 919f70b to 33d8052 Compare September 17, 2023 13:33
Copy link
Member

@angelikatyborska angelikatyborska left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi! Thank you for bringing this problem to my attention 🙏

I am unable to write a solution that would fail the new test on my machine. I tried this:

  def register(pid, register_to) do
    state = Agent.get(pid, fn state -> state end)
    %{registry: registry, next_id: next_id} = state
    new_plot = %Plot{plot_id: next_id, registered_to: register_to}
    updated = Map.put(registry, next_id, new_plot)
    state = %{state | registry: updated, next_id: next_id + 1}
    Agent.update(pid, fn x -> state end)

    new_plot
  end

I suspect that due to the nature of the problem (concurrency and being unlucky with timing), it might be impossible to write a test that would fail reliably if Agent.get_and_update was not used.

Maybe it would be a better idea to add an analyzer check that ensures Agent.get_and_update is called from CommunityGarden.register?

Some links:

@@ -102,4 +102,24 @@ defmodule CommunityGardenTest do
assert {:ok, pid} = CommunityGarden.start()
assert {:not_found, "plot is unregistered"} = CommunityGarden.get_registration(pid, 1)
end

@tag task_id: 6
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Task nr 6 does not exist. I believe you wanted to add this test to task 3 instead.

Comment on lines 110 to 115
# Spawn 25 processes that register a plot concurrently
for _ <- 1..25 do
Task.async(fn -> CommunityGarden.register(pid, "user") end)
end
|> Enum.map(&Task.await/1)
|> Enum.map(& &1.plot_id)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In learning exercises, we try to avoid using not-yet-learned concepts anywhere in the exercise, including tests. At this point in the syllabus, tasks weren't learned yet. Could this test be rewritten to use only concurrency basics like spawn, send, and receive?

CONTRIBUTING.md Outdated Show resolved Hide resolved
@cshintov
Copy link
Contributor Author

cshintov commented Oct 15, 2023

Hi! Thank you for bringing this problem to my attention 🙏

I am unable to write a solution that would fail the new test on my machine. I tried this:

  def register(pid, register_to) do
    state = Agent.get(pid, fn state -> state end)
    %{registry: registry, next_id: next_id} = state
    new_plot = %Plot{plot_id: next_id, registered_to: register_to}
    updated = Map.put(registry, next_id, new_plot)
    state = %{state | registry: updated, next_id: next_id + 1}
    Agent.update(pid, fn x -> state end)

    new_plot
  end

I suspect that due to the nature of the problem (concurrency and being unlucky with timing), it might be impossible to write a test that would fail reliably if Agent.get_and_update was not used.

Actually I misunderstood the cause of the problem.

I tried your solution and found no problem with get and update as you said.

So I tried my solution again and it was giving me non-unique ids. The problem was not get and update, it was how I used them.

  def register(pid, register_to) do
    new_id = Agent.get(pid, & &1.id) + 1

    new_plot = %Plot{plot_id: new_id, registered_to: register_to}
    Agent.update(pid, &%{id: new_id, plots: [new_plot | &1.plots]})

    new_plot
  end

I first extract the current latest id, construct new plot with incremented id and then finally makes the update. This causes concurrent register to give non-unique ids.

Gives error!
non-unique-ids

No errors!
no-error-when-excluded

You can find my whole solution here

@angelikatyborska
Copy link
Member

Sorry for the late reply!

I can see now that your wrong solution (https://github.com/cshintov/exercism/blob/master/elixir/community-garden/lib/community_garden.ex) fails this new test reliably. So I decided to add the new test that you're proposing, but I modified it to incorporate the feedback that I had about it.

@jiegillet Could you take a look if I didn't make any silly mistakes?

receive do
{^n, plot} -> plot.plot_id
after
100 -> nil
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

20 * 100ms = 2s, so even is somebody messes up their solution so bad they trigger the timeout every time, they should still see some output of the test. I think the test runner only gets killed by exercism after 3 * average_run_time (which is 3 * 4s in our case)

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.

I didn't dive deep into this one, but the added test makes sense to me and looks sensible

@angelikatyborska angelikatyborska merged commit 4dc3f9b into exercism:main Nov 23, 2023
11 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants