Problem: nqmgr's fairness breaks down for single-recipient bulk mail Solution: schedule a delivery request depending on how many other pending requests that sender already has in the scheduler's in-memory queues. This metric does not depend on whether bulk mail is sent as multi-recipient or single-recipient mail.
Alternative 1: modify nqmgr's job/peer grouping Nqmgr ensures a form of fairness, by interleaving multi-recipient mail deliveries with single-recipient mail. This relies on a clever way of grouping things in jobs and peers, but fails to enforce fairness down when bulk mail is sent as single-recipient mail. To achieve fairness regardless of whether bulk mail is sent as single-recipent mail, one would adapt nqmgr's scheduler, such that the job and peer structures are populated based on the number of pending delivery requests from that sender. This would be a non-trivial change to a non-trivial scheduler. Alternative 2: multiple mostly independent scheduling classes This alternative blows new life into the old queue manager. The idea is to introduce three classes: one class for small senders (senders that have a small number of delivery requests in the scheduler's in-memory queues), one class for medium senders, and one class for large senders. In the updated scheduler, each (transport, per-destination FIFO queue) becomes (transport, class, per-destination FIFO queue). Thus, there can be three same-destination queues under the same transport, instead of just one. And everything else just follows from that. As usual, the scheduler reads envelope information from a queue file, and creates a delivery request with a limited number of recipients that have the same next-hop destination domain. The updated scheduler then computes the class for the delivery request, based on the number of delivery requests that that sender already has in the scheduler's in-memory queues (something like int(10log(n)) would do). The scheduler then assigns the delivery request to the FIFO queue for that transport, class, and destination. During delivery, the scheduler round-robin selects a transport, round-robin selects a non-empty class, round-robin selects a destination queue within that class, and then selects the first delivery request within that FIFO queue. Additionally, we arrange that same-destination queues under a transport will have shared info for concurrency, throttling, and rate delay. The implementation effort is limited: during delivery request creation, also compute the class; during delivery, an additional round-robin selection stage to select a non-empty class; and sharing the concurrency and rate-delay info among same-destination queues under the same transport. We keep all the logic that old queue manager already has; data-wise, we just insert one extra level of indirection. Main benefits: within a class, all deliveries to the same destination are still made in FIFO order; there is no starvation as each class is guaranteed at least 1/3 of delivery resources, or all resources if the other classes are empty; and finally, the scheduler remains brain-dead simple: going from one to three FIFO queues is not a fundamental change. Other benefits: we can choose the class based on other properties, for example message size, or both size and number of pending delivery requests per sender, or something else. It seems unlikely that we will need more than three classes: small, medium, large. Wietse