Module notifier

Source
Expand description

Multi-producer, multi-consumer oneshot notification channel

Notifier is an asynchronous, multi-producer, multi-consumer, oneshot channel satisfying three additional requirements:

  1. Dropped receivers do not consume resources.
  2. Messages are not copied for receivers who don’t want them.
  3. 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
ReceiveHandle 🔒
A handle that closes a subscriber when dropped.
Subscriber 🔒
WaitFor
A pending request for notification.

Traits§

Predicate
A predicate on a type <T>.