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);
            }
        }