Queues
Durable competing-consumer work queues over AMQP 1.0 — at-least-once delivery, credit-driven consume, and settlement on the KubeMQ Queues pattern.
Queues are durable, competing-consumer work queues over the AMQP 1.0 connector. Attach a link to a node whose address begins with queues/<channel> and the connector binds it to the KubeMQ Queues pattern. A message goes to exactly one consumer (it is moved, not copied), survives until it is settled, and is delivered at-least-once by default.
Overview
The queues/ prefix selects the Queues pattern. A producer attaches a sender to queues/<ch>; each TRANSFER becomes a KubeMQ SendQueueMessage. A consumer attaches a receiver and grants credit; the server runs a credit-driven Get long-poll and frames each returned message as a TRANSFER. Do the work, then accept — the connector emits an AckRange and removes the message from the queue.
Many consumers can attach to the same queues/<ch>: the broker hands each message to one of them (competing-consumer move semantics — not fan-out). Add consumers to scale throughput; each message is still processed once.
| Operation | AMQP action | KubeMQ mapping |
|---|---|---|
| Produce | Attach a sender to queues/<ch>, TRANSFER (unsettled for at-least-once) | SendQueueMessage |
| Consume | Attach a receiver, grant credit, Receive | Credit-driven Get long-poll |
| Accept | accepted / rejected DISPOSITION | AckRange — message removed |
| Requeue | released / modified DISPOSITION | NAckRange — redelivered to the tail |
How it works
A producer enqueues unsettled messages (each blocks for the server's accepted disposition); competing consumers grant credit, receive, do the work, and accept — the message is removed from the queue.
Each queued message is moved to exactly one competing consumer; an accepted disposition acks the message and removes it from the queue.
Send and receive
Each example produces 10 unsettled messages (every send blocks until the server returns an accepted disposition, confirming the broker stored it), then consumes and accepts each one, and finally confirms the queue is empty. Every client reads the broker endpoint from KUBEMQ_AMQP_URL (default amqp://localhost:5672).
package main
import (
"context"
"fmt"
"log"
"os"
"time"
amqp "github.com/Azure/go-amqp"
)
const channel = "amqp10.examples.basic"
const total = 10
func amqpURL() string {
if v := os.Getenv("KUBEMQ_AMQP_URL"); v != "" {
return v
}
return "amqp://localhost:5672"
}
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
defer cancel()
addr := "queues/" + channel // queues/ prefix → KubeMQ Queues pattern
conn, err := amqp.Dial(ctx, amqpURL(), nil)
if err != nil {
log.Fatalf("dial: %v", err)
}
defer func() { _ = conn.Close() }()
session, _ := conn.NewSession(ctx, nil)
// 1. Produce — each Send is unsettled and blocks for the accepted DISPOSITION.
sender, _ := session.NewSender(ctx, addr, nil)
for i := 0; i < total; i++ {
if err := sender.Send(ctx, amqp.NewMessage([]byte(fmt.Sprintf("msg-%03d", i))), nil); err != nil {
log.Fatalf("send: %v", err)
}
}
_ = sender.Close(ctx)
// 2. Consume — grant credit, Receive, AcceptMessage (⇒ AckRange, removed).
receiver, _ := session.NewReceiver(ctx, addr, &amqp.ReceiverOptions{Credit: 10})
seen := make(map[string]struct{}, total)
for len(seen) < total {
msg, err := receiver.Receive(ctx, nil)
if err != nil {
log.Fatalf("receive: %v", err)
}
if err := receiver.AcceptMessage(ctx, msg); err != nil {
log.Fatalf("accept: %v", err)
}
seen[string(msg.GetData())] = struct{}{}
}
fmt.Printf("consumed and accepted %d messages\n", len(seen))
// 3. Assert the queue is empty — a further Receive must time out.
emptyCtx, emptyCancel := context.WithTimeout(ctx, 2*time.Second)
if _, err := receiver.Receive(emptyCtx, nil); err == nil {
log.Fatal("expected an empty queue")
}
emptyCancel()
_ = receiver.Close(ctx)
}import os
from proton import Message
from proton.utils import BlockingConnection
CHANNEL = "amqp10.examples.basic"
TOTAL = 10
def amqp_url() -> str:
return os.environ.get("KUBEMQ_AMQP_URL", "amqp://localhost:5672")
def main() -> None:
addr = "queues/" + CHANNEL # queues/ prefix → KubeMQ Queues pattern
conn = BlockingConnection(amqp_url())
try:
# 1. Produce — each send is unsettled and blocks for the accepted DISPOSITION.
sender = conn.create_sender(addr)
for i in range(TOTAL):
sender.send(Message(body=f"msg-{i:03d}"))
sender.close()
# 2. Consume — grant credit, receive, accept (⇒ AckRange, removed).
receiver = conn.create_receiver(addr, credit=10)
seen: set[str] = set()
while len(seen) < TOTAL:
msg = receiver.receive(timeout=30.0)
receiver.accept()
seen.add(str(msg.body))
print(f"consumed and accepted {len(seen)} messages")
# 3. Assert the queue is empty — a further receive must time out.
try:
receiver.receive(timeout=2.0)
except Exception:
pass # the EXPECTED idle timeout on an empty queue
else:
raise SystemExit("expected an empty queue")
receiver.close()
finally:
conn.close()
if __name__ == "__main__":
main()import java.util.HashSet;
import java.util.Set;
import javax.jms.Connection;
import javax.jms.Message;
import javax.jms.MessageConsumer;
import javax.jms.MessageProducer;
import javax.jms.Queue;
import javax.jms.Session;
import javax.jms.TextMessage;
import org.apache.qpid.jms.JmsConnectionFactory;
public final class Main {
private static final String CHANNEL = "amqp10.examples.basic";
private static final int TOTAL = 10;
public static void main(String[] args) throws Exception {
String url = System.getenv().getOrDefault("KUBEMQ_AMQP_URL", "amqp://localhost:5672");
String address = "queues/" + CHANNEL; // queues/ prefix → KubeMQ Queues pattern
JmsConnectionFactory factory = new JmsConnectionFactory(url);
try (Connection connection = factory.createConnection()) {
connection.start();
try (Session session = connection.createSession(false, Session.CLIENT_ACKNOWLEDGE)) {
Queue queue = session.createQueue(address);
// 1. Produce — a MessageProducer is a server-receiver link; each send
// blocks for the accepted DISPOSITION (unsettled, at-least-once).
try (MessageProducer producer = session.createProducer(queue)) {
for (int i = 0; i < TOTAL; i++) {
producer.send(session.createTextMessage(String.format("msg-%03d", i)));
}
}
// 2. Consume — a CLIENT_ACKNOWLEDGE consumer; acknowledge() settles
// `accepted` ⇒ AckRange (removed from the queue).
try (MessageConsumer consumer = session.createConsumer(queue)) {
Set<String> seen = new HashSet<>();
while (seen.size() < TOTAL) {
Message msg = consumer.receive(30_000);
if (msg == null) throw new IllegalStateException("timed out");
String body = msg.getBody(String.class);
msg.acknowledge();
seen.add(body);
}
System.out.printf("consumed and accepted %d messages%n", seen.size());
// 3. Assert the queue is empty — a further receive times out.
if (consumer.receive(2_000) != null) {
throw new IllegalStateException("expected an empty queue");
}
}
}
}
}
}using System.Text;
using Amqp;
using Amqp.Framing;
const string channel = "amqp10.examples.basic";
const int total = 10;
static string AmqpUrl() =>
Environment.GetEnvironmentVariable("KUBEMQ_AMQP_URL") is { Length: > 0 } v
? v
: "amqp://localhost:5672";
var addr = "queues/" + channel; // queues/ prefix → KubeMQ Queues pattern
var connection = await Connection.Factory.CreateAsync(new Address(AmqpUrl()));
try
{
var session = new Session(connection);
// 1. Produce — each Send is unsettled and blocks for the accepted DISPOSITION.
var sender = new SenderLink(session, "basic-sender", addr);
for (var i = 0; i < total; i++)
{
var message = new Message { BodySection = new Data { Binary = Encoding.UTF8.GetBytes($"msg-{i:D3}") } };
sender.Send(message, TimeSpan.FromSeconds(15));
}
// 2. Consume — grant credit, Receive, Accept (⇒ AckRange, removed).
// Keep the sender open through the consume phase (see the README gotcha).
var receiver = new ReceiverLink(session, "basic-receiver", addr);
receiver.SetCredit(10, autoRestore: true);
var seen = new HashSet<string>();
while (seen.Count < total)
{
var message = receiver.Receive(TimeSpan.FromSeconds(30))
?? throw new InvalidOperationException("receive timed out");
receiver.Accept(message);
seen.Add(BodyString(message));
}
Console.WriteLine($"consumed and accepted {seen.Count} messages");
// 3. Assert the queue is empty — a further Receive must time out (null).
if (receiver.Receive(TimeSpan.FromSeconds(2)) is not null)
throw new InvalidOperationException("expected an empty queue");
await receiver.CloseAsync();
await sender.CloseAsync();
await session.CloseAsync();
}
finally
{
await connection.CloseAsync();
}
static string BodyString(Message message) => message.BodySection switch
{
Data d => Encoding.UTF8.GetString(d.Binary),
AmqpValue { Value: byte[] bytes } => Encoding.UTF8.GetString(bytes),
AmqpValue { Value: string str } => str,
AmqpValue v => v.Value?.ToString() ?? string.Empty,
_ => string.Empty,
};import {
Connection,
ReceiverEvents,
type EventContext,
type Receiver,
} from "rhea-promise";
const channel = "amqp10.examples.basic";
const total = 10;
function bodyToString(body: unknown): string {
return Buffer.isBuffer(body) ? body.toString("utf8") : String(body);
}
async function main(): Promise<void> {
const url = new URL(process.env["KUBEMQ_AMQP_URL"] ?? "amqp://localhost:5672");
const address = `queues/${channel}`; // queues/ prefix → KubeMQ Queues pattern
const connection = new Connection({
host: url.hostname,
port: url.port ? Number(url.port) : 5672,
container_id: `kubemq-amqp10-js-basic-${process.pid}`,
reconnect: false,
});
await connection.open();
try {
// 1. Produce — an AwaitableSender; each send() resolves on the accepted DISPOSITION.
const sender = await connection.createAwaitableSender({ target: { address } });
for (let i = 0; i < total; i++) {
await sender.send({ body: `msg-${String(i).padStart(3, "0")}` }, { timeoutInSeconds: 15 });
}
await sender.close();
// 2. Consume — grant credit manually, settle manually. Register the handler
// BEFORE addCredit so early deliveries are not missed.
const receiver = await connection.createReceiver({
source: { address },
credit_window: 0,
autoaccept: false,
autosettle: false,
});
const seen = new Set<string>();
await receiveUntil(receiver, (ctx) => {
ctx.delivery?.accept(); // accept ⇒ AckRange ⇒ removed from the queue
seen.add(bodyToString(ctx.message?.body));
return seen.size >= total;
}, total + 1, 30_000);
console.log(`consumed and accepted ${seen.size} messages`);
await receiver.close();
} finally {
await connection.close();
}
}
function receiveUntil(
receiver: Receiver,
onMessage: (ctx: EventContext) => boolean,
credit: number,
timeoutMs: number,
): Promise<void> {
return new Promise<void>((resolve, reject) => {
const timer = setTimeout(() => {
receiver.removeListener(ReceiverEvents.message, handler);
reject(new Error("timed out waiting for messages"));
}, timeoutMs);
const handler = (ctx: EventContext): void => {
if (onMessage(ctx)) {
clearTimeout(timer);
receiver.removeListener(ReceiverEvents.message, handler);
resolve();
}
};
receiver.on(ReceiverEvents.message, handler);
receiver.addCredit(credit);
});
}
main().catch((err) => {
console.error(err);
process.exit(1);
});use std::collections::HashSet;
use std::time::Duration;
use fe2o3_amqp::link::delivery::Delivery;
use fe2o3_amqp::link::receiver::CreditMode;
use fe2o3_amqp::{Connection, Receiver, Sender, Session};
use fe2o3_amqp_types::definitions::SenderSettleMode;
use fe2o3_amqp_types::messaging::{Body, Message};
use fe2o3_amqp_types::primitives::Value;
const CHANNEL: &str = "amqp10.examples.basic";
const TOTAL: usize = 10;
fn amqp_url() -> String {
std::env::var("KUBEMQ_AMQP_URL").unwrap_or_else(|_| "amqp://localhost:5672".to_string())
}
fn body_bytes(msg: &Message<Body<Value>>) -> Vec<u8> {
match &msg.body {
Body::Data(batch) => batch.iter().flat_map(|d| d.0.to_vec()).collect(),
Body::Value(v) => match &v.0 {
Value::Binary(b) => b.to_vec(),
Value::String(s) => s.clone().into_bytes(),
other => format!("{other:?}").into_bytes(),
},
_ => Vec::new(),
}
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let addr = format!("queues/{CHANNEL}"); // queues/ prefix → KubeMQ Queues pattern
let mut connection = Connection::open("amqp10-examples-basic", amqp_url().as_str()).await?;
let mut session = Session::begin(&mut connection).await?;
// 1. Produce — pin SenderSettleMode::Unsettled (the connector rejects the AMQP
// default `mixed`); each send is at-least-once and blocks for the Accepted outcome.
let mut sender = Sender::builder()
.name("basic-send-receive-sender")
.target(addr.as_str())
.sender_settle_mode(SenderSettleMode::Unsettled)
.attach(&mut session)
.await?;
for i in 0..TOTAL {
let outcome = sender.send(format!("msg-{i:03}")).await?;
if !outcome.is_accepted() {
return Err(format!("send msg-{i:03}: unexpected outcome {outcome:?}").into());
}
}
sender.close().await?;
// 2. Consume — CreditMode::Auto(10) issues 10 credits and replenishes on settle;
// accept ⇒ AckRange ⇒ removed from the queue.
let mut receiver = Receiver::builder()
.name("basic-send-receive-receiver")
.source(addr.as_str())
.credit_mode(CreditMode::Auto(10))
.attach(&mut session)
.await?;
let mut seen: HashSet<String> = HashSet::with_capacity(TOTAL);
while seen.len() < TOTAL {
let delivery: Delivery<Body<Value>> = receiver.recv().await?;
let body = String::from_utf8_lossy(&body_bytes(delivery.message())).into_owned();
receiver.accept(&delivery).await?;
seen.insert(body);
}
println!("consumed and accepted {} messages", seen.len());
// 3. Assert the queue is empty — a further recv must time out.
if let Ok(Ok(_)) = tokio::time::timeout(Duration::from_secs(2), receiver.recv::<Body<Value>>()).await {
return Err("expected an empty queue".into());
}
receiver.close().await?;
session.end().await?;
connection.close().await?;
Ok(())
}Delivery guarantees
Settlement mode is negotiated at ATTACH and decides the produce/consume guarantee:
| Mode | How | Guarantee | Use when |
|---|---|---|---|
| At-least-once (default) | unsettled deliveries; you accept after the work succeeds | survives a crash; may be redelivered | the work must not be lost |
| Pre-settled (at-most-once) | snd-settle-mode=settled on the sender / consume | fast, no DISPOSITION round-trip; a failed publish/consume is dropped | loss is acceptable for speed |
At-least-once means a consumer crash before accept requeues the message, so workers must be idempotent — a message can arrive more than once. The receiver settle mode is always replied as first; requesting second returns DETACH(amqp:not-implemented).
Delivery-state outcome mapping
When you consume, you settle each delivery by sending a DISPOSITION with a delivery state. The connector maps that state onto the KubeMQ queue Ack/NAck machinery:
| Your DISPOSITION | Client call (typical) | KubeMQ action | Effect |
|---|---|---|---|
accepted | AcceptMessage | AckRange | message removed |
rejected | RejectMessage | AckRange | discarded; poison handled by the broker's MaxReceiveQueue policy |
released | ReleaseMessage | NAckRange | redelivered to the tail; delivery-count grows; increments receive-count |
modified{...} | ModifyMessage | NAckRange | requeued to the tail |
| nil state (settled, no outcome) | settle without a state | AckRange | treated as success |
| unknown terminal state | — | NAckRange | conservatively requeued — never silently dropped |
A redelivered message carries header.delivery-count = ReceiveCount − 1 and first-acquirer = (ReceiveCount == 1) — use these to detect and de-duplicate redeliveries. On detach, connection close, or shutdown, every unsettled delivery is NAcked exactly once (returned to the queue tail), so a disconnecting worker loses nothing — a fresh consumer recovers the in-flight work.
released / modified increment the receive-count. Every release/modify for redelivery bumps ReceiveCount toward the broker's MaxReceiveQueue cap. A message you keep NAcking eventually hits that cap and is removed even though you never rejected it — there is no requeue-without-increment. To genuinely discard, reject; to retry, understand the count climbs.
Body sections
A message body must be Data (binary — the default; multiple Data sections concatenate) or AmqpValue (a typed value). An empty body is valid. An AmqpSequence body is rejected (rejected DISPOSITION then DETACH(amqp:not-implemented)).
What queues do not have
The Queues pattern is deliberately minimal — none of these exist:
- No peek / browse / visibility-timeout. Receive is destructive credit-based consume only; there is no "look without taking".
- No connector dead-letter exchange. A
rejectedmessage is discarded; poison handling is the broker-sideMaxReceiveQueuepolicy, not a per-link DLX. - No
copydistribution-mode. Requestingcopyon aqueues/link returnsDETACH(amqp:invalid-field)— queues are move-only. - No selectors. A selector filter on a
queues/link returnsamqp:not-implemented(selectors are pub/sub only). - No transactions. Reliability comes from settlement, not
SESSION_TRANSACTED.
Related
Was this page helpful?
Queries
Native AMQP 1.0 request/reply over KubeMQ Queries — fetch a result body over the shared RPC path, with timeouts when no query result is returned.
Getting Started
Connect a stock AMQP 1.0 client to KubeMQ and run a send-and-receive round-trip through a KubeMQ Queue in minutes — no KubeMQ SDK required.