XRAPH/Writing/A queue does not make the work go away

A queue does not make the work go away

Putting slow work on a queue moves the latency somewhere the user cannot see it, and adds ordering, poison messages and a second failure domain. Notes on the trade.

Published
Jul 2022
Length
2 min read
Systems
2

#The obvious win

A request does something slow. Send the slow part to a queue, return immediately, and the user gets a fast response. This is correct and it is why queues are everywhere.

What it does not do is reduce the work. The same operation still runs, and now it runs somewhere with different failure characteristics and no user waiting to notice that it did not.

#What you take on

Ordering. Most queues guarantee order per partition, not globally . If two operations on the same entity land in different partitions, they may be processed out of order, and the entity ends up in a state neither operation intended. Partitioning by entity identifier fixes it and limits your parallelism to the number of distinct entities.

Poison messages. One message fails every time it is processed. Without a limit it retries forever, and it will block its partition while doing so. A dead letter queue is not a nice extra, it is the thing that stops one bad message from halting a partition.

A second failure domain. The queue can be full, slow, or unavailable, and it can be any of those while your service and your database are healthy. That is a new way for the system to fail and it needs its own alerting.

Visibility. A user who submitted work has no idea whether it succeeded. Either you build status reporting or you accept support tickets asking whether it worked.

#The failure everybody hits

The write commits, and the enqueue fails. Now the database says an order exists and no worker will ever process it.

Doing it in the other order is worse: enqueue first, and a worker can pick up the message before the transaction commits, then fail to find the row it was told about.

The answer is to write the message to the same database in the same transaction and have a separate process relay it to the queue. Two things commit together or neither does, and the relay is at least once, which the consumer handles by being idempotent . This is the outbox pattern, and it is the least clever and most reliable option available .

#When not to reach for one

If the work takes fifty milliseconds, a queue makes it slower and harder to debug. If the user needs the result before the next screen, an asynchronous answer means building a polling or push mechanism to hand it back, which is more machinery than doing the work.

Backpressure is often the better instinct. If work is arriving faster than it can be processed, a queue absorbs the difference until it cannot, and a queue absorbing a permanent imbalance is a slow outage rather than a fix .

References

  1. [1]Jay Kreps, Neha Narkhede, Jun Rao, Kafka: A Distributed Messaging System for Log Processing, NetDB Workshop, 2011
  2. [2]Michael T. Nygard, Release It! Design and Deploy Production-Ready Software, Pragmatic Bookshelf, 2nd edition, 2018
  3. [3]Martin Kleppmann, Designing Data-Intensive Applications, O'Reilly Media, 2017
  4. [4]Pat Helland, Life beyond Distributed Transactions: An Apostate's Opinion, Conference on Innovative Data Systems Research (CIDR), 2007