Complete API reference for the CrazyGoat\RabbitStream\Client\Producer class.
namespace CrazyGoat\RabbitStream\Client;
class Producer
{
// Constructor and internal methods...
public function send(string $message, ?float $timeout = null): void;
public function sendWithFilter(string $message, ?string $filterValue, ?float $timeout = null): void;
public function sendBatch(array $messages, ?float $timeout = null): void;
public function close(): void;
public function isClosed(): bool;
public function waitForConfirms(float $timeout = 5.0): void;
public function getLastPublishingId(): ?int;
public function querySequence(): int;
public function getPendingConfirms(): int;
public function getLostConfirmCount(): int;
public function isStale(): bool;
public function getRedeclareCount(): int;
}The Producer class is instantiated via Connection::createProducer(). Direct instantiation is not recommended.
$producer = $connection->createProducer(
string $stream, // Required: Stream name
?string $name = null, // Optional: Producer name for deduplication
?callable $onConfirm = null, // Optional: Confirmation callback
int $maxPendingConfirms = Producer::DEFAULT_MAX_PENDING_CONFIRMS, // Optional
float $redeclareTimeout = Producer::DEFAULT_REDECLARE_TIMEOUT, // Optional
float $closeConfirmDrainTimeout = Producer::DEFAULT_CLOSE_CONFIRM_DRAIN_TIMEOUT, // Optional
): ProducerInterface| Parameter | Type | Required | Description |
|---|---|---|---|
$stream |
string |
Yes | Name of the stream to publish to |
$name |
?string |
No | Unique producer name for deduplication. If provided, the producer reads back its publishing sequence on creation and resumes from it, so a reconnect/restart continues above the broker's stored sequence instead of replaying it. |
$onConfirm |
?callable |
No | Callback invoked for each publish confirmation. Receives ConfirmationStatus object. |
$maxPendingConfirms |
int |
No | Hard back-pressure cap on outstanding (unconfirmed) publishes; default 10000. Sends drain confirms until there is room for the next publish or the entire batch. A batch larger than a positive cap is rejected with InvalidArgumentException; 0 disables the cap (unlimited behavior). See Performance Tuning. |
$redeclareTimeout |
float |
No | How long (seconds) a publish keeps retrying DeclarePublisher after a MetadataUpdate dropped the publisher; default 5.0. 0 fails on the first attempt. Must be >= 0. See isStale(). |
$closeConfirmDrainTimeout |
float |
No | How long close() waits for in-flight publish confirms before abandoning them; default 2.0 seconds. Must be >= 0. |
The underlying Producer constructor also takes an optional $onClose callback (called with the publisher id once close() has run, so the owning Connection can reclaim it) and an optional $logger (LoggerInterface, defaults to NullLogger); Connection supplies both, so they are not part of createProducer().
Anonymous producer (no deduplication):
$producer = $connection->createProducer('my-stream');Named producer with confirm callback:
use CrazyGoat\RabbitStream\Client\ConfirmationStatus;
$producer = $connection->createProducer(
'my-stream',
name: 'my-producer',
onConfirm: function (ConfirmationStatus $status) {
if ($status->isConfirmed()) {
echo "Confirmed: #{$status->getPublishingId()}\n";
} else {
echo "Failed: #{$status->getPublishingId()}\n";
}
}
);Publish a single message to the stream.
public function send(string $message, ?float $timeout = null): void| Parameter | Type | Required | Description |
|---|---|---|---|
$message |
string |
Yes | Message body as string |
$timeout |
?float |
No | Socket write timeout in seconds. Null uses connection default. |
void
ConnectionException- If the socket is not connected or the write/read failsDeserializationException- If a response or server-push frame read while draining back-pressure cannot be deserializedInvalidArgumentException- If the serializedPublishrequest exceeds the negotiated outgoing frame sizeProtocolException- If the broker rejects a re-declare or sends an unexpected frameTimeoutException- If the write, a re-declare, or themaxPendingConfirmsback-pressure drain times out
$producer->send('Hello, World!');
$producer->send('Urgent message', timeout: 1.0);$messageis a plain payload string; it is automatically wrapped in a single AMQP 1.0 Data section on the wire, so a consumer'sMessage::getBody()returns the same string unchanged- To publish pre-encoded AMQP 1.0 bytes, use the low-level API with
AmqpMessageEncoder::encodeDataSection()—send()would wrap them again - Messages are assigned an auto-incrementing publishing ID internally
- The message is not immediately confirmed; use
waitForConfirms()or theonConfirmcallback - For high throughput, consider
sendBatch()instead
Publish multiple messages in a single batch.
/**
* @param string[] $messages
*/
public function sendBatch(array $messages, ?float $timeout = null): void| Parameter | Type | Required | Description |
|---|---|---|---|
$messages |
array<string> |
Yes | Array of message strings |
$timeout |
?float |
No | Socket write timeout in seconds |
void
ConnectionException- If the socket is not connected or the write/read failsDeserializationException- If a response or server-push frame read while draining back-pressure cannot be deserializedInvalidArgumentException- If the serializedPublishrequest exceeds the negotiated outgoing frame sizeProtocolException- If the broker rejects a re-declare or sends an unexpected frameTimeoutException- If the write, a re-declare, or themaxPendingConfirmsback-pressure drain times out
$messages = [
'Message 1',
'Message 2',
'Message 3',
];
$producer->sendBatch($messages);- More efficient than multiple
send()calls for high throughput - All messages in the batch share the same network frame
- Each message is a plain payload string, automatically wrapped in an AMQP 1.0 Data section (same encoding as
send()) - Each message still gets its own publishing ID and confirmation
- Empty arrays are silently ignored (no-op)
Publish a single message tagged with a stream filtering value.
public function sendWithFilter(string $message, ?string $filterValue, ?float $timeout = null): void| Parameter | Type | Required | Description |
|---|---|---|---|
$message |
string |
Yes | Message body as string, same encoding as send() (wrapped in an AMQP 1.0 Data section on the wire) |
$filterValue |
?string |
Yes | Stream filtering value attached to the message. null publishes an unfiltered message (still contributes to the chunk's bloom filter as "no value") |
$timeout |
?float |
No | Socket write timeout in seconds; null uses connection default |
void
ConnectionException- If the socket is not connected or the write/read failsDeserializationException- If a response or server-push frame read while draining back-pressure cannot be deserializedInvalidArgumentException- If the serialized request exceeds the negotiated outgoing frame sizeProtocolException- If$filterValueis not null but the broker did not negotiatePublishv2 (stream filtering requires RabbitMQ 3.13+), or a re-declare is rejectedTimeoutException- If the write, a re-declare, or themaxPendingConfirmsback-pressure drain times out
$producer->sendWithFilter(json_encode(['region' => 'eu', 'order_id' => 1]), filterValue: 'eu');
$producer->sendWithFilter(json_encode(['region' => 'us', 'order_id' => 2]), filterValue: 'us');- With a non-null
$filterValuethe message is sent as aPublishRequestV2/PublishedMessageV2frame carrying the per-message filter value. With a null value the plain v1 frame is sent instead (v1 has no filter field); a non-null value on a broker that never negotiated Publish v2 is a hardProtocolExceptionrather than a silent unfiltered publish - Filtering is broker-side and chunk-granular: the broker maintains a bloom filter per chunk and delivers the whole chunk once its filter may contain a matching value — every message in a delivered chunk arrives, matching or not. A consumer must still filter application-side for exact matching; see
$filterValues/$matchUnfilteredonConsumerand the Stream Filtering guide - Each message still gets its own publishing ID and confirmation, exactly like
send()
Close the producer and release server resources.
public function close(): voidNone
void
ConnectionException- If the socket is not connected or theDeletePublisherwrite/read failsDeserializationException- If a response or server-push frame read while draining confirms cannot be deserializedProtocolException- If theDeletePublisherresponse has an unexpected version or commandTimeoutException- If theDeletePublisherresponse does not arrive in time
try {
$producer->send('Final message');
$producer->waitForConfirms(timeout: 5.0);
} finally {
$producer->close();
}- Sends
DeletePublishercommand to server - Unregisters the publisher from the connection
- Frees the publisher ID for reuse
- Does not close the underlying connection
- Safe to call multiple times (idempotent)
- Keeps the confirm callback registered through the
DeletePublisherexchange and drains any still-in-flight confirms (bounded); anything not drained in time is counted bygetLostConfirmCount()
Whether close() has already run.
public function isClosed(): boolReturns true once close() has been called, even when the DeletePublisher exchange failed (the publisher id is released either way). Always false before the first close(), including after a waitForConfirms() timeout.
Block until all pending publish confirmations are received.
public function waitForConfirms(float $timeout = 5.0): void| Parameter | Type | Required | Description |
|---|---|---|---|
$timeout |
float |
No | Maximum time to wait in seconds. Default: 5.0 |
void
TimeoutException- If timeout is reached before all confirms receivedConnectionException- If the socket is not connected or a read failsDeserializationException- If aPublishConfirm/PublishErroror other frame read while waiting cannot be deserializedProtocolException- If a server-push frame read while waiting has an unexpected version or command
// Publish messages
for ($i = 0; $i < 100; $i++) {
$producer->send("Message {$i}");
}
// Wait up to 10 seconds for all confirms
try {
$producer->waitForConfirms(timeout: 10.0);
echo "All messages confirmed!\n";
} catch (TimeoutException $e) {
echo "Timeout: {$e->getMessage()}\n";
}- Returns immediately if there are no pending confirms
- Internally calls
connection->readLoop()to process incoming frames - The
onConfirmcallback is invoked during this call - Does not guarantee message delivery if connection drops during wait
Get the last publishing ID that was used.
public function getLastPublishingId(): ?intNone
?int - The last publishing ID used. For an anonymous producer this is null
until the first send(); a named producer can return a non-null value
before any send() (see Notes).
// Anonymous producer
$producer->send('Message 1');
$id1 = $producer->getLastPublishingId(); // 0
$producer->send('Message 2');
$id2 = $producer->getLastPublishingId(); // 1
// Batch publishing
$producer->sendBatch(['A', 'B', 'C']);
$id3 = $producer->getLastPublishingId(); // 4 (1 + 3 messages)- Returns the ID of the last message that was sent, not necessarily confirmed
- Counter-intuitive: for a named producer this can be non-null before any
send(). The constructor runsquerySequence()and resumes fromsequence + 1, sogetLastPublishingId()immediately returns the broker's last confirmed sequence (0when the broker stored nothing) rather thannull. It isnullonly for an anonymous producer that has not sent yet. - For named producers, this is automatically managed based on
querySequence() - Publishing IDs start at 0 for unnamed producers (the first
send()uses id0, sogetLastPublishingId()isnullbefore it and0after) - Publishing IDs start at
querySequence() + 1for named producers, which is why a named producer's firstsend()usesquerySequence() + 1and dedup resumes correctly after a reconnect
Get the number of publishes sent but not yet confirmed or errored.
public function getPendingConfirms(): intNone
int - Current count of outstanding (unconfirmed) publishes
$producer->send('Message 1');
$producer->send('Message 2');
echo $producer->getPendingConfirms(); // 2
$producer->waitForConfirms();
echo $producer->getPendingConfirms(); // 0- Decremented as
onConfirm/publish-error frames arrive, whether observed viawaitForConfirms(), theonConfirmcallback, or themaxPendingConfirmsback-pressure drain insend()/sendBatch() - Incremented only once the frame has actually been written: a
send()that throws leaves the count (and the publishing ID) untouched, so a laterwaitForConfirms()is never blocked by a message the broker never received - Tracked per publishing id, not as a bare counter (since #521): a duplicate or late
PublishConfirm/PublishErrorfor an id that has already been retired is ignored, sowaitForConfirms()cannot return while messages are still unconfirmed and aMetadataUpdatethat makes the producer stale (isStale()returnstrue) reports exactly the still-outstanding ids as failed - Tracked as a set, so bookkeeping is O(outstanding) rather than O(1): with
maxPendingConfirms: 0it grows until confirms are drained (see Performance Tuning) - Useful for custom throttling or metrics alongside
maxPendingConfirms
Cumulative number of publishes whose confirms were abandoned by the bounded
close() drain timeout (since #522).
public function getLostConfirmCount(): intNone
int - Total number of publishes lost to drain timeouts over this producer's
lifetime (0 in normal operation)
$producer->send('Message 1');
$producer->close();
if ($producer->getLostConfirmCount() > 0) {
// The broker stopped confirming before the 2 s drain expired: these
// publishes may or may not have reached the broker.
}close()waits up to 2 s (CLOSE_CONFIRM_DRAIN_TIMEOUT) for in-flightPublishConfirm/PublishErrorframes. Anything still outstanding when that expires can never be confirmed, because the publisher id is released — the confirms are lost. Previously this happened silently (#522).- Each timeout also emits a
warninglog line naming the producer and stream, the number of affected publishing ids, and a bounded prefix of them (at most 10 ids in the log context, regardless of how many were outstanding), so operators can tell "the broker stopped confirming" from "everything drained". - The counter is not reset by
close();close()is idempotent, so it cannot double-count. - The stranded ids are still visible through
getPendingConfirms()afterclose(). waitForConfirms()timing out does not increment this counter: those confirms belong to a still-open producer and can still arrive.
public function isStale(): boolWhether the broker has dropped this publisher — it pushed a MetadataUpdate
for the stream, or answered a publish with PUBLISHER_NOT_EXIST /
STREAM_NOT_AVAILABLE. The next send(), sendBatch() or sendWithFilter()
re-runs DeclarePublisher before publishing, retrying with exponential
back-off for up to $redeclareTimeout seconds and throwing a
ProtocolException if the stream is still gone.
Unconfirmed messages are lost when this happens: they are reported to the
onConfirm callback as a failed ConfirmationStatus carrying the broker's
response code, and they stop counting toward maxPendingConfirms.
$producer->send('a');
// ... the stream is deleted; any readLoop()/waitForConfirms() dispatches the MetadataUpdate ...
$producer->isStale(); // true
$producer->send('b'); // re-declares first, then publishes
$producer->isStale(); // falseSee Publishing → Stream Deleted or Leader Moved.
public function getRedeclareCount(): intHow many times this publisher has been successfully re-declared after a
MetadataUpdate. Useful as a health metric: a number that keeps growing means
the stream is flapping.
Query the last confirmed publishing ID from the server.
public function querySequence(): intNone
int - The highest publishing ID confirmed by the server for this named producer
InvalidArgumentException- If called on an unnamed producer (anullor""name)ConnectionException- If the socket is not connected or the exchange failsDeserializationException- If the response frame cannot be deserializedProtocolException- If the broker answers with a non-OK response codeTimeoutException- If the response does not arrive in timeUnexpectedResponseException- If the server returns an unexpected response
// Only works with named producers
$producer = $connection->createProducer('my-stream', name: 'my-producer');
// After some publishing and reconnecting...
$lastConfirmed = $producer->querySequence();
echo "Server has confirmed up to ID: {$lastConfirmed}";- Only available for named producers (throws
InvalidArgumentExceptionfor anonymous producers, i.e. anullor""name) - Automatically called during producer construction for named producers, which then resumes from
sequence + 1 - The broker ignores a publish whose ID is ≤ the returned value. The high-level API assigns IDs itself and never lets the application choose one, so it cannot deliberately re-send such an ID; the rule makes the automatic resume safe rather than being an application-facing "resend and it will be deduplicated" feature
- Makes a round-trip to the server
The ConfirmationStatus object is passed to the onConfirm callback.
namespace CrazyGoat\RabbitStream\Client;
class ConfirmationStatus
{
public function isConfirmed(): bool;
public function getPublishingId(): ?int;
public function getErrorCode(): ?int;
}Returns true if the message was successfully stored by the server.
public function isConfirmed(): boolReturns the publishing ID of the message.
public function getPublishingId(): ?intReturns null if the publishing ID is not available (rare).
Returns the error code if the message failed.
public function getErrorCode(): ?intReturns null if the message was confirmed successfully.
Common error codes:
0x02-STREAM_NOT_EXIST- Stream does not exist0x12-PUBLISHER_NOT_EXIST- Publisher ID is invalid0x10-ACCESS_REFUSED- No write permission
See ResponseCodeEnum for all error codes.
For scenarios where you don't need to wait for confirms:
$producer = $connection->createProducer('logs');
// Publish without waiting
$producer->send('Log message 1');
$producer->send('Log message 2');
$producer->send('Log message 3');
// Close (may lose unconfirmed messages)
$producer->close();Pros: Maximum throughput
Cons: No guarantee of durability
For scenarios requiring durability guarantees:
$producer = $connection->createProducer('orders');
// Publish batch
$producer->sendBatch($orderMessages);
// Wait for confirms before proceeding
$producer->waitForConfirms(timeout: 10.0);
// Safe to close - all messages confirmed
$producer->close();Pros: Guaranteed durability
Cons: Lower throughput due to blocking
For scenarios requiring per-message handling:
$confirmed = [];
$failed = [];
$producer = $connection->createProducer(
'events',
onConfirm: function (ConfirmationStatus $status) use (&$confirmed, &$failed) {
$id = $status->getPublishingId();
if ($status->isConfirmed()) {
$confirmed[] = $id;
} else {
$failed[$id] = $status->getErrorCode();
}
}
);
// Publish
for ($i = 0; $i < 100; $i++) {
$producer->send("Event {$i}");
}
// Process other work while waiting...
// ...
// Eventually wait for remaining confirms
$producer->waitForConfirms(timeout: 5.0);
// Handle failures
foreach ($failed as $id => $code) {
echo "Message #{$id} failed with code {$code}\n";
}
$producer->close();Pros: Non-blocking, per-message tracking
Cons: More complex code
For a publish sequence that resumes cleanly after a reconnect or process restart:
$producer = $connection->createProducer(
'payments',
name: 'payment-service-producer',
onConfirm: function (ConfirmationStatus $status) {
if (!$status->isConfirmed()) {
error_log("Payment failed: #{$status->getPublishingId()}");
}
}
);
// Publish; a restart with the same name resumes above the stored sequence
$producer->send(json_encode(['order_id' => 123, 'amount' => 99.99]));
$producer->waitForConfirms(timeout: 5.0);
// If the process restarts and recreates the producer with the same name,
// the constructor queries the broker's last confirmed sequence and continues
// from there, so the client does not replay already-stored messages.Pros: Restart resumes the sequence instead of replaying it
Cons: Slightly higher overhead for sequence tracking
The broker also drops a publish whose ID is ≤ the stored sequence, but the high-level API assigns IDs internally and cannot re-send an old one, so that rule is not an application-level "resend and it will be deduplicated" feature. See Publishing → Named Producers & Deduplication.
-
Use batch publishing for high throughput:
// Good: Single batch $producer->sendBatch($messages); // Bad: Individual sends foreach ($messages as $msg) { $producer->send($msg); }
-
Adjust batch size based on message size:
- Small messages (< 1KB): 100-1000 messages per batch
- Large messages (> 10KB): 10-100 messages per batch
-
Use appropriate timeouts:
// Short timeout for fast failure detection $producer->send($msg, timeout: 0.5);
-
Don't wait for every message:
// Bad: High latency foreach ($messages as $msg) { $producer->send($msg); $producer->waitForConfirms(timeout: 1.0); } // Good: Lower latency foreach ($messages as $msg) { $producer->send($msg); } $producer->waitForConfirms(timeout: 5.0);
-
Use async confirms for non-critical messages:
$producer = $connection->createProducer('logs'); // No onConfirm callback, no waitForConfirms
use CrazyGoat\RabbitStream\Exception\TimeoutException;
use CrazyGoat\RabbitStream\Exception\ConnectionException;
try {
$producer = $connection->createProducer('my-stream');
$producer->send('Message');
$producer->waitForConfirms(timeout: 5.0);
} catch (TimeoutException $e) {
// Some messages not confirmed within timeout
// May or may not be stored - check application logic
echo "Timeout: {$e->getMessage()}\n";
} catch (ConnectionException $e) {
// Connection lost - may need to reconnect
echo "Connection lost: {$e->getMessage()}\n";
} finally {
$producer?->close();
}function publishWithRetry($connection, $stream, $message, $maxRetries = 3) {
$attempts = 0;
while ($attempts < $maxRetries) {
try {
$producer = $connection->createProducer($stream);
$producer->send($message);
$producer->waitForConfirms(timeout: 5.0);
$producer->close();
return true;
} catch (\Exception $e) {
$attempts++;
if ($attempts >= $maxRetries) {
throw $e;
}
usleep(100000 * $attempts); // Exponential backoff
}
}
return false;
}