public async Task Can_perform_ack_nack_pattern() { var random = new Random(); const int expected = 100; var actual = 0; // counting finalizations var retryPolicy = new RetryPolicy(); retryPolicy.Default(RetryDecision.Backlog); // use external requeuing retryPolicy.After(3, RetryDecision.Undeliverable); // // ACK/NACK: // ========= // 1. The main thread pushes messages, while two consumer producers pull messages. // 2. If the message fails (50% probability), it is returned to the push thread. // The number of attempts are retained when the message is relayed. This way // the retry policy is enforced across all consumers, provided each consumer // implements the same policy. var producer = new BackgroundThreadProducer <BaseEvent> { MaxDegreeOfParallelism = 1 }; var consumer1 = new BackgroundThreadProducer <BaseEvent> { MaxDegreeOfParallelism = 1 }; var consumer2 = new BackgroundThreadProducer <BaseEvent> { MaxDegreeOfParallelism = 1 }; // // Consumer 1: consumer1.AttachUndeliverable(x => { _console.WriteLine("[consumer2] message " + x.Message.Id + " abandoned after " + x.Attempts + " attempt(s)"); Interlocked.Increment(ref actual); // undelivered }); consumer1.AttachBacklog(async x => { _console.WriteLine("[consumer1] message " + x.Message.Id + " requeued"); await SendToRandomConsumer(x); // passes context data to other consumers }); consumer1.Attach(new ActionConsumer <BaseEvent>(x => { if (NextBool(random)) { throw new Exception(); } Interlocked.Increment(ref actual); // sent })); consumer1.RetryPolicy = retryPolicy; await consumer1.Start(); // // Consumer 2: consumer2.AttachUndeliverable(x => { _console.WriteLine("[consumer2] message " + x.Message.Id + " abandoned after " + x.Attempts + " attempt(s)"); Interlocked.Increment(ref actual); // undelivered }); consumer2.AttachBacklog(async x => { _console.WriteLine("[consumer2] message " + x.Message.Id + " requeued"); await SendToRandomConsumer(x); // passes context data to other consumers }); consumer2.Attach(new ActionConsumer <BaseEvent>(x => { if (NextBool(random)) { throw new Exception(); } Interlocked.Increment(ref actual); // sent })); consumer2.RetryPolicy = retryPolicy; await consumer2.Start(); async Task SendToRandomConsumer(QueuedMessage <BaseEvent> x) { if (NextBool(random)) { _console.WriteLine($"message {x.Message.Id} sent to consumer1 ({x.Attempts} attempt(s))"); await consumer1.Produce(x); } else { _console.WriteLine($"message {x.Message.Id} sent to consumer2 ({x.Attempts} attempt(s))"); await consumer2.Produce(x); } } // // Producer: for (var i = 0; i < expected; i++) { await producer.Produce(new BaseEvent { Id = i }); } producer.Attach(async x => { await SendToRandomConsumer(x); }); await producer.Start(); // start with a full buffer while (actual != expected) // wait for all finalizations { await Task.Delay(10); } }