KubeMQ
ConnectorsAMQP 1.0Tutorials

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.

Get a message flowing through the KubeMQ AMQP 1.0 connector in minutes. You point a standard AMQP 1.0 client at the broker, attach a sender to queues/<channel>, produce a few messages, then attach a receiver and consume them back — all over the native AMQP 1.0 wire, with no KubeMQ SDK. This walkthrough takes you from a running server to a verified at-least-once round-trip.

Prerequisites

  • A running kubemq-server with the AMQP 1.0 connector enabled and reachable on port 5672 (plain TCP). The connector is opt-in (disabled by default) — see the enable step below.
  • One of the AMQP 1.0 clients below for your language (the examples pin a native client per language — there is no KubeMQ SDK).

Enable the connector

The AMQP 1.0 connector is disabled by default — a stock kubemq-server does not bind the AMQP 1.0 listener until you turn it on. Enable it with its enable variable:

docker run -d \  --name kubemq \  -p 5672:5672 \  -p 5671:5671 \  -p 50000:50000 \  -e KUBEMQ_TOKEN=YOUR_LICENSE_KEY \  -e CONNECTORS_AMQP10_ENABLE=true \  europe-docker.pkg.dev/kubemq/images/kubemq:next

The enable variable is CONNECTORS_AMQP10_ENABLE — the literal 10 stays attached to AMQP with no underscore. CONNECTORS_AMQP_1_0_ENABLE and CONNECTORS_AMQP10ENABLE do not bind. For Kubernetes, set spec.amqp10.enabled: true in the KubemqCluster CR.

Bring up a throwaway local broker with AMQP 1.0 enabled:

Every example reads a single environment variable for the broker endpoint. A URL with no userinfo negotiates SASL ANONYMOUS, so a stock dev broker is clone-and-run with no credentials:

# default: amqp://localhost:5672
export KUBEMQ_AMQP_URL="amqp://localhost:5672"

To disable the AMQP 1.0 connector after enabling it, set its enable variable to false:

docker run -d -e KUBEMQ_TOKEN=YOUR_LICENSE_KEY -e CONNECTORS_AMQP10_ENABLE=false europe-docker.pkg.dev/kubemq/images/kubemq:next

When Enable is false, all other AMQP 1.0 validation is skipped and no listener binds. See Configuration for the full settings list.

How it works

A producer attaches a sender link to a node address; the connector resolves the address prefix to a KubeMQ pattern and channel and enqueues each message. A consumer attaches a receiver link to the same address, grants credit, and the connector delivers and removes each accepted message.

A sender produces unsettled messages to a Queue channel; a receiver grants credit, and the connector delivers and removes each accepted message.

Steps

Connect to the broker

Open an AMQP 1.0 connection to the endpoint in KUBEMQ_AMQP_URL. A URL with no userinfo negotiates SASL ANONYMOUS; the client sends a non-empty container-id automatically (the connector requires one). One session carries both the producer and consumer links in the steps below.

The language tabs across all three steps run the complete round-trip from a single program: connect, produce 10 messages to queues/amqp10.examples.basic, consume and accept each, and confirm the queue drains to empty.

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

	// CONNECT: OPEN (SASL ANONYMOUS) + BEGIN one session.
	conn, err := amqp.Dial(ctx, amqpURL(), nil)
	if err != nil {
		log.Fatalf("dial: %v", err)
	}
	defer func() { _ = conn.Close() }()
	session, err := conn.NewSession(ctx, nil)
	if err != nil {
		log.Fatalf("new session: %v", err)
	}

	// SEND: attach a sender; each unsettled Send blocks for the accepted disposition.
	sender, err := session.NewSender(ctx, addr, nil)
	if err != nil {
		log.Fatalf("new sender: %v", err)
	}
	for i := 0; i < total; i++ {
		body := fmt.Sprintf("msg-%03d", i)
		if err := sender.Send(ctx, amqp.NewMessage([]byte(body)), nil); err != nil {
			log.Fatalf("send %s: %v", body, err)
		}
	}
	_ = sender.Close(ctx)
	fmt.Printf("[send] Produced %d messages to %s\n", total, addr)

	// RECEIVE: attach a receiver with credit; accept each => removed from the queue.
	receiver, err := session.NewReceiver(ctx, addr, &amqp.ReceiverOptions{Credit: 10})
	if err != nil {
		log.Fatalf("new receiver: %v", err)
	}
	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("[recv] Consumed and accepted %d messages (no loss)\n", len(seen))
	_ = 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

    # CONNECT: OPEN (SASL ANONYMOUS) — proton sends a non-empty container-id.
    conn = BlockingConnection(amqp_url())
    try:
        # SEND: attach a sender; each send 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()
        print(f"[send] Produced {TOTAL} messages to {addr}")

        # RECEIVE: attach a receiver with credit; accept each => removed from the queue.
        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"[recv] Consumed and accepted {len(seen)} messages (no loss)")
        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 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;

        // CONNECT: OPEN (SASL ANONYMOUS). The JMS destination name IS the node address.
        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);

                // SEND: a producer is a server-receiver link; send blocks for accepted.
                try (MessageProducer producer = session.createProducer(queue)) {
                    for (int i = 0; i < TOTAL; i++) {
                        producer.send(session.createTextMessage(String.format("msg-%03d", i)));
                    }
                }
                System.out.printf("[send] Produced %d messages to %s%n", TOTAL, address);

                // RECEIVE: a CLIENT_ACKNOWLEDGE consumer; acknowledge() settles accepted.
                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 before draining the queue");
                        }
                        String body = msg.getBody(String.class);
                        msg.acknowledge();
                        seen.add(body);
                    }
                    System.out.printf("[recv] Consumed and accepted %d messages (no loss)%n", seen.size());
                }
            }
        }
    }
}
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;

