Перейти к основному содержимому
Нормативная версия документации - английская

Эта страница переведена с английского языковой моделью. При любом расхождении верен английский оригинал.

Переобработка и DLQ

Это руководство обрабатывает сообщение SQS заново сразу или позже, ограничивает число его доставок и поручает SQS переносить в DLQ то, что раз за разом не обрабатывается.

Обе задачи SQS решает сам. Переобработка - это то же сообщение, снова сделанное видимым, с сохранённым счётчиком получений. Предел и DLQ - это политика перенаправления очереди, и объявляет её точка монтирования.

Пример - сервис счетов, который списывает оплату счетов с клиентов.

Переобработка сейчас и позже​

src/main.rs
/// The largest invoice charged without a credit check.
const CREDIT_LIMIT_CENTS: u64 = 100_000;

/// How long an invoice waits for the credit check before it comes back.
const CREDIT_CHECK: Duration = Duration::from_secs(30);

#[derive(Debug, PartialEq, Serialize, Deserialize, Outgoing)]
#[outgoing(name = "invoices")]
struct Invoice {
id: u64,
customer: String,
amount_cents: u64,
}

#[subscriber(SqsQueue::new("invoices").create_if_missing())]
async fn charge(invoice: &Invoice) -> HandlerOutcome {
if invoice.customer.is_empty() {
// Another try gets the same answer.
tracing::warn!(invoice.id, "invoice without a customer dropped");
return HandlerOutcome::drop();
}
if invoice.amount_cents > CREDIT_LIMIT_CENTS {
tracing::info!(invoice.id, "invoice waits for the credit check");
return HandlerOutcome::retry_after(CREDIT_CHECK);
}
tracing::info!(invoice.id, invoice.amount_cents, "invoice charged");
HandlerOutcome::ack()
}

Счёт выше кредитного лимита ждёт проверки кредита. retry_after задаёт тайм-аут видимости сообщения 30 секунд, и по их истечении SQS выдаёт его повторно. SQS считает целые секунды, поэтому доля секунды округляется вверх. SQS скрывает сообщение не дольше двенадцати часов с момента получения, поэтому задержка заканчивается в их пределах. Более долгая задержка сокращается до конца этих двенадцати часов, и крейт пишет в журнал предупреждение с именем очереди. Всё это время сообщение остаётся в своей очереди, поэтому отложенная переобработка переживает перезапуск сервиса.

retry задаёт нулевую видимость, и сообщение сразу выдаётся повторно. Счёт без клиента при каждой попытке получает один и тот же ответ, поэтому charge его отбрасывает.

Предел и DLQ​

src/main.rs
#[ruststream::app]
fn app() -> impl App {
RustStream::new(AppInfo::new("invoices", "0.1.0")).with_broker(
SqsBroker::new()
.endpoint("http://localhost:4566")
.region("us-east-1")
.test_credentials(),
|b| {
// Four deliveries of one invoice, the first included; then SQS moves it.
b.include(charge)
.max_attempts(nonzero!(4u32))
.dead_letter("invoices-dead");
b.include(review);
},
)
}

max_attempts и dead_letter вместе образуют политику перенаправления очереди. Предел становится её maxReceiveCount, а DLQ - её целью. Подписка записывает политику в очередь при открытии. С этого момента SQS сам считает получения и переносит сообщение, у которого они кончились; сервис ничего не публикует. Здесь счёт доставляется четыре раза, а на пятом получении SQS переносит его в invoices-dead.

Два шага задаются вместе. Предел без DLQ или DLQ без предела останавливают сервис при старте с ошибкой SqsError::IncompleteRedrive, которая называет недостающую половину. SQS принимает maxReceiveCount не больше 1000.

DLQ должна существовать, когда открывается подписка, если только её не создаёт дескриптор приложения с create_if_missing. Она должна быть того же вида, что и её очередь: очереди FIFO нужна DLQ типа FIFO. Для записи политики нужны sqs:SetQueueAttributes на очереди и sqs:GetQueueAttributes на DLQ.

drop удаляет сообщение. На очереди с пределом drop доставки, которая израсходовала последнее получение, вместо удаления возвращает сообщение, и SQS переносит его в DLQ, а не теряет.

Чтение DLQ​

src/main.rs
/// Reads the invoices SQS moved away after their last delivery.
#[subscriber(SqsQueue::new("invoices-dead").create_if_missing())]
async fn review(invoice: &Invoice) -> HandlerOutcome {
tracing::error!(invoice.id, invoice.customer, "invoice needs a review");
HandlerOutcome::ack()
}

DLQ - обычная очередь, и review читает её, как любую другую. Сообщение приходит таким, каким его отправили, со своими атрибутами. Очередь без читателя хранит сообщения до конца срока хранения, поэтому человек может посмотреть их и в консоли.

Тестирование задержки​

Тесты в файле примера запускают приложение внутри процесса на остановленных часах, поэтому tb.advance(CREDIT_CHECK) пропускает 30 секунд мгновенно. Один тест проводит счёт через четыре доставки в invoices-dead, другой проверяет, что предел без DLQ не запускается. Обвязку разбирает руководство Тестирование сервиса на SQS.