A. Job Execution & Promise Contract
| # |
Scenario |
Expected Observation |
| A1 |
Successful handler – _perform() resolves, no explicit cancel, retry, or reject. |
channel.ack is sent once; message disappears from queue; promise resolves with handler result. |
| A2 |
Handler throws synchronous error. |
Job rejects promise; reject() path is followed (tests in section C). |
| A3 |
Handler returns rejected promise. |
Same behaviour as A-2. |
| A4 |
Handler hangs – exceed a reasonable timeout (e.g. 5 s) to confirm nothing is auto-acked. |
No ack or reject; message remains unacked (visible via rabbitmqctl list_queues messages_unacknowledged). |
| A5 |
Duplicate resolve – handler resolves, then calls resolve again (hack a stub). |
Only first resolve honoured; no double ack; second resolve ignored (check debug log). |
B. Explicit Cancel Flow
| # |
Scenario |
Expected Observation |
| B1 |
Call job.cancel() before _perform() finishes. |
cancel event emitted, promise rejects with PeanarJobCancelledError; no ack sent. |
| B2 |
Call cancel() after _perform() already resolved. |
Second “late” cancel is ignored; exactly one ack. |
C. Reject / Retry Handling
(All require def.retry_exchange to be declared beforehand.)
| # |
Scenario |
Expected Observation |
| C1 |
Retry eligible – handler throws; attempt=1, max_retries=3. |
Job declares retry queue, channel.reject(requeue=false) sent; message re-emerges on original queue after retry_delay; attempt incremented in payload. |
| C2 |
Max retries reached – trigger on final allowed attempt. |
Job publishes to error_exchange; original delivery is acked; message appears once in error queue/exchange and never again in main queue. |
| C3 |
Retry flag off – handler throws an error with retry=false. |
Message sent directly to error_exchange (or discarded if none). |
| C4 |
Unlimited retries – set max_retries=-1; cause 3 consecutive failures. |
Message retries indefinitely (at least three times within test window). |
| C5 |
Retry exchange missing – omit def.retry_exchange but throw error. |
Job throws PeanarInternalError; consumer channel emits exception; message stays unacked (verify). |
D. Ack / Reject Semantics & Delivery-Tag Accuracy
| # |
Scenario |
Expected Observation |
| D1 |
Large delivery tag – publish > 2^53 messages then process one; ensure tag > JS safe integer. |
ack fails (precision loss) → expect “unknown delivery tag” from broker. Confirms BigInt→Number truncation bug. |
| D2 |
Manual reject path – inside handler call job.reject() explicitly. |
See channel.reject; message routed per retry/error logic. |
| D3 |
Reject when channel lost – close channel before reject. |
PeanarAdapterError thrown; message redelivers on new channel. |
E. Error-Exchange / Dead-Letter Routing
| # |
Scenario |
Expected Observation |
| E1 |
Job final-fails with def.error_exchange set to fanout exchange. |
One new message arrives in bound error queue with JSON body { id, name, error, args }. |
| E2 |
Same but with topic exchange and custom routingKey. |
Message uses requested routing key. |
| E3 |
No error_exchange configured; retries exhausted. |
Job issues channel.reject(requeue=false); message is discarded (queue’s messages_ready and messages_unacknowledged back to 0). |
F. Queue Pause / Resume Delegation
| # |
Scenario |
Expected Observation |
| F1 |
Call pauseQueue() during steady consumption. |
Broker stops delivering from queue; rabbitmqctl list_consumers shows consumer flow-state = blocked. |
| F2 |
Call resumeQueue() and verify delivery resumes. |
New messages begin flowing; backlog drains. |
G. Retry Queue Declaration Side-Effects
| # |
Scenario |
Expected Observation |
| G1 |
Trigger two retries in rapid succession; ensure _declareRetryQueues runs twice. |
Second call succeeds but broker logs “queue already exists”; verify idempotence (no channel-error 406). |
| G2 |
Change retry_delay between attempts (e.g. 1 s then 5 s) and retry. |
Expect broker precondition error because queue arguments mismatch, illustrating why declaration per job is risky. |
H. Channel Failure / Redelivery Edge Cases
| # |
Scenario |
Expected Observation |
| H1 |
Mid-flight channel close – close channel after delivery but before job finishes. |
Handler finishes; when ack is attempted an exception is thrown; message redelivers via new channel; second processing succeeds. |
| H2 |
Connection drop – kill TCP connection; wait for reconnection logic; confirm job is re-delivered exactly once after reconnect. |
|
I. Subclass / Extension-Point Behaviour
| # |
Scenario |
Expected Observation |
| I1 |
Create subclass overriding _perform() and calling super._perform(). |
Both base and subclass logic run; final ack behaviour unchanged. |
| I2 |
Subclass overrides retry() to implement custom back-off; ensure original retry queue still used. |
Custom delay honoured; retries proceed. |
Coverage checklist
These scenarios exercise:
- Handler success & failure paths
ack, reject, cancel intent & side-effects
- Retry-queue declaration and routing
- Error-exchange publishing
- Delivery-tag precision & channel coupling
- Channel/connection failure resilience
- Public helper APIs (
pauseQueue, resumeQueue, retry, subclass hooks)
A. Job Execution & Promise Contract
_perform()resolves, no explicitcancel,retry, orreject.channel.ackis sent once; message disappears from queue; promise resolves with handler result.reject()path is followed (tests in section C).ackorreject; message remains unacked (visible viarabbitmqctl list_queues messages_unacknowledged).resolveagain (hack a stub).ack; second resolve ignored (check debug log).B. Explicit Cancel Flow
job.cancel()before_perform()finishes.cancelevent emitted, promise rejects withPeanarJobCancelledError; noacksent.cancel()after_perform()already resolved.ack.C. Reject / Retry Handling
(All require
def.retry_exchangeto be declared beforehand.)attempt=1,max_retries=3.channel.reject(requeue=false)sent; message re-emerges on original queue afterretry_delay;attemptincremented in payload.error_exchange; original delivery isacked; message appears once in error queue/exchange and never again in main queue.retry=false.error_exchange(or discarded if none).max_retries=-1; cause 3 consecutive failures.def.retry_exchangebut throw error.PeanarInternalError; consumer channel emits exception; message stays unacked (verify).D. Ack / Reject Semantics & Delivery-Tag Accuracy
ackfails (precision loss) → expect “unknown delivery tag” from broker.Confirms BigInt→Number truncation bug.
job.reject()explicitly.channel.reject; message routed per retry/error logic.reject.PeanarAdapterErrorthrown; message redelivers on new channel.E. Error-Exchange / Dead-Letter Routing
def.error_exchangeset to fanout exchange.{ id, name, error, args }.routingKey.channel.reject(requeue=false); message is discarded (queue’smessages_readyandmessages_unacknowledgedback to 0).F. Queue Pause / Resume Delegation
pauseQueue()during steady consumption.rabbitmqctl list_consumersshows consumer flow-state = blocked.resumeQueue()and verify delivery resumes.G. Retry Queue Declaration Side-Effects
_declareRetryQueuesruns twice.retry_delaybetween attempts (e.g. 1 s then 5 s) and retry.H. Channel Failure / Redelivery Edge Cases
ackis attempted an exception is thrown; message redelivers via new channel; second processing succeeds.I. Subclass / Extension-Point Behaviour
_perform()and callingsuper._perform().retry()to implement custom back-off; ensure original retry queue still used.Coverage checklist
These scenarios exercise:
ack,reject,cancelintent & side-effectspauseQueue,resumeQueue,retry, subclass hooks)