Part of the Transport Configuration Reference.
Transport name: servicebus
Factory: servicebus.NewFactory(logger)
Capabilities: visibility_extension, source_redelivery, delayed_send
Azure Service Bus is stateless in GoBridge (no bridge-level sessions).
The options: block is decoded into a nested typed config: receiver settings
under options.receiver, sender settings under options.sender, and shared
connection/credential settings under options.connection. The transport
supports both queues and topic/subscription patterns, plus Azure SB sessions
for ordered processing.
Capabilities are mode-aware. The transport advertises
visibility_extension,source_redelivery, anddelayed_send, but the builder narrows the set per route from the receiver config. APeekLockqueue honours all three. APeekLocksubscription dropsdelayed_send: a scheduled retry would address the topic and fan out to sibling subscriptions, so a delayedRetryfalls back to an immediateAbandon(see Retry Wire Semantics). AReceiveAndDeletereceiver honours none of them — the message is gone at receive time — so its empty set lets the runtime’s “no retry + no DLQ = silent drop” check fire instead of being masked.
receivers:
- id: asb-queue-receiver
transport: servicebus
options:
connection:
connection_string: "Endpoint=sb://myns.servicebus.windows.net/;SharedAccessKeyName=listen;SharedAccessKey=..."
receiver:
queue_name: "orders"
max_messages: 10
max_wait_time: "30s"
receive_mode: "PeekLock"
lock_duration: "30s"
auto_extend: true
max_lock_renewal_duration: "5m"
- id: asb-topic-receiver
transport: servicebus
options:
connection:
namespace: "myns.servicebus.windows.net"
use_managed_identity: true
receiver:
topic_name: "events"
subscription_name: "bridge-sub"
receive_mode: "PeekLock"
session_id: "partition-1"
senders:
- id: asb-queue-sender
transport: servicebus
options:
connection:
connection_string: "Endpoint=sb://myns.servicebus.windows.net/;SharedAccessKeyName=send;SharedAccessKey=..."
sender:
queue_name: "commands"
batch_size: 10
timeout: "30s"
default_session_id: "partition-1"
- id: asb-topic-sender
transport: servicebus
options:
connection:
namespace: "myns.servicebus.windows.net"
tenant_id: "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee"
client_id: "ffffffff-0000-1111-2222-333333333333"
client_secret: "my-secret"
sender:
topic_name: "notifications"
options.receiver.*)| Key | Type | Default | Description |
|---|---|---|---|
queue_name |
string | – | Service Bus queue name |
topic_name |
string | – | Service Bus topic name |
subscription_name |
string | – | Subscription on the topic |
session_id |
string | – | Pin the receiver to ONE ASB session (cannot combine with sub_queue or use_sessions) |
use_sessions |
bool | false |
Consume a session-enabled entity by accepting the next available session and rotating between sessions (cannot combine with session_id or sub_queue) |
max_messages |
int | 10 | Messages per receive call (1–100). Forced to 1 in ReceiveAndDelete mode (warned). |
max_wait_time |
duration | 30s |
Maximum wait for messages (>= 1s; a bare int decodes as nanoseconds and is rejected) |
receive_mode |
string | PeekLock |
PeekLock or ReceiveAndDelete (case-insensitive; unknown values rejected). ReceiveAndDelete settles at the broker on receive — at-most-once: a crash after receive is unrecoverable loss. Rejected at config parse unless allow_at_most_once: true is also set. |
allow_at_most_once |
bool | false |
Explicit opt-in required for receive_mode: ReceiveAndDelete. Without it the config fails to parse, because ReceiveAndDelete deletes at the broker on receive (Ack is a no-op, Retry is unsupported) and a crash after receive loses the message. |
sub_queue |
string | – | "", "deadletter", or "transferdeadletter" (case-insensitive) |
lock_duration |
duration | 30s |
Expected lock duration (for auto-extend). Accepted range 5s–5m; 0 → 30s default. |
auto_extend |
bool | true |
Renew lock at 50% of duration |
max_lock_renewal_duration |
duration | 5m |
Caps total wall-clock time a single delivery’s lock is auto-renewed. When the cap is hit the delivery’s context is cancelled and renewal stops, so a hung pipeline cannot hold a message locked forever. Counted by ASBLockRenewalCapExceeded. |
Either queue_name or both topic_name + subscription_name are required.
Exactly one entity kind. Setting
queue_nameandtopic_name/subscription_nameon the same receiver is rejected at build. The two name a different entity; silently preferring the queue and ignoring the topic would consume from the wrong place. Configure a queue or a topic+subscription, never both.
Session-required entities. A session-enabled queue or subscription needs either
session_id(pin one session) oruse_sessions(rotate over available sessions); with neither, the receiver fails fast withErrNotSupportednaming both remedies.use_sessionsaccepts the next available session, rotates to another when the held session idles and every outstanding delivery has settled, backs off quietly when no session is available (not counted as a failure), and sheds the held session on a receive error.
Removed: the flat
prefetchkey no longer exists. The receiver runs atmax_messagescredit with no separate prefetch knob.
options.sender.*)| Key | Type | Default | Description |
|---|---|---|---|
queue_name |
string | – | Service Bus queue name |
topic_name |
string | – | Service Bus topic name |
default_session_id |
string | – | Default ASB session for messages |
batch_size |
int | 10 | Upper bound on messages per batch |
timeout |
duration | 30s |
Per-call send timeout (applied per chunk in SendBatch) |
Either queue_name or topic_name is required.
Exactly one entity kind. Setting both
queue_nameandtopic_nameon the same sender is rejected at build — a queue or a topic, never both, so a message can never be published to a different entity than intended.
options.connection.*)| Key | Type | Default | Description |
|---|---|---|---|
connection_string |
string | – | Full Service Bus connection string (redacted on marshal) |
namespace |
string | – | Namespace FQDN (token-based auth) |
use_managed_identity |
bool | false |
Use Azure Managed Identity |
tenant_id |
string | – | Azure AD tenant (app auth) |
client_id |
string | – | Azure AD app client ID |
client_secret |
string | – | Azure AD app client secret (redacted on marshal) |
ca_pem |
string | – | Custom CA certificate (PEM string) |
client_cert_pem |
string | – | Client certificate PEM for mutual TLS (requires client_key_pem) |
client_key_pem |
string | – | Client private key PEM for mutual TLS (redacted; requires client_cert_pem) |
insecure_skip_verify |
bool | false |
Skip TLS server verification |
A top-level options.credentials_uri resolves connection material from the
bridge credential store at build time. Either connection_string or namespace
is required.
SDK retry (
options.connection.retry.*). The Azure SDK retries inside every client operation (send, receive, settlement) beneath gobridge’s own retry policy and the poll backoff, so on a throttled namespace the layers multiply. Tune the SDK layer withretry.max_retries(SDK default 3; a negative value disables SDK retries so gobridge owns all retry),retry.retry_delay(initial backoff, default 4s), andretry.max_retry_delay(backoff cap, default 120s). Leaving theretryblock unset keeps the SDK defaults unchanged.
Service Bus has no native delayed-redelivery for a scheduled retry that resets
the broker DeliveryCount, so a delayed Retry schedules a fresh copy of the
message and stamps two reserved application properties on that copy:
x-bridge.retry-attempt — the accumulated 1-based receive count at schedule
time. Ingress adds it to the broker DeliveryCount so the runtime’s
MaxReplayAttempts gate and the broker’s MaxDeliveryCount still fire; a
poison message cannot ping-pong forever.x-bridge.original-message-id — the first delivery’s MessageID, restored as
the envelope ID on ingress so end-to-end dedup still sees one logical message.The scheduled copy’s own MessageID is salted with the attempt number so
broker duplicate detection never silently discards a scheduled retry. Both
reserved properties are stripped at ingress before headers reach the envelope,
so an external producer cannot inject them.
The scheduled delay is honored only on a queue. On a topic subscription a
delayed Retry falls back to an immediate Abandon — the delay is dropped
because a scheduled message addresses the topic and would fan out to sibling
subscriptions. Redelivery still happens; only the delay is lost.
Prefer duplicates over loss on an ambiguous settle. A delayed
Retryfirst schedules the copy, then completes the original. If the copy is durably scheduled butCompleteMessagethen fails ambiguously (timeout / connection-lost — the broker may already have committed the complete), the adapter does not cancel the scheduled copy and surfaces the error. Cancelling would, in the commit-but-error case, erase the only retry copy while the original is already gone — permanent loss. Keeping the copy yields at worst a duplicate (the original’s lock lapses and the broker redelivers it) that the copy’s saltedMessageID+x-bridge.original-message-idlet downstream dedup absorb.
A broker message with no MessageID does not get a fresh random envelope ID per
delivery — that would defeat downstream dedup, which would treat each redelivery
as a distinct message. The adapter derives a stable fallback ID from the broker
SequenceNumber, namespaced by the fully-qualified receive entity:
asb-seq:<scope>:<sequence>. The scope encodes the entity kind so no two
distinct entities can collide:
q:<queue-name>s:<topic-name>:<subscription-name>t:<topic-name>The SequenceNumber is unique only within one entity, so without the scope
prefix a queue and a subscription that each assign sequence number 5 would derive
the same fallback ID and cross-entity dedup could suppress a legitimate message.
The q:/s:/t: prefixes make the mapping injective, so that cannot happen. A
bridge-scheduled retry copy keeps its salted wire MessageID and restores the
first delivery’s MessageID from x-bridge.original-message-id, so this
fallback only applies to messages that genuinely arrived without one.
For a queue, a topic sender, or a non-session receiver, rotation is atomic: the adapter builds a fresh client first and commit-and-swaps only on success, so in-flight operations finish against the old client, new operations use the new one, and a failed build leaves the old client serving. There is no window with no client on this path.
A pinned-session receiver (session_id) is different. The broker holds an
exclusive lock on the session, so two clients cannot hold it at once. Rotation
closes the old link before building the replacement, leaving a brief window
where the receiver has no client. If the rebuild fails, the new connection stays
uncommitted and the rebuild is marked pending; the poll loop (or a re-push of the
same credentials) retries it, and currentClient() returns nil until it
succeeds. The gap is visible and recoverable — never a nil-panic — but it is a
real no-client window that the non-session path does not have.
Managed identity → client secret takes precedence. When a rotation
delivers an AAD client secret (a username/password credential), the adapter
switches to client-secret auth and clears use_managed_identity. The credential
builder evaluates managed identity before client-secret auth, so a lingering
use_managed_identity: true would otherwise ignore the rotated secret and keep
authenticating as the wrong identity. A supplied secret unambiguously means
client-secret auth, so the flag is cleared even when the client ID and secret
already match the current values (a stuck flag alone forces the switch).
A username without a secret is rejected, not silently downgraded. A rotation
credential that supplies a username but an empty password/secret is malformed:
AAD client-secret auth needs both, and the connection-string path requires an
empty username. Rather than treat the empty secret as client-secret material —
which would clear use_managed_identity, store a zero secret, and drift the
identity to DefaultAzureCredential — the adapter rejects the rotation with
ErrInvalidPayload and leaves the existing connection untouched. Supply the
secret for client-secret auth, or omit the username to rotate a connection
string.
lock_duration is a client-side mirror. It does not configure the
broker; the queue or subscription entity carries the authoritative
LockDuration. The receiver uses lock_duration only to seed the auto-extend
renewal cadence (half the value); once a message arrives its broker
LockedUntil deadline governs renewal. The accepted range is 5s–5m, and 0
resolves to the 30s default.lock_duration as the window. With
auto_extend on (the default) the check is skipped. With auto_extend: false
the builder judges send_timeout against the declared lock_duration, not
the broker’s real lock – a declared-short lock_duration is rejected at
build even when the broker entity permits a longer lock. Set lock_duration
to match the broker entity LockDuration so the declared window reflects what
the broker enforces.CodeClosed) never recovers on its
own. On such an error the sender tears the dead link down and rebuilds a fresh
one on the next Send/SendBatch, so a single closed link no longer wedges
the sender until process restart. A self-healing transient error
(CodeConnectionLost, which the SDK reopens on the next send) does not
trigger a rebuild. The teardown is fenced by link identity: if a concurrent
credential rotation already swapped in a fresh link, the stale teardown is a
no-op and never destroys the healthy replacement. Concurrent Send/SendBatch
callers resolve the live link atomically — a single locked ensure-and-
snapshot — so a closed-link teardown that nils the seam can never be observed
between “ensure” and “use”. Each caller captures one guaranteed-non-nil link
and finishes against it; there is no nil-dereference window under concurrent
sends racing a rebuild.