Awaits for the specified number of consumers to be connected.
Awaits for the specified number of consumers to be connected.
This is an utility to ensure that a certain number of consumers are connected before we start emitting events.
is a number indicating the number of consumers that need to be connected before the returned task completes
a task that will complete only after the required number of consumers are observed as being connected to the channel
Create a ConsumerF value that can be used to consume events from the channel.
Create a ConsumerF value that can be used to consume events from the channel.
Note in case multiple consumers are created, all of them will see the
events being pushed, so a broadcasting setup is possible. Also multiple
workers can consumer from the same ConsumerF
value, to share the load.
The returned value is a Resource, because a consumer can be unsubscribed from the channel, with its internal buffer being garbage collected.
consumeWithConfig for fine tuning the internal buffer of the created consumer
Version of consume that allows for fine tuning the underlying buffer used.
Version of consume that allows for fine tuning the underlying buffer used.
is configuration for the created buffer, see ConsumerF.Config for details
Stops the channel and sends a halt event to all current and future consumers.
Stops the channel and sends a halt event to all current and future consumers.
Consumers will receive a Left(e)
event after halt is observed.
Note that if multiple halt
events happen, then only the first one
will be taken into account, all other halt
messages are ignored.
Publishes an event on the channel.
Publishes an event on the channel.
If the internal buffer is full, it asynchronously waits until the operation succeeds, or until the channel is halted.
If the channel has been halted (via halt), then nothing gets
published, the function eventually returning a false
value, which
signals that no more values can be published on the channel.
import cats.implicits._ import cats.effect.Sync sealed trait Complete object Complete extends Complete def range[F[_]](from: Int, until: Int, increment: Int = 1) (channel: ConcurrentChannel[F, Complete, Int]) (implicit F: Sync[F]): F[Unit] = { if (from != until) channel.push(from).flatMap { case true => range(from + increment, until, increment)(channel) case false => F.unit // we need to stop } else // we're done, close the channel channel.halt(Complete) }
is the message to publish
a boolean that is true
if the value was pushed on the internal
queue and the producer can push more values, or false
if the
channel is halted and cannot receive any more events
Publishes multiple events on the channel.
Publishes multiple events on the channel.
If the channel has been halted (via halt), then the publishing is
interrupted, the function returning a false
value signalling that
the channel was halted and can no longer receive any more events.
import cats.implicits._ import cats.effect.Sync sealed trait Complete object Complete extends Complete def range[F[_]](from: Int, until: Int, increment: Int = 1) (channel: ConcurrentChannel[F, Complete, Int]) (implicit F: Sync[F]): F[Unit] = { channel.pushMany(Range(from, until, increment)).flatMap { case true => channel.halt(Complete) case false => F.unit // was already halted, do nothing else } }
is the sequence of messages to publish on the channel
a boolean that is true
if all the values were pushed on the
internal queue and the producer can push more values, or false
if the channel is halted and cannot receive any more events
ConcurrentChannel
can be used to model complex producer-consumer communication channels.It exposes these fundamental operations:
Example
Unicasting vs Broadcasting vs Multicasting
Unicasting: A communication channel between one producer and one ConsumerF. Multiple workers can share the load of processing incoming events. For example in case we want to have 8 workers running in parallel, you can create one ConsumerF, via consume and then use it for multiple workers. Internally this setup uses a single queue and whatever workers you have will share it.
Broadcasting: the same events can be sent to multiple consumers, thus duplicating the load, as a broadcasting setup can be created by creating and consuming from multiple ConsumerF via multiple calls to consume. Internally each
ConsumerF
gets its own queue and hence messages are duplicated.Multicasting: multiple producers can push events at the same time, provided the channel's type is configured as a MultiProducer.
Back-Pressuring and the Polling Model
When consumers get created via consume, a buffer gets created and assigned per consumer.
Depending on what the BufferCapacity is configured to be, the initialized consumer can work with a maximum buffer size, a size that could be rounded to a power of 2, so you can't rely on it to be precise. See consumeWithConfig for customizing this buffer on a per-consumer basis, or the ConcurrentChannel.withConfig builder for setting the default used in consume.
On push, when the queue is full, the implementation back-pressures until the channel has room again in its internal buffer(s), the task being completed when the value was pushed successfully. Similarly ConsumerF.pull (returned by consume) awaits the channel to have items in it. This works for both bounded and unbounded channels.
For both
push
andpull
, in case awaiting a result happens, the implementation does so asynchronously, without any threads being blocked.Multi-threading Scenario
This channel supports the fine-tuning of the concurrency scenario via ChannelType.ProducerSide (see ConcurrentChannel.withConfig) and the ChannelType.ConsumerSide that can be specified per consumer (see consumeWithConfig).
The default is set to MultiProducer and MultiConsumer, which is always the safe choice, however these can be customized for better performance.
These scenarios are available:
The default is
MPMC
, because that's the safest scenario.Note that in this example, even if we used
SingleConsumer
as the type passed in consumeWithConfig, we can still consume from two ConsumerF instances at the same time, because each one gets its own internal buffer. But you cannot have multiple workers per ConsumerF in this scenario, because this would break the internal synchronization / visibility guarantees.WARNING: default is
MPMC
, however any other scenario implies a relaxation of the internal synchronization between threads.This means that using the wrong scenario can lead to severe concurrency bugs. If you're not sure what multi-threading scenario you have, then just stick with the default
MPMC
.Credits
Inspired by Haskell's Control.Concurrent.ConcurrentChannel, but note that this isn't a straight port — e.g. the Monix implementation has a cleaner, non-leaky interface, is back-pressured and allows for termination (via halt), which changes its semantics significantly.