Fix unique-job lock leak in inline testing mode - #173
Conversation
The provisional/final lock split added in 0.15.0 assumes the block passed to `lock_for_scheduling!` only enqueues the job. Under inline testing mode (`Cloudtasker::Testing.inline!`) the block also executes the job synchronously, which releases the job's lock via the execute middleware. Step 3 then re-acquires a "final" lock that nothing will ever release, so a subsequent enqueue of the same unique job is wrongly rejected for the full lock TTL. Skip the final-lock step under inline execution: the job has already run and released its lock by the time scheduling returns. The async (production) path is unchanged. Adds an integration spec (two successive inline enqueues of the same unique job both run) and a unit spec (the lock is not promoted to the final TTL in inline mode).
|
Hey @lovitt, Thanks for the PR ❤️ I need to do some testing/investigations on my side. I bet this issue also impacts the new To avoid future extension issues with |
inline! is meant to run an enqueued job as the real server would. These specs pin three properties that hold when inline execution happens at the universal backend point (Backend::MemoryTask.create), through which every enqueue path funnels: - Execution coverage: perform_async, perform_at/perform_in, the instance #schedule method, and batch children (enqueued via #schedule) all run. - Error hooks: a raising job triggers the on_error hook, like the server path does via WorkerHandler.with_worker_handling. - Context fidelity: the worker runs with a task_id, as assigned by the backend and read by app logging/instrumentation. Only perform_async had integration coverage; the batch spec drains a fake! queue manually rather than running inline!. Pinning these guards against a refactor that relocates inline execution onto a narrower path.
|
Thanks for looking into this @alachaum. I ran the #176 branch against our app's test suite. The unique-job lock leak is fixed, but a batch of tests that pass on 0.15.0 fail. The failures are related to tests that exercise scheduled jobs, batches, and error hooks and worker context provided by I pushed some regression specs in 6778d9a which pass on #173 but fail on #176. |
|
Hey @lovitt, thanks a lot for the tests; this was really useful 🙏 I have incorporated these tests in my PR and added a new commit that addresses these issues. My original work was a bit too focused on I have reworked things to:
Your test suite should now be green, but don't hesitate to let me know if you still see red tests. Note: I'm trying to define a single interface |
|
@alachaum looks good! Our full test suite passes now on this revision. |
Problem
In inline mode (
Cloudtasker::Testing.inline!), a unique job runs only the first time it's enqueued — later enqueues of the same job are silently rejected.lock_for_scheduling!(added in 0.15.0) takes a provisional lock, yields to enqueue the job, then takes the final lock. That final lock is meant to be released later, when the job runs — the execute middleware unlocks it.But in inline mode the job runs during the yield, so it unlocks before the final lock is even taken. The final lock is then set after the job has already finished, leaving it with no future run to release it. It lingers for the full
lock_ttland rejects later enqueues of the same job (on_conflict: :reject). 0.14.0 did a plainlock!; yield, so this is a regression.Fix
Skip the final-lock step in inline mode, where the job has already run and released its lock. The async/production path is unchanged:
inline_mode?is guarded bydefined?(Cloudtasker::Testing), which is false in production (same idiom asCloudTask.backendandBackend::MemoryTask.inline_mode?).Tests
Integration spec (both inline enqueues run) and unit spec (lock not promoted to the final TTL).