Ir al contenido
Español

Delivery overview

Esta página aún no está disponible en tu idioma.

By default, tell is fire-and-forget. Messages may be lost (stopped recipient, mailbox overflow, network drops in cluster setups). For workloads where loss is unacceptable, the framework provides reliable delivery via the ProducerController / ConsumerController pair.

ConsumerControllerProducerControllerConsumerControllerProducerControllerbuffer + dispatch to handler...msgackmsg

Adds sequence numbers + acks to the basic tell contract. Producer holds unacked messages; consumer dedups by seq.

For workloads where:

  • Loss is unacceptable — payment instructions, audit records, billing events.
  • Duplicates are tolerable but rare — at-least-once with consumer-side dedup is fine.
  • Order matters within a stream — sequence numbers preserve.

For workloads where loss is acceptable (telemetry, metrics, fire-and-forget UX updates), use plain tell.

The framework’s reliable delivery is at-least-once by default — a message may be redelivered if the producer crashes between sending and receiving the ack.

For effectively-once, the controller already dedupes incoming duplicates per (producerId, seq) — but only within its in-memory lifetime. If you need dedup that survives consumer restarts, persist your own processed-seq alongside the business state in the handler:

new ConsumerController<DeliveryMsg>({
handler: async (body) => {
if (await alreadyProcessed(body.id)) return; // your idempotency check
await this.handle(body);
await markProcessed(body.id);
},
});

The controller’s in-memory dedup handles the common retransmit-before-ack case; your handler’s persisted dedup handles consumer crashes mid-processing. Together they give effectively-once in the durable sense.

ControllerRole
ProducerControllerWraps the sender side — assigns sequence numbers, holds unacked messages, retransmits.
ConsumerControllerWraps the receiver side — orders messages by seq, dedupes, sends acks.

Pair them via a producer-consumer link — each producer talks to one consumer (or N consumers via routing, but the link is 1:1 per stream).

import {
Props,
ProducerController,
ConsumerController,
} from 'actor-ts';
// Consumer side — handler function, auto-ack on resolve:
const consumer = system.spawn(
Props.create(() => new ConsumerController<OrderEvent>({
handler: async (order) => {
await processOrder(order);
},
})),
);
// Producer side — wraps outgoing messages in seq + ack tracking:
const producer = system.spawn(
Props.create(() => new ProducerController<OrderEvent>({
producerId: 'order-producer-1',
consumer,
windowSize: 16,
})),
);
producer.tell({
kind: 'reliable-delivery.send',
body: { orderId: 'o-1', amount: 100 },
});

The framework handles seq assignment, retransmission, ordering — your code handles the business logic + dedup.

Each producer assigns strictly increasing seq numbers, starting at 1:
msg 1, msg 2, msg 3, ...
The consumer sees them IN ORDER (after retransmissions sort out):
msg 1, msg 2, msg 3, ...
Duplicates (due to retransmit) appear with the SAME seq:
msg 1, msg 1 (dup), msg 2, ...
→ consumer dedupes by seq

The seq is per-producer — multiple producers have independent seq spaces.

For full durability across producer / consumer crashes:

// Producer-side: persist the in-flight buffer (the messages
// you've tell'd but haven't seen an ack for) so a crash before
// ack doesn't lose them.
// Consumer-side: persist the processed-seq per producer in
// your handler so a crash mid-processing doesn't re-run a
// completed unit of work.

Persisting on both sides gives end-to-end effectively-once:

  • Producer crashes mid-send → recovers persisted in-flight buffer → resumes retransmitting unacked messages.
  • Consumer crashes mid-process → recovers persisted processed-seq → handler dedupes the redelivery.

Without persistence, recovery resets to zero on both sides — the producer forgets unacked messages and the consumer’s in-memory dedup is gone.

Producer/ConsumerController: in-cluster reliable delivery
Kafka / RabbitMQ / NATS: external broker-mediated delivery

Both achieve at-least-once + dedup-via-seq. Differences:

AspectIn-cluster controllersExternal broker
Operational complexityLow — part of the clusterHigh — separate broker to run
LatencySub-millisecondNetwork + broker overhead
ThroughputBounded by single-actor processingHigher (broker scales independently)
External consumersNo (cluster-internal)Yes
PersistenceVia PersistentActorBuilt into broker

For cluster-internal reliable delivery, use the controllers. For external systems or cross-cluster, use a broker (Kafka, etc.).