Expand description
Multi-producer, multi-consumer oneshot notification channel
Notifier
is an asynchronous, multi-producer, multi-consumer, oneshot channel satisfying
three additional requirements:
- Dropped receivers do not consume resources.
- Messages are not copied for receivers who don’t want them.
- Minimal resource contention for concurrent subscriptions.
§Dropped receivers do not consume resources
This requirement is a direct prerequisite of the broader requirement that passive requests for resources do not consume resources. This is important because in general, passive requests may be for resources that are not guaranteed to exist, and thus may never terminate. Just like we avoid spawning a task for passive requests, since it may never complete, we need receivers for passive requests not to persist beyond the lifetime of the request, or they may never be closed.
This requirement is implemented via garbage collection: each time a message is sent, resources belonging to dropped receivers are cleaned up. Thus, strictly speaking, dropped receivers do consume resources, but only briefly. There is no need to keep them around until the desired message is delivered, for example.
§Messages are not copied for receivers who don’t want them.
The second requirement simplifies the higher level fetching logic by allowing us to maintain a single channel for all notifications about a particular resource type, rather than separate channels for each specific request. Since messages are not copied for all subscribers, but only for the subscribers interested in a particular message, this simplification becomes nearly cost-free.
This requirement is implemented by attaching a predicate to each subscription, which takes a message by reference. The predicate is checked on the sending side, and the message is only copied to the subscription if the predicate is satisfied.
§Minimal resource contention for concurrent subscriptions.
This is important because subscriptions are requested in response to read-only client requests, which are supposed to run in parallel as much as possible. By contrast, notifications are usually send from internal server tasks (e.g. the background task that updates the data source when new blocks are committed). It is less of a problem if these internal tasks contend with each other, because they are not directly blocking responses to clients, and we have more control over how and when they acquire shared resources.
This requirement also empowers us to create a simpler design for the high-level fetching logic.
Specifically, we can reuse the same code for fetching individual resources as we use for
long-lived subscription streams (e.g. subscribe_blocks
is a thin wrapper around
get_block_range
). We do not have to worry about adding complex logic to reuse notification
subscriptions for long-lived streams, because subscribing anew for each entry in the stream has
low overhead in terms of contention over shared resources – the dominant caused in any
concurrent channel, after data copying (see above).
This further lets us simplify the interface of this channel a bit: since all notifications are oneshot, consumers deal with futures rather than streams.
This requirement is satisfied by maintaining the list of subscribers to a Notifier
in a way
that moves most resource contention to message senders, rather than receivers. We make the
assumption that there is less concurrency among senders. In the common case, there is just one
sender: the task monitoring HotShot for new blocks. Occasionally, there may be other tasks
spawned to fetch missing resources and send them through the Notifier
, but these should be
relatively few and rare.
Structs§
- Notifier
- Multi-producer, multi-consumer oneshot notification channel
- Receive
Handle 🔒 - A handle that closes a subscriber when dropped.
- Subscriber 🔒
- WaitFor
- A pending request for notification.
Traits§
- Predicate
- A predicate on a type
<T>
.