All notable changes to com.babelqueue:babelqueue-sqs are documented here.
The format is based on Keep a Changelog, and
this project adheres to Semantic Versioning.
The envelope wire format is versioned separately by meta.schema_version
(currently 1) — see the contract at babelqueue.com.
- The poison DLQ block is built by
babelqueue-core1.14.0; no wire change. Requiresbabelqueue-core1.14.0 (was 1.13.0).
MINOR (ADR-0037): the consumer gains an opt-in BabelQueue DLQ and settles poison bodies and version-gated
deliveries on their first delivery. The envelope is unchanged (schema_version stays 1).
SqsConsumer.Builder.deadLetterQueueUrl(url)(opt-in, like the .NETDeadLetterQueueUrland the NodedeadLetterQueueUrl): the<queue>-dlqqueue (a FIFO one is<queue>-dlq.fifo; the consumer sends it the queue name as message group and the SQS message id as deduplication id). "Dead-lettering on" means this URL is set. The queue must exist.- With a DLQ queue set, the
dead_letterunknown-URN strategy now writes the envelope, annotated with adead_letterblock (reasonunknown_urn), to it and deletes the message only after the send succeeded. Without one it still degrades todelete.
- Poison body (not JSON, not an envelope, no valid identity, or an unsupported
meta.schema_version): never handed to a handler and never retried. With a DLQ queue it is sent as the ADR-0035 record (PoisonRecord,reason: "poison",original_queue= this queue) and deleted only after the send succeeded; a failed send leaves it unsettled and is reported. Under an explicitdeletestrategy it is deleted. With no DLQ queue nothing changes: it is reported and left to the queue'sRedrivePolicy. - Version-gated delivery (
bq-schema-versionpresent, not blank, not1): settled on the first delivery, body never decoded, no handler or hook, no retry. With a DLQ queue the raw body, byte for byte (nodead_letterblock) is sent to it and the message deleted only after the send succeeded (a failed send leaves it unsettled, so the write is retried on redelivery); under an explicitdeletestrategy it is deleted (dropped); with no DLQ queue nothing changes (reported, not deleted, native redrive). - The poison and version-gate rules read only the explicitly configured
unknownUrnStrategy(failwhen none is set). AnonUnknownUrnhook, and thedeleteit implies for an unknown URN, no longer reach them: a hook-only consumer never drops a poison or version-gated delivery silently (owner decision, 2026-10-25: the poison route reads only the explicitly configured strategy, like the version gate). The unknown-URN path is unchanged. - Built against
babelqueue-core1.13.0 (PoisonRecord), unchanged. - Vendored the cross-SDK conformance suite v1.8.0 (
version_gate_sqs,poison_dlqrunners added) and moved the CI pin to it.
- The BabelQueue DLQ on SQS is
<queue>-dlq(FIFO<queue>-dlq.fifo), not<queue>.dlq: an SQS queue name cannot contain a dot (ADR-0037 decision 7). README and Javadoc only;deadLetterQueueUrlis an explicit URL, so no code change.
SqsConsumer.Buildernow exposes the shared retry-backoff parameters (ADR-0034 §9):retryBackoff(seconds),retryBackoffMultiplier(m),retryBackoffMax(seconds)andretryJitter("none" | "full")(the jitter name is trimmed and matched case-insensitively, like the coreRetryBackoff). The visibility timeout on a handler failure ismin(max, base * multiplier^(n-1))(n = receives so far, 1-based), optionallyuniform(0, d(n)), rounded up to a whole second and clamped to 0-43200.releaseDelaySeconds(int)stays as the alias of the base, so the defaults (multiplier 1, no cap, no jitter) keep the constant release delay. A zero base delay stays0even whenmultiplier^(n-1)overflows to infinity. Implemented locally in the consumer: this module now builds againstbabelqueue-core1.13.0.
- Vendored the cross-SDK conformance suite v1.6.0 (no SQS case changed) and pinned
fetch.shby SHA-256 (BQ_FETCH_SH_SHA256) in the CI workflow.
SqsConsumernow applies the §3.7 schema-version gate before decoding the body: when thebq-schema-versionmessage attribute is present and is not the supported version (1), the body is never decoded and the handler is never called; the message is reported viaonError(with anullenvelope) and is not deleted, so SQS redrives it to the DLQ. A missing or blank attribute (empty or ASCII-whitespace-only) keeps the existing decode path. Cross-SDK parity (GR-5): the value is compared exactly, with no trimming.onErrormay now receive anullenvelope for a schema-version gate rejection (the body is not decoded, so there is no envelope); callbacks that dereference the envelope should null-check it.
MINOR release: the failure path's behaviour and the public Builder API change.
- Release now follows broker-bindings.md §3 — behaviour change. A throwing handler's
message is always released with
ChangeMessageVisibility(ReceiptHandle, VisibilityTimeout = backoff)instead of being left to wait out the queue's full visibility timeout; it is still never deleted. The backoff is the newBuilder.releaseDelaySeconds(int), default0= redeliver now. Previously a failing message reappeared only after the visibility timeout (e.g. 30 s). SetreleaseDelaySeconds(N)to keep a delay between attempts. - Poison-loop risk. With the default
0, a message that fails permanently is redelivered on every receive, and a short outage can burn throughmaxReceiveCountin under a second. Configure aRedrivePolicyon the queue (deadLetterTargetArn+maxReceiveCount) so SQS moves such a message to a native DLQ, and pick a non-zeroreleaseDelaySecondsif the handler depends on something that can be briefly unavailable. - Out-of-range delays are clamped to 0–43200 s (the SQS 12 h cap), like the Go and Python transports; they no longer throw.
- Require
com.babelqueue:babelqueue-core 1.8.0(unknown-key preservation,Envelope.withAttempts).
- A failed
DeleteMessageafter a successful handler (throttling, network, stale receipt handle) is no longer treated as a handler failure: it is reported toonErroras the newSqsDeleteException(broker error as its cause) and the message is not released, so it is redelivered only after its visibility timeout instead of immediately. The same applies to thedelete/dead_letterunknown-URN strategies; a failed delete never escapespoll(). - A failed release (
ChangeMessageVisibilityrejected: stale receipt handle, throttling, network) no longer escapespoll()/run(). The handler error is reported toonErrorfirst, then the release error; the batch continues and SQS still redelivers the message once its visibility timeout expires. attemptsreconciliation now preserves unknown top-level /metakeys (core 1.8.0 extras), so a newer producer's additions reach the handler unchanged.
- Unknown-URN strategy (
Builder.unknownUrnStrategy(String),fail|delete|release|dead_letter, the coreUnknownUrnStrategynames).releasecallsChangeMessageVisibilitywithBuilder.unknownUrnReleaseDelaySeconds(int)(default0, matching the Python and Node transports; clamped to 0–43200);dead_letterdegrades todelete(this transport has no DLQ publisher — the contract's "DLQ disabled" rule). Default stays backward compatible:fail, ordeletewhen onlyonUnknownUrnis set.onUnknownUrnis now invoked before every strategy. SqsConsumer.MAX_VISIBILITY_TIMEOUT_SECONDS(43200)..github/dependabot.yml(Maven + GitHub Actions, weekly).
- OpenTelemetry
traceparenttransport wiring (ADR-0028, v0.2).SqsPublishergainspublishWithHeaders(Envelope, Map<String,String>)— the produce-side seam the optional corecom.babelqueue.otel.HeaderSenderwires to: out-of-band headers (e.g. a W3Ctraceparent) ride on the SQSMessageAttributeschannel beside the contractbq-*attributes (SqsAttributes.projectWithHeaders: the contract projection wins a key collision, merge in sorted order, bounded by the SQS 10-attribute cap), never inside the frozen envelope (GR-1). NewSqsHeaders.of(Message)surfaces a delivered message's attributes as aMap<String,String>, the consume-side seam forTracing.wrapHandler(tracer, handler, Supplier), so a carriedtraceparentmakes the consumer span a true child of the producer span. A header-less publish is byte-identical to before;trace_idis preserved (GR-4);schema_versionstays 1. No new runtime dependency — the header seam is a plainMap<String,String>; OpenTelemetry is needed only by callers who opt in.
- Require
com.babelqueue:babelqueue-core 1.5.0(the out-of-band header-carrier seam).
1.0.0 - 2026-06-12
- Initial release. An Amazon SQS transport on
babelqueue-core+ AWS SDK for Java v2:SqsPublisher(canonical-envelopeSendMessagewith the §3MessageAttributesprojection —bq-job/bq-trace-id/bq-message-id/bq-schema-version/bq-source-lang/bq-created-at; FIFO group/dedup) andSqsConsumer(long-poll receive → URN-routedBabelHandlers →DeleteMessage; SQS-native visibility-timeout retry;attemptsreconciled toApproximateReceiveCount − 1, never lowering a runtime-incremented count;onError/onUnknownUrnhooks). Java 17, JUnit 5, JaCoCo ≥90% line coverage (currently ~94%); unit tests run against a fakeSqsClient(no AWS, no network). The envelope is unchanged (schema_version: 1); SQS is purely additive.