Buffered Data Provider and Relay is a thread safe, generic component that can be used in any setup that constitutes of multiple listeners processing the same set of data in their own ways such that there is a single source of input data.
- BuDaPro ingests data from the source and places it in an internal buffer array
- A separate thread is spun up for each consumer from inside the relay, which watches the internal buffer and supplies data to the consumers
- Allows multiple consumers running on different threads (and physical cores) to consume data simultaneously at different indices in the internal array, allowing consumers to run at their own pace
- Use of incremental sequence numbers to inform users regarding any gaps
- The relay sits in between the singular source of raw data and consumers
- A thread of the relay (let's call it the
writer threadfor naming purposes) watches for new raw data, and puts it in the internal array along with asequence numberthat can be used by consumers to spot any gaps in the data processed by them - Thread safety is guaranteed by locks on individual indices of the internal buffer array - there is a shared mutex for each index, with which the writer thread takes up a unique lock and the worker threads for each consumer take up a shared lock
- Whenever a new subscriber wants to subscribe to data, it has to give two function objects -
- A function that takes in the size of next data packet, and returns a pointer to void where data will be put
- A callback function which will be called after data is put into the buffer pointed to by the pointer returned by the other function.
NOTE: The maximum size of a data packet is assummed to be known by consumers, relay and the raw data provider
- For each subscriber, a new worker thread is spun up which watches for new data in the internal buffer, caches it internally when available (so that slow consumers do not block the internal buffer array), then passes it to the consumer.
This is the relay class. It has an internal struct type called internal_buffer which holds a pointer to a buffer, the size of the current data packet and the internal sequence number. All atomics, mutexes and condition variables used in the writer thread and the consumer worker threads are contained inside it.
- The
startfunction is called when the operations have to be started. It is passed a functionget_network_datawhich is blocked on, to get new data.startspins up a new thread which runsraw_source_and_internal_queue_coordinatorwhich is passedget_network_dataand a buffer pointer which is allocated withinstart get_network_datashould take in a buffer (void *) and populate it with data, and return the number of bytes written- The
raw_source_and_internal_queue_coordinatorfunction runs a loop inside which it checks for new data by callingget_network_data, then copies that data into the buffer it was given, along with the size and sequence number. It returns when the relay is stopped. stopjust sets an atomic boolean flag which is checked by the writer thread and consumer worker threads to stop working.subscribe_to_mdshould be called by a consumer to subscribe itself to data.subscribe_to_mdrequiresget_next_data_loc_fn-> called with asize_targument to get a pointer to a buffer where the worker thread will place data. The worker thread blocks on it to get the buffer pointerdata_copied_cb-> a callback called by the worker thread to indicate that data has been produced.
- The design has been made in such a way that the worker threads act as proxies of consumers. Consumers can offload all processing to these threads via
data_copied_cb. subcribe_to_mdspins up a new thread with a lambda that watches the internal buffer array, copies data into a local buffer, checks for gaps, then gets location of the consumer's buffer and places data in that buffer.- The local buffer is used so that if
get_next_data_loc_fnblocks the worker thread, we do not hold the read lock to the internal buffer array for long
- The local buffer is used so that if
- Since some consumers can be fast, there are condition variables, one for each index of the internal buffer array, on which the worker threads sleep if new data is not available. The predicate for waking up also includes the
m_stoppedflag so that workers don't keep sleeping when operations stop.
These are the first next TODO steps in planning currently. They are not the end goals of this project, and single sections might be incomplete
- Write a sample application that uses this component to distribute real world market data to multiple consumers
- Test out performance in cases where single data packets fit on a single cache line vs when they don't
- Test out various sizes of internal buffer array of the relay
- Using
mmapto get page level memory allocation and dividing it internally - Allowing users to start relay with a mode to wait for all worker threads to consumer data before writing new data in internal buffer array