Import path github.com/smartpricing/queen/clients/client-go, package name queen. Requires Go
1.24 or newer. Every network method takes a context.Context as its first argument. Version
1.3.0, aligned with the broker’s 1.3 line.
import queen "github.com/smartpricing/queen/clients/client-go"The streaming SDK is a separate package under .../client-go/streams.
Constructor
func New(config interface{}) (*Queen, error)config accepts a string (single URL), a []string (multiple URLs), a ClientConfig, or a
*ClientConfig. Anything else returns
invalid config type: expected string, []string, or ClientConfig.
ClientConfig
| Field | Type | Default applied | Effect |
|---|---|---|---|
URLs |
[]string |
none | Multiple brokers. More than one entry activates the load balancer. |
URL |
string |
none | Single broker, converted to URLs. |
TimeoutMillis |
int |
30000 |
Per-request deadline. |
RetryAttempts |
int |
3 |
Retries after the first attempt, so the default is 4 attempts in total. A negative value means exactly one attempt. |
RetryDelayMillis |
int |
1000 |
First retry delay; doubles per attempt. |
LoadBalancingStrategy |
string |
"affinity" |
"affinity", "round-robin" or "session". |
AffinityHashRing |
int |
128 |
Virtual nodes per backend on the hash ring. |
EnableFailover |
bool |
false in practice (see below) |
Whether a failing backend is marked unhealthy. |
HealthRetryAfterMillis |
int |
5000 |
How long a backend stays marked unhealthy. |
BearerToken |
string |
"" |
Sent as Authorization: Bearer <token>. |
Headers |
map[string]string |
nil |
Extra headers on every request. |
MaxIdleConnsPerHost |
int |
256 when <= 0 |
Go’s own default is 2, which cripples a single-host high-concurrency producer pool. |
MaxConnsPerHost |
int |
0 (unlimited) |
Hard cap on total connections per host. |
Retry429 |
*Retry429Config |
nil |
HTTP 429 backoff policy. |
The exported ClientDefaults, QueueDefaults, ConsumeDefaults, PopDefaults and
BufferDefaults variables hold these values, so an application can read one instead of
restating it. Unlike the other clients, the load balancer is constructed unconditionally, even
for a single URL, so LoadBalancingStrategy and the health settings are always in play.
Retry429Config
Retry429Config (MaxAttempts, BaseMs, CapMs) covers HTTP 429 separately from
RetryAttempts, and a zero field means “use the kind-based default”; the defaults, the
Retry-After contract and the jitter are in
What the clients do with a 429.
*Queen
| Method | Signature |
|---|---|
Queue |
Queue(name string) *QueueBuilder |
Admin |
Admin() *Admin. Lazily created, one per client |
Transaction |
Transaction() *TransactionBuilder |
Ack |
Ack(ctx, messages interface{}, success bool, opts AckOptions) ([]AckResponse, error) |
Renew |
Renew(ctx, messages interface{}) ([]RenewResponse, error) |
FlushAllBuffers |
FlushAllBuffers(ctx) error |
GetBufferStats |
GetBufferStats() BufferStats |
DeleteConsumerGroup |
DeleteConsumerGroup(ctx, consumerGroup string, deleteMetadata bool) error |
UpdateConsumerGroupTimestamp |
UpdateConsumerGroupTimestamp(ctx, consumerGroup string, timestamp time.Time) error |
Close |
Close(ctx) error. Flushes buffers, then closes the HTTP client |
GetHttpClient |
GetHttpClient() *HttpClient |
GetBufferManager |
GetBufferManager() *BufferManager |
Ack
messages accepts *Message, Message, []*Message or []Message; anything else returns an
error. success maps to AckStatusCompleted ("completed") or AckStatusFailed ("failed").
AckOptions carries ConsumerGroup and Error.
One message uses POST /api/v1/ack, more than one uses POST /api/v1/ack/batch. Both return one
AckResponse{Success, Error} per message in request order. ValidateMessages runs first, so a
message missing TransactionID or PartitionID fails before any request is sent.
Renew
Accepts a lease-id string, []string, or any of the message forms. Lease ids are deduplicated
first: with multi-partition pop every message in a batch shares one LeaseID and one extend call
renews every claimed partition, so passing the whole slice issues one HTTP call. RenewResponse
carries LeaseID, Success, NewExpiresAt time.Time and Error.
*QueueBuilder
client.Queue(name). Configuration methods return *QueueBuilder; terminal methods are Push,
Pop, Consume, ConsumeBatch, Create, Delete, DLQ and FlushBuffer.
Addressing
| Method | Default | Notes |
|---|---|---|
Name() string |
none | The queue name (used by the streaming SDK). |
Partition(name) |
DefaultPartition ("Default") |
The ordered lane. Anything else switches pop to the partition-scoped route. |
Namespace(name) |
"" |
Grouping label at create time; also a pop filter. |
Task(name) |
"" |
Second grouping label; same dual role. |
Group(name) |
"" |
Consumer group. Absent, the broker uses QueueModeConsumerGroup ("__QUEUE_MODE__"). |
Queue lifecycle
| Method | Wire call | Returns |
|---|---|---|
Config(config QueueConfig) |
none | *QueueBuilder |
Create() |
POST /api/v1/configure |
*OperationBuilder |
Delete() |
DELETE /api/v1/resources/queues/:queue |
*OperationBuilder |
*OperationBuilder has one method: Execute(ctx) (map[string]interface{}, error). Create
validates the queue name first and adds "configured": true to the returned map for parity with
the other clients.
QueueConfig holds the fields Create can send, each mapped onto its camelCase wire key:
LeaseTime, RetryLimit, DelayedProcessing, WindowBuffer, RetentionSeconds,
CompletedRetentionSeconds and EncryptionEnabled, whose QueueDefaults values are the
shared client-side defaults listed under
Queue option defaults, plus three booleans the
other clients’ defaults dicts do not carry: RetentionEnabled, DeadLetterQueue and
DlqAfterMaxRetries, all false, which by the rule below means they are not sent at all.
Two builder methods sit on *OperationBuilder for this:
| Method | Effect |
|---|---|
Replace(enabled bool) |
true puts "mode":"replace" at the top level of the body. false, or never calling it, sends no mode key at all, so the request stays byte-identical to every released version of this SDK. Read only by the create path. |
Option(key string, value any) |
Puts one option on the wire literally, applied after the QueueConfig bag and therefore winning over it. This is the only way to send an explicit false or 0, which the omit-the-zero rule above cannot express, and nil sends JSON null, the broker’s “restore this option’s default”. The spelling is the wire’s camelCase and is not validated here. |
_, err := client.Queue("orders").
Config(queen.QueueConfig{LeaseTime: 60}).
Option("deadLetterQueue", false). // an explicit false, unsendable from QueueConfig
Option("retentionSinkHold", nil). // null: back to the default
Create().
Execute(ctx)A broker older than 1.6.0 ignores mode and replaces either way, which is what it has always
done.
Producing
| Method | Notes |
|---|---|
Buffer(config BufferConfig) |
{MessageCount, TimeMillis, MaxSize, RetryDelayMillis}; BufferDefaults is 100 / 1000 / 400 / 250. Turns Push into a bounded enqueue. |
Push(payload interface{}) *PushBuilder |
payload is one value or a slice. |
*PushBuilder:
| Method | Notes |
|---|---|
TransactionID(id string) |
Explicit id. Applied to the first item only. |
TraceID(id string) |
Trace id, forwarded only when it is a valid UUID. |
Execute(ctx) ([]PushResponse, error) |
Sends POST /api/v1/push, or enqueues when Buffer is set. |
PushResponse carries Status ("queued", "duplicate", "failed"), TransactionID and
Error. The client mints a UUIDv7 transactionId for items that have none.
res, err := client.Queue("orders").
Partition("customer-42").
Push(map[string]any{"orderId": 9137, "amount": 99.5}).
Execute(ctx)
if err != nil {
return err
}
// res[0].Status == "queued"Consuming
| Method | Pop default |
Consume default |
Notes |
|---|---|---|---|
Batch(size) |
broker-sized | broker-sized | Total message budget for the call, shared across every partition it claims. Left unset the broker sizes it (pop autopilot); the pre-1.2 client default of 1 comes back with autopilot off. |
Partitions(n) |
broker-sized | broker-sized | Claim up to N partitions per call; all share one LeaseID. Left unset the broker sizes it (pop autopilot). Sent verbatim when set, 1 included, because a pinned width is a decision the broker must not widen; with autopilot off it is sent only when > 1. |
Autopilot(enabled) |
true |
true |
Broker-side pop sizing for the knobs above that you did not set. On by default; false restores the pre-1.2 client defaults (batch 1, partitions 1) and sends no autopilot parameter. QUEEN_SDK_POP_AUTOPILOT=off does the same for a whole process, read once when the client is built. Setting both knobs leaves nothing to decide, so no parameter is sent then either. See pop autopilot. Requires broker >= 1.2; an older one applies its own defaults (batch 200, partitions 1) to the omitted knobs, which is a sizing difference and nothing more. |
Wait(enabled) |
false |
true |
Long-poll. Tri-state internally: unset falls back to the per-method default. |
TimeoutMillis(ms) |
30000 |
30000 |
Server-side deadline. Pop adds 5 s of client slack when waiting. |
AutoAck(enabled) |
not sent | true |
The two meanings differ. See the warning. |
SubscriptionMode(mode) |
"" |
"" |
SubscriptionModeAll, SubscriptionModeNew, SubscriptionModeNewOnly. |
SubscriptionFrom(from) |
"" |
"" |
SubscriptionFromNow ("now") or an ISO timestamp. |
Conflation(enabled) |
false |
false |
Last-value delivery: the pop returns only each partition’s newest visible message and retires the rest. Sent only when true. A property of the consumer GROUP, stored on its first registration; see conflation. Requires broker >= 1.1.0; against an older one the call raises rather than draining the backlog quietly. |
Concurrency(count) |
n/a | 1 |
Consume only. Worker goroutines. |
Limit(count) |
n/a | 0 (unlimited) |
Consume only. |
IdleMillis(ms) |
n/a | 0 (no timeout) |
Consume only. |
RenewLease(enabled, intervalMillis) |
n/a | false |
Consume only. |
Each() |
n/a | batch mode | Consume only. One message per handler call. |
Pop
func (qb *QueueBuilder) Pop(ctx context.Context) ([]*Message, error)Returns queue name, namespace, or task is required when none of the three is set. Route
selection: queue plus non-Default partition uses /api/v1/pop/queue/:queue/partition/:partition;
queue alone uses /api/v1/pop/queue/:queue; namespace or task alone uses /api/v1/pop.
messages, err := client.Queue("orders").
Batch(10).
Wait(true).
Pop(ctx)
if err != nil {
return err
}PopResult
func (qb *QueueBuilder) PopResult(ctx context.Context) (PopResult, error)The same call as Pop, returning PopResult{Messages, Autopilot}: the broker’s account of how
it sized the claim, with Partitions, Batch, and an optional WaitMillis pacing hint the
consume loop honours in place of its own delay between empty polls. Autopilot is nil when the
pop did not engage autopilot, when the broker is older than 1.2, or when the answer was a bodiless
204, which has no body to carry an echo. See pop autopilot.
Consume and ConsumeBatch
func (qb *QueueBuilder) Consume(ctx, handler MessageHandler) *ConsumeBuilder
func (qb *QueueBuilder) ConsumeBatch(ctx, handler BatchMessageHandler) *ConsumeBuildererr = client.Queue("orders").
Group("billing").
SubscriptionMode("all").
Limit(1).
Each().
Consume(ctx, func(ctx context.Context, msg *queen.Message) error {
fmt.Println(msg.Data)
return nil
}).
Execute(ctx)
if err != nil {
return err
}MessageHandler is func(ctx context.Context, msg *Message) error; BatchMessageHandler is
func(ctx context.Context, msgs []*Message) error. *ConsumeBuilder exposes
Execute(ctx) error and Start(ctx) error (Start is an alias). Both block until every worker
stops; cancel the context to stop them.
Worker loop behaviour:
- A handler error with
AutoAckon nacks the message and the worker continues. - A 429 that escapes the HTTP layer’s own retry backs off and continues.
- A 403 stops the worker. Use
(*HTTPError).IsClusterSuspended()to distinguish the terminal case.
(*Message).Trace
func (m *Message) Trace(ctx context.Context, config TraceConfig) (*TraceResponse, error)TraceConfig carries TraceName string, TraceNames []string, EventType string and
Data map[string]interface{}. Posts to /api/v1/traces.
Buffering
FlushBuffer(ctx) error flushes this builder’s queue/partition buffer. *BufferManager
exposes Add, Flush(ctx, key), FlushAll(ctx), GetStats() BufferStats, GetBuffer(key) and
Clear(). BufferStats carries ActiveBuffers, TotalBufferedMessages, OldestBufferAge and
FlushesPerformed.
The buffer is bounded and lossless under errors. At MaxSize waiting messages (default
4 * MessageCount), Add blocks, honoring its context, until the flusher drains below the
bound. A batch whose POST fails goes back to the front of the buffer, in order, and is retried
every RetryDelayMillis; it is never dropped, though occupancy can overshoot MaxSize by that
one restored batch. A broker outage therefore shows up as blocked producers and a full buffer,
not as silent loss.
Dead-letter queue
DLQ(consumerGroup string) *DLQBuilder.
| Method | Notes |
|---|---|
Limit(count) / Offset(count) |
Paging. |
From(timestamp) / To(timestamp) |
Time filters. |
Get(ctx) (*DLQResponse, error) |
DLQResponse{Messages []Message, Total int} |
The listing is read-only, but Admin.RetryMessage(ctx, partitionID, transactionID) replays.
Since broker 1.6.0 it runs on a move: one transaction claims the newest dead-letter row at
the address under a row lock, pushes the snapshot back into the log and deletes that row. Only
dead-lettered addresses replay, and a live one answers 404.
It is now idempotent by row. The replayed frame carries the deterministic transaction id
dlq:<dead-letter row id>, a second call for the same address answers 404 because the row is
gone, and two concurrent callers serialise on the lock so the loser is told the row is gone
rather than pushing a second copy. Only the addressed consumer group’s record is removed: an
address can carry one row per group, and the others stay where they were. A result:"duplicate"
verdict means nothing was written and nothing was removed, and a 503 means push
maintenance is on, which refuses the move and leaves the row. The client still sends it with
failover retry disabled, because it is a write and a blind resend by the transport would hide a
verdict the caller has to read. Admin.MoveMessageToDLQ is still a stub, because the broker
registers no force-move route.
*TransactionBuilder
client.Transaction(). Pushes and acks in one PostgreSQL transaction, all-or-nothing.
| Method | Notes |
|---|---|
Ack(messages interface{}, status string, opts AckOptions) *TransactionBuilder |
Same message forms as Queen.Ack. A LeaseID is collected into requiredLeases. |
Queue(name string) *TransactionQueueBuilder |
Sub-builder with Partition(name) and Push(payload) *TransactionBuilder. Pass a PushItem to control that item’s TransactionID, TraceID and Partition; pass anything else and it is treated as a bare payload with a minted id. |
Commit(ctx) (*TransactionResponse, error) |
POST /api/v1/transaction. Check resp.Success as well as err. |
messages, err := client.Queue("orders").
Group("invoicing").
SubscriptionMode("all").
Batch(1).
Wait(true).
Pop(ctx)
if err != nil {
return err
}
message := messages[0]
_, err = client.Transaction().
Queue("invoices").
Push(map[string]any{"orderId": message.Data["orderId"], "invoiced": true}).
Ack(message, queen.AckStatusCompleted, queen.AckOptions{ConsumerGroup: "invoicing"}).
Commit(ctx)
if err != nil {
return err
}The two operations can be given in either order: the builder collects them and Commit sends
one request.
A transaction is atomic, not exactly-once end to end; see the limit.
*Admin
client.Admin(). Every method takes a context and returns map[string]interface{}, the
broker’s JSON body verbatim, except where noted.
Resources
| Method | Route |
|---|---|
GetOverview(ctx) |
GET /api/v1/resources/overview |
GetNamespaces(ctx) |
GET /api/v1/resources/namespaces |
GetTasks(ctx) |
GET /api/v1/resources/tasks |
ListQueues(ctx, params ListQueuesParams) |
GET /api/v1/resources/queues |
GetQueue(ctx, name) |
GET /api/v1/resources/queues/:name |
GetPartitions(ctx, queueName) |
GET /api/v1/status/queues/:queue?includePartitions=true |
ListQueuesParams carries Namespace, Task, Limit, Offset. GetPartitions requires a
queue name and returns the queue-detail payload: the broker bundles partition data there rather
than exposing a partitions collection.
Messages, DLQ and traces
| Method | Route |
|---|---|
ListMessages(ctx, params ListMessagesParams) |
GET /api/v1/messages |
GetMessage(ctx, partitionID, transactionID) |
GET /api/v1/messages/:partitionId/:transactionId |
DeleteMessage(ctx, partitionID, transactionID) |
DELETE /api/v1/messages/:partitionId/:transactionId |
ListDLQ(ctx, params ListDLQParams) |
GET /api/v1/dlq |
GetTraceNames(ctx, limit, offset) |
GET /api/v1/traces/names |
GetTracesByName(ctx, traceName, limit, offset) |
GET /api/v1/traces/by-name/:traceName |
GetTracesForMessage(ctx, partitionID, transactionID) |
GET /api/v1/traces/:partitionId/:transactionId |
ListMessagesParams carries Queue, Partition, Status, ConsumerGroup, Limit, Offset.
ListDLQParams carries Queue, ConsumerGroup, Partition, From, To, Limit, Offset.
Status and analytics
| Method | Route |
|---|---|
GetStatus(ctx, params GetStatusParams) |
GET /api/v1/status |
GetQueueStats(ctx, namespace, task) |
GET /api/v1/status/queues |
GetQueueDetail(ctx, name, includePartitions bool) |
GET /api/v1/status/queues/:name |
GetAnalytics(ctx, from, to) |
GET /api/v1/status/analytics |
GetQueueLagAnalytics(ctx, from, to) |
GET /api/v1/analytics/queue-lag |
GetQueueOpsAnalytics(ctx, from, to) |
GET /api/v1/analytics/queue-ops |
GetQueueParkedReplicas(ctx, from, to) |
GET /api/v1/analytics/queue-parked-replicas |
GetRetentionAnalytics(ctx, from, to) |
GET /api/v1/analytics/retention |
GetSystemMetrics(ctx, from, to) |
GET /api/v1/analytics/system-metrics |
GetWorkerMetrics(ctx, from, to) |
GET /api/v1/analytics/worker-metrics |
GetPostgresStats(ctx) |
GET /api/v1/analytics/postgres-stats |
GetStatusParams carries Queue, Namespace, Task.
Consumer groups
| Method | Route |
|---|---|
ListConsumerGroups(ctx) |
GET /api/v1/consumer-groups |
GetConsumerGroup(ctx, name) |
GET /api/v1/consumer-groups/:name |
GetLaggingConsumers(ctx, minLagSeconds) |
GET /api/v1/consumer-groups/lagging?minLagSeconds= |
DeleteConsumerGroupForQueue(ctx, group, queue, deleteMetadata) |
DELETE /api/v1/consumer-groups/:group/queues/:queue |
SeekConsumerGroup(ctx, group, queue, opts SeekConsumerGroupOptions) |
POST /api/v1/consumer-groups/:group/queues/:queue/seek |
SeekConsumerGroupPartition(ctx, group, queue, partition, opts) |
POST /api/v1/consumer-groups/:group/queues/:queue/partitions/:partition/seek |
RefreshConsumerStats(ctx) |
POST /api/v1/stats/refresh |
SeekConsumerGroupOptions carries Timestamp string (RFC3339) and ToEnd bool, which are
mutually exclusive. The wire body is {"toEnd": true} or {"timestamp": "…"}.
System
| Method | Route | Returns |
|---|---|---|
Health(ctx) |
GET /health |
map |
Metrics(ctx) |
GET /metrics |
(string, error): the raw body when it is not JSON, "" when it parsed as JSON |
PrometheusMetrics(ctx) |
GET /metrics/prometheus |
(string, error) |
GetMaintenanceMode(ctx) |
GET /api/v1/system/maintenance |
map |
SetMaintenanceMode(ctx, enabled) |
POST /api/v1/system/maintenance |
map |
GetPopMaintenanceMode(ctx) |
GET /api/v1/system/maintenance/pop |
map |
SetPopMaintenanceMode(ctx, enabled) |
POST /api/v1/system/maintenance/pop |
map |
Errors
Non-2xx responses come back as *HTTPError:
var he *queen.HTTPError
if errors.As(err, &he) && he.StatusCode == 429 { /* ... */ }| Field | Notes |
|---|---|
StatusCode int |
HTTP status. |
Body string |
Raw body. |
Code string |
The body’s code field, empty when the body has none; the proxy’s stable values are listed under Behind the proxy. |
RetryAfterSeconds *float64 |
Parsed from the Retry-After header on a 429; nil when absent or non-numeric. |
IsClusterSuspended() reports the terminal 403 that no amount of retrying resolves. Error()
renders as HTTP <status> [<code>]: <body>.
A 204 response yields a nil map:
a 204 carries no body at all.
*HttpClient
Reachable via GetHttpClient() when you need a route the SDK does not wrap.
| Method | Notes |
|---|---|
Get(ctx, path string, timeoutMs int, affinityKey string, opts ...RequestOption) |
timeoutMs of 0 uses the client default. |
Post(ctx, path, body interface{}, opts ...RequestOption) |
|
PostWithAffinity(ctx, path, body, affinityKey, opts ...RequestOption) |
|
Delete(ctx, path, opts ...RequestOption) |
|
GetLoadBalancer() |
|
Close() |
All four return (map[string]interface{}, error); a top-level JSON array is wrapped as
{"data": [...]}. WithLongPollRetry() is the only RequestOption: it marks the request as a
long-poll pop so a 429 backs off indefinitely instead of using the bounded budget.
Logging
Set by the QUEEN_CLIENT_LOG environment variable, read once at package init:
| Value | Level |
|---|---|
| unset | LogLevelNone |
debug, true, 1 |
LogLevelDebug |
info |
LogLevelInfo |
warn, warning |
LogLevelWarn |
error |
LogLevelError |
| anything else | LogLevelNone |
SetLogLevel(level LogLevel) and GetLogLevel() LogLevel change it at runtime.
Helpers
| Function | Notes |
|---|---|
GenerateUUID() string |
The id used for transactionId. |
GenerateUUIDv4() string, GenerateUUIDv7() (string, error) |
Explicit versions. |
ParseUUID(s) (uuid.UUID, error), MustParseUUID(s) uuid.UUID |
|
IsValidUUID(s) bool |
|
ValidateURL(raw) (string, error), ValidateURLs(urls) ([]string, error) |
|
ValidateQueueName, ValidatePartitionName, ValidateConsumerGroup |
(string, error): normalise and reject. |
ValidateBatchSize, ValidateConcurrency, ValidateTimeout |
error. |
ValidateMessage(*Message) error, ValidateMessages([]*Message) error |
Run by Ack before any request. |
Message
| Field | JSON | Notes |
|---|---|---|
TransactionID |
transactionId |
|
PartitionID |
partitionId |
Required for every ack. |
LeaseID |
leaseId |
Empty when the pop used server-side auto-ack. |
Queue, Partition |
queue, partition |
|
Data |
data |
map[string]interface{}. |
CreatedAt |
createdAt |
|
ErrorMessage |
errorMessage |
|
RetryCount |
retryCount |
|
ProducerSub |
producerSub |
The authenticated producer identity, stamped by the broker from the JWT sub at push time. Present only when JWT auth is on. Clients cannot set it. |
Streaming SDK
import "github.com/smartpricing/queen/clients/client-go/streams"
s := streams.From(queen.Queue("orders").AsStreamSource())AsStreamSource() on a *QueueBuilder adapts it to runtime.Source. Every combinator returns a
new *Stream.
| Group | Methods |
|---|---|
| Stateless | Map(MapFn), Filter(FilterFn), FlatMap(FlatMapFn) |
| Keying | KeyBy(KeyFn) |
| Windows | WindowTumbling(seconds float64, opts ...WindowOption), WindowSliding(size, slide float64, opts ...), WindowSession(gap float64, opts ...), WindowCron(every string, opts ...) |
| Reduce | Reduce(ReduceFn, initial interface{}), Aggregate(map[string]ExtractorFn, fieldOrder ...string) |
| Gate | Gate(GateFn) |
| Sink | To(Sink), ToPartitioned(Sink, resolver), Foreach(ForeachFn) |
| Terminal | Run(ctx, opts RunOptions) (*Runner, error), Compile() (*runtime.CompiledStream, error) |
Every WindowX constructor takes the same five WindowOption values, which control when a window
closes and what happens to whatever arrives after it did:
| Option | Default | What it controls |
|---|---|---|
WithGracePeriod(seconds float64) |
0 |
Seconds a window stays open past its own end before it closes, so a straggler still lands in the bucket it belongs to. |
WithIdleFlushMs(ms int64) |
5000, but 1000 for WindowSession and 30000 for WindowCron |
How often the runner sweeps partitions that have gone quiet for windows that are ripe to close. 0 disables the sweep, and a window on a silent partition then stays open until traffic returns. |
WithEventTime(fn EventTimeFn) |
none | Switches bucketing from the broker’s createdAt to your own timestamp, and turns on a per-partition watermark. |
WithAllowedLateness(seconds float64) |
0 |
Event-time mode only. Seconds of out-of-orderness tolerated before an event is called late, measured against that partition’s watermark. |
WithOnLate(policy string) |
"drop" |
What happens to an event older than the watermark minus the allowed lateness. "drop" is the only value that keeps emits exact. "include" accumulates it anyway, which for an already-flushed window recreates the state row and produces a second emit for it. |
To(sink) reuses the source partition name for each push; ToPartitioned(sink, resolver)
chooses the destination partition instead, resolver being a fixed partition string or a
func(value interface{}) string called per emit (anything else panics), the mirror of the
JavaScript .to(queue, { partition }) form. The resolver is not part of the config hash, so
switching between the two, or changing the resolver, neither trips the registration mismatch
check nor resets query state.
streams/helpers provides TokenBucketGate(TokenBucketGateOptions) and
SlidingWindowGate(SlidingWindowGateOptions), both returning a GateFn.
RunOptions
| Field | Default |
|---|---|
QueryID |
required, the durable identity |
URL |
required, the broker base URL for the /streams/v1/* calls |
BearerToken |
"" |
BatchSize |
200 |
MaxPartitions |
4 |
MaxWaitMillis |
1000 |
SubscriptionMode |
"" |
SubscriptionFrom |
"" |
Conflation |
false. Requests last-value delivery on the source pop; the reducer only ever sees each partition’s newest message, so messages conflation skips never reach it. See conflation |
Reset |
false (wipe state on a config-hash mismatch) |
ConsumerGroup |
streams.<QueryID> |
Logger |
nil |
*Runner exposes Metrics() Metrics with the counters CyclesTotal, FlushCyclesTotal,
MessagesTotal, PushItemsTotal, StateOpsTotal, LateEventsTotal, ErrorsTotal,
GateAllowsTotal, GateDenialsTotal and LastError.
The chain shape is fingerprinted into a config hash; re-deploying a different chain under the
same QueryID is rejected at registration unless Reset is set. Only operator kinds and their
structural config are hashed, not the bodies of your functions.
Same surface, other languages
Parity here is a claim about the surface, not about shared code: each client is written natively
in its own idiom. These sixteen rows are that surface, read out of clients/ on this tree. Six
of them are full parity across all six clients; the other ten are where a client stops.
| Capability | JavaScript | Python | Go | Rust | PHP | C++ |
|---|---|---|---|---|---|---|
| Push, pop, ack, multi-partition claim | yes | yes | yes | yes | yes | yes |
| Client-side push buffering | yes | yes | yes | yes | yes | yes |
Transactions through POST /api/v1/transaction |
yes | yes | yes | yes | yes | yes |
| Dead-letter reader | yes | yes | yes | yes | yes | yes |
| HTTP 429 policy, separate from the retry counter | yes | yes | yes | yes | yes | yes |
kv and timers riders on a transaction |
yes | yes | yes | yes | yes | yes |
| Key/value: the seven operations | yes | yes | yes | yes | yes | five of seven |
Timers: schedule, cancel, peek, list |
yes | yes | yes | yes | yes | two of four |
once, the idempotency marker in one call |
yes | yes | no | no | no | no |
Admin facade |
yes | yes | yes | yes | yes | no |
affinity load-balancing strategy |
yes | yes | yes | yes | yes | no |
Streams DSL over /streams/v1/* |
yes | yes | yes | yes | no | no |
| Lease renewal inside the consume loop | yes | yes | yes | yes | yes | no |
Per-message trace helper |
yes | yes | yes | no | yes | no |
Host override independent of the address dialled |
yes | no | no | yes | no | no |
pop reports a failure instead of an empty result |
no | no | yes | yes | yes | no |
The ten divergent rows, with their conditions:
- Key/value. The seven operations are
get,getMany,getPrefix,put,putIfAbsent,deleteandincr. C++ wraps five of them:getManyandgetPrefixhave no method, and both remain reachable throughPOST /api/v1/kvon the sharedHttpClient.getPrefixinside a transaction is refused by the broker in every client, which is its rule and not a client’s gap. - Timers. C++ has
scheduleandcancelonly;peekandlistare reads and stay onGET /api/v1/timers/{queue}[/{timerKey}]. There is no reschedule operation anywhere, becausescheduleis the upsert andstatusreports which happened; Python, Go, Rust and PHP still spell areschedulealias over it, JavaScript and C++ do not. once. JavaScript (kv.once,transaction().once) and Python (kv.once,transaction().once) foldputIfAbsentplusrequiredinto the question people actually ask, “did I win?”. The other four write theputIfAbsentthemselves, which is the same wire and one line longer.kvon a streams operator context. No client has it, so it is not a row. Inside a stream the state primitive isstate_ops, which commits with the sink and the ack in the cycle’s own transaction; a key/value write from an operator would not, and that atomicity is the thing the stream already gives you for free.Adminfacade. C++ has noAdminclass. Every management and observability route is still reachable, through the sharedHttpClientreturned byget_http_client().affinity. The C++LoadBalancerimplements round-robin and"session"only, andload_balancing_strategy = "affinity"falls through to round-robin silently. The strategy matters because it keeps one consumer’s pops on one backend, which is what keeps two clients from contending on the same partition claim.- Streams. The
Streambuilder, the four window kinds, gates and the/streams/v1/*runtime are in the JavaScript, Python, Go and Rust clients. PHP and C++ have none of it. - Lease renewal. In C++,
renew_lease()sets a flag the consumer loop does not act on: the worker carries an unimplemented placeholder where the timer would go. Callclient.renew(message)yourself before the lease expires. - Per-message
trace. Where it exists it is attached by the consume loop, so a message returned by a manualpop()never carries it. Rust records a trace throughAdmin::record_traceinstead of hanging a method off the message. C++ cannot hang one off a JSON object at all, and its trace hook is an explicit no-op. Hostoverride. JavaScript (hostHeader) and Rust (host_header) advertise a request authority, and a TLS SNI, independent of the address the socket dials. That is what addresses a named tenant cluster behind a shared proxy endpoint. The other four clients send the dialled address.popfailures. Go, Rust and PHP surface a 4xx, an exhausted 429 budget, a terminal 403 or a network fault to the caller, so an empty result means an empty queue. JavaScript, Python and C++ log the failure and return an empty result, which does not distinguish an empty queue from revoked credentials.
The go suite in the test matrix runs the root-package unit
tests, then ./tests/ and ./tests/streams_integration/ with -count=1 so a cached result can
never hide a live-broker failure, on the single, ha and tenanted topologies. The
./streams/... package is green at 13 of 13 on this tree, with one known deterministic failure
named on that page.
JavaScript
Thenable builders, client-side auto-ack on consume.
Python
Async client with the same builder chain, snake_case.
Rust
Async on tokio, broker wire types, full streams runtime.
PHP
Synchronous Guzzle client with Laravel integration.
C++
Single-header, thread-pool consumers.
queenctl
A CLI built on this package.