From b7ff2c84666abc6b5de00dcf23c2dd0263bb4f37 Mon Sep 17 00:00:00 2001 From: Afri <220195275+l5yth@users.noreply.github.com> Date: Fri, 16 Oct 2026 09:07:27 +0200 Subject: [PATCH] web,data: timeout specs hold their jobs on gates, not fixed sleeps (#1050) --- tests/test_daemon_unit.py | 12 +++++++++--- web/spec/federation_crawl_limits_spec.rb | 24 ++++++++++++++---------- web/spec/worker_pool_spec.rb | 24 +++++++++++++++++++----- 3 files changed, 42 insertions(+), 18 deletions(-) diff --git a/tests/test_daemon_unit.py b/tests/test_daemon_unit.py index 514caa82..e333f475 100644 --- a/tests/test_daemon_unit.py +++ b/tests/test_daemon_unit.py @@ -126,10 +126,16 @@ def test_close_interface_respects_timeout(monkeypatch): class SlowInterface: def close(self): - blocker.wait(timeout=0.1) + # Held until the test releases it, so the close still runs however + # late the 0.01 s join returns. After 5 s it ends anyway, so a + # _close_interface that waits for the close fails instead of hanging. + blocker.wait(timeout=5.0) - daemon._close_interface(SlowInterface()) - assert any("timeout_seconds" in entry for entry in log_calls) + try: + daemon._close_interface(SlowInterface()) + assert any("timeout_seconds" in entry for entry in log_calls) + finally: + blocker.set() def test_close_interface_immediate_path(monkeypatch): diff --git a/web/spec/federation_crawl_limits_spec.rb b/web/spec/federation_crawl_limits_spec.rb index fc8bdfcb..1327f838 100644 --- a/web/spec/federation_crawl_limits_spec.rb +++ b/web/spec/federation_crawl_limits_spec.rb @@ -403,26 +403,30 @@ RSpec.describe "Federation crawl limits" do it "lets the worker pool's task timeout end a crawl" do fed.lists[hostile] = %w[p1 p2 p3 p4].map { |name| peers.record("#{name}.fed.invalid") } + # The first request holds until the pool's task timeout interrupts it, + # however late that fires, so the crawl cannot go on by itself. After 5 s + # it ends anyway, so a pool and a wait that both ignore their timeouts + # fail, not hang. + gate = Queue.new first = true fed.hooks << lambda do |_entry| if first first = false - sleep 0.6 + gate.pop(timeout: 5) end end worker_pool = PotatoMesh::App::WorkerPool.new(size: 1, max_queue: 2, task_timeout: 0.3, name: "spec-fl6") task = worker_pool.schedule { crawl(hostile) } - outcome = begin - task.wait(timeout: 5) - :completed - rescue PotatoMesh::App::WorkerPool::TaskTimeoutError - :timed_out - ensure - worker_pool.shutdown(timeout: 1, force_kill_after: 1) - end + begin + # The pool's message: the wait's own deadline raises "task deadline + # exceeded", which is no task timeout ending the crawl (SPEC FL6). + expect { task.wait(timeout: 3) }.to raise_error(PotatoMesh::App::WorkerPool::TaskTimeoutError, "task exceeded timeout") + ensure + gate.close + worker_pool.shutdown(timeout: 1, force_kill_after: 1) + end - expect(outcome).to eq(:timed_out) expect(fed.total).to eq(1) end diff --git a/web/spec/worker_pool_spec.rb b/web/spec/worker_pool_spec.rb index ef0b5af7..2490ae0e 100644 --- a/web/spec/worker_pool_spec.rb +++ b/web/spec/worker_pool_spec.rb @@ -39,10 +39,17 @@ RSpec.describe PotatoMesh::App::WorkerPool do end it "fails tasks that exceed the configured timeout" do + gate = Queue.new with_pool(task_timeout: 0.01) do |pool| - task = pool.schedule { sleep 0.05; :late } - expect { task.wait(timeout: 1) }.to raise_error(described_class::TaskTimeoutError) + # The job holds until the pool's task timeout interrupts it, however + # late that fires. The message is the pool's: the wait's own deadline + # raises "task deadline exceeded". After 5 s the job ends anyway, so a + # pool and a wait that both ignore their timeouts fail, not hang. + task = pool.schedule { gate.pop(timeout: 5); :late } + expect { task.wait(timeout: 3) }.to raise_error(described_class::TaskTimeoutError, "task exceeded timeout") end + ensure + gate.close end it "ignores invalid timeout values" do @@ -130,13 +137,20 @@ RSpec.describe PotatoMesh::App::WorkerPool do describe PotatoMesh::App::WorkerPool::Task do it "raises a timeout when the job exceeds the provided deadline" do + gate = Queue.new with_pool do |pool| - task = pool.schedule { sleep 0.1; :done } + # The job holds until the gate opens, so the first wait can only end + # at its own deadline, however late it wakes. After 5 s the job ends + # anyway, so a wait that ignores its deadline fails instead of hanging. + task = pool.schedule { gate.pop(timeout: 5); :done } expect do task.wait(timeout: 0.01) - end.to raise_error(PotatoMesh::App::WorkerPool::TaskTimeoutError) - expect(task.wait(timeout: 1)).to eq(:done) + end.to raise_error(PotatoMesh::App::WorkerPool::TaskTimeoutError, "task deadline exceeded") + gate << :open + expect(task.wait(timeout: 5)).to eq(:done) end + ensure + gate.close end it "reports completion status" do