// CONNECT: OPEN (SASL ANONYMOUS) + one session.
var connection = await Connection.Factory.CreateAsync(new Address(AmqpUrl()));
try
{
    var session = new Session(connection);

    // SEND: a SenderLink is a server-receiver link; Send blocks for accepted.
    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));
    }
    Console.WriteLine($"[send] Produced {total} messages to {addr}");

    // RECEIVE: grant credit; Accept each => removed from the queue. Keep the
    // sender open through the consume phase (detaching it early can stall delivery).
    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($"[recv] Consumed and accepted {seen.Count} messages (no loss)");

    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 ConnectionOptions,
  type EventContext,
  type Receiver,
} from "rhea-promise";

const channel = "amqp10.examples.basic";
const total = 10;

function connectionOptions(): ConnectionOptions {
  const url = new URL(process.env["KUBEMQ_AMQP_URL"] ?? "amqp://localhost:5672");
  return {
    host: url.hostname,
    port: url.port ? Number(url.port) : 5672,
    container_id: `kubemq-amqp10-js-${process.pid}`,
    reconnect: false,
  };
}

function bodyToString(body: unknown): string {
  return Buffer.isBuffer(body) ? body.toString("utf8") : String(body);
}

async function main(): Promise<void> {
  const address = `queues/${channel}`;

  // CONNECT: OPEN (SASL ANONYMOUS).
  const connection = new Connection(connectionOptions());
  await connection.open();
  try {
    // SEND: 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();
    console.log(`[send] Produced ${total} messages to ${address}`);

    // RECEIVE: manual credit + manual settle; accept() removes from the queue.
    const receiver = await connection.createReceiver({
      source: { address },
      credit_window: 0,
      autoaccept: false,
      autosettle: false,
    });
    const seen = new Set<string>();
    await new Promise<void>((resolve, reject) => {
      receiver.on(ReceiverEvents.message, (ctx: EventContext) => {
        ctx.delivery?.accept();
        seen.add(bodyToString(ctx.message?.body));
        if (seen.size >= total) resolve();
      });
      receiver.on(ReceiverEvents.receiverError, (ctx: EventContext) =>
        reject(ctx.receiver?.error ?? new Error("receiver error")));
      (receiver as Receiver).addCredit(total);
    });
    console.log(`[recv] Consumed and accepted ${seen.size} messages (no loss)`);
    await receiver.close();
  } finally {
    await connection.close();
  }
}

main().catch((err) => {
  console.error(err);
  process.exit(1);
});
use std::collections::HashSet;

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_string(msg: &Message<Body<Value>>) -> String {
    match &msg.body {
        Body::Data(batch) => batch.iter().flat_map(|d| d.0.iter().copied()).collect::<Vec<u8>>(),
        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(),
    }
    .into_iter()
    .map(|b| b as char)
    .collect()
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let addr = format!("queues/{CHANNEL}");

    // CONNECT: OPEN (SASL ANONYMOUS) + BEGIN one session.
    let mut connection = Connection::open("amqp10-examples-basic", amqp_url().as_str()).await?;
    let mut session = Session::begin(&mut connection).await?;

    // SEND: a sender pinned to Unsettled (at-least-once); the default `mixed` is rejected.
    let mut sender = Sender::builder()
        .name("basic-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 {i}: unexpected outcome {outcome:?}").into());
        }
    }
    sender.close().await?;
    println!("[send] Produced {TOTAL} messages to {addr}");

    // RECEIVE: client-granted auto credit; accept each => removed from the queue.
    let mut receiver = Receiver::builder()
        .name("basic-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?;
        receiver.accept(&delivery).await?;
        seen.insert(body_string(delivery.message()));
    }
    println!("[recv] Consumed and accepted {} messages (no loss)", seen.len());

    receiver.close().await?;
    session.end().await?;
    connection.close().await?;
    Ok(())
}

Send messages

The send phase in the program above attaches a sender link with the target address queues/amqp10.examples.basic and produces 10 messages. Because the sends are unsettled, each call blocks until the connector returns an accepted disposition — confirmation that the broker stored the message (at-least-once). Always use the explicit pattern prefix (queues/…); never rely on the connector's bare-address fallback.

Receive and verify

The receive phase attaches a receiver link to the same address and grants link credit; the connector delivers each message, and accept settles it — emitting an AckRange that removes it from the queue. After consuming all 10, a further receive times out, proving the queue drained with no loss:

[send] Produced 10 messages to queues/amqp10.examples.basic
[recv] Consumed and accepted 10 messages (no loss)
[recv] Queue drained to empty (no further messages)

Behind that, the full AMQP 1.0 handshake ran end-to-end: OPENBEGINATTACH (sender, then receiver) → TRANSFER / FLOW / DISPOSITIONDETACH / CLOSE.

Two flow-control footguns to internalize early: Events drop silently at zero credit (keep a consumer's credit topped up), and Events-Store stalls and loses its window if a durable consumer's credit is not replenished. Both are covered in Flow control.

Next steps

Was this page helpful?

On this page