philomena/test/philomena_job/semaphore_test.exs

144 lines
3.8 KiB
Elixir
Raw Normal View History

2024-08-04 07:10:29 +02:00
defmodule PhilomenaJob.SemaphoreTest do
use ExUnit.Case, async: true
alias PhilomenaJob.Semaphore.Server, as: SemaphoreServer
@max_concurrency 8
describe "Server functionality" do
setup do
{:ok, pid} = SemaphoreServer.start_link(max_concurrency: @max_concurrency)
on_exit(fn -> Process.exit(pid, :kill) end)
%{pid: pid}
end
test "allows max_concurrency processes to acquire", %{pid: pid} do
# Check acquire
results =
(1..@max_concurrency)
|> Task.async_stream(fn _ -> SemaphoreServer.acquire(pid) end)
|> Enum.map(fn {:ok, res} -> res end)
assert true == Enum.all?(results)
# Check linking to process exit
# If this hangs, linking to process exit does not release the semaphore
results =
(1..@max_concurrency)
|> Task.async_stream(fn _ -> SemaphoreServer.acquire(pid) end)
|> Enum.map(fn {:ok, res} -> res end)
assert true == Enum.all?(results)
end
test "does not allow max_concurrency + 1 processes to acquire (exit)", %{pid: pid} do
processes =
(1..@max_concurrency)
|> Enum.map(fn _ -> acquire_and_wait_for_release(pid) end)
# This task should not be able to acquire
task = Task.async(fn -> SemaphoreServer.acquire(pid) end)
assert nil == Task.yield(task, 10)
# Terminate processes holding the semaphore
Enum.each(processes, &Process.exit(&1, :kill))
# Now the task should be able to acquire
assert {:ok, :ok} == Task.yield(task, 10)
end
test "does not allow max_concurrency + 1 processes to acquire (release)", %{pid: pid} do
processes =
(1..@max_concurrency)
|> Enum.map(fn _ -> acquire_and_wait_for_release(pid) end)
# This task should not be able to acquire
task = Task.async(fn -> SemaphoreServer.acquire(pid) end)
assert nil == Task.yield(task, 10)
# Release processes holding the semaphore
Enum.each(processes, &send(&1, :release))
# Now the task should be able to acquire
assert {:ok, :ok} == Task.yield(task, 10)
end
test "does not allow max_concurrency + 1 processes to acquire (run)", %{pid: pid} do
processes =
(1..@max_concurrency)
|> Enum.map(fn _ -> run_and_wait_for_release(pid) end)
# This task should not be able to acquire
task = Task.async(fn -> SemaphoreServer.acquire(pid) end)
assert nil == Task.yield(task, 10)
# Release processes holding the semaphore
Enum.each(processes, &send(&1, :release))
# Now the task should be able to acquire
assert {:ok, :ok} == Task.yield(task, 10)
end
test "does not allow re-acquire from the same process", %{pid: pid} do
acquire = fn ->
try do
{:ok, SemaphoreServer.acquire(pid)}
rescue
err -> {:error, err}
end
end
task = Task.async(fn ->
acquire.()
acquire.()
end)
assert {:ok, {:error, %MatchError{}}} = Task.yield(task)
end
test "allows re-release from the same process", %{pid: pid} do
release = fn ->
try do
{:ok, SemaphoreServer.release(pid)}
rescue
err -> {:error, err}
end
end
task = Task.async(fn ->
release.()
release.()
end)
assert {:ok, {:ok, :ok}} = Task.yield(task)
end
end
defp run_and_wait_for_release(pid) do
spawn(fn ->
SemaphoreServer.run(pid, fn ->
wait_for_release()
end)
end)
end
defp acquire_and_wait_for_release(pid) do
spawn(fn ->
SemaphoreServer.acquire(pid)
wait_for_release()
SemaphoreServer.release(pid)
end)
end
defp wait_for_release do
receive do
:release ->
:ok
_ ->
wait_for_release()
end
end
end