Skip to content

Commit 3f96a4c

Browse files
wip: design doc
Signed-off-by: Matt Peterson <[email protected]>
1 parent 7744a13 commit 3f96a4c

File tree

1 file changed

+95
-0
lines changed

1 file changed

+95
-0
lines changed
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
# Bi-directional Producer/Consumer Streaming with gRPC
2+
3+
## Purpose
4+
The `hedera-block-node` relies on the Helidon 4.x.x server implementations of
5+
HTTP/2 and gRPC services to ingest block data from Consensus Nodes and to stream
6+
block data to downstream consumers. It does this by defining bidirectional
7+
streaming services based on protobuf definitions.
8+
9+
Helidon provides well-defined APIs and extension points to implement business
10+
logic for these services. The main entry point for custom logic is an implementation
11+
of `GrpcService`.
12+
13+
---
14+
A bidirectional consumer service receiving BlockItems from an upstream Consensus Node must fulfill the client-side
15+
contract:
16+
17+
```java
18+
BidiStreamingMethod<Stream<T>, StreamObserver<T>> clientBidiStreamingMethod;
19+
```
20+
* At runtime, Helidon provides the latest inbound BlockItem to the client-side implementation as well as
21+
an object to send a response back to the producer by calling the `onNext()` method.
22+
23+
---
24+
25+
A bidirectional producer service handling downstream BlockItems to consumers must
26+
fulfill the server-side contract:
27+
28+
```java
29+
BidiStreamingMethod<Stream<Response>, StreamObserver<BlockItem>> serverBidiStreamingMethod;
30+
```
31+
* At runtime, Helidon provides the latest inbound response from the downstream consumer to the server-side
32+
implementation by calling the `onNext()` method.
33+
34+
35+
## Entities
36+
37+
**ProducerBlockStreamObserver** - A custom implementation of StreamObserver called by Helidon which is responsible for:
38+
1) Receiving the latest BlockItem from the producer (e.g. Consensus Node).
39+
2) Returning a response to the producer.
40+
41+
**ConsumerBlockStreamObserver** - A custom implementation of StreamObserver called by Helidon which is responsible for:
42+
1) Receiving the latest response from the downstream consumer.
43+
2) Sending the latest BlockItem to the downstream consumer.
44+
45+
## Approaches:
46+
47+
---
48+
49+
### Approach 1: Directly passing BlockItems from the producer bidirectional service to N bidirectional consumer services
50+
51+
Directly passing BlockItems from the producer bidirectional service to N bidirectional consumer services has the
52+
following drawbacks:
53+
54+
1) Each producer must iterate over the list of consumers to pass the BlockItem to each consumer before saving the
55+
BlockItem to disk and issuing a response to the producer. Linear scaling of consumers will aggregate latency resulting
56+
in the last consumer in the list being delayed by the sum of the latencies of all consumers before it.
57+
2) Dynamically subscribing/unsubscribing consumers while deterministically broadcasting BlockItems to each consumer in
58+
the correct order complicates and slows down the process. It requires thread-safe data structures and synchronization
59+
on all reads and writes to ensure new/removed subscribers do not disrupt the iteration order of the consumers.
60+
61+
### Approach 2: Shared data structure between producer and consumer services. Consumers busy-wait for new BlockItems.
62+
63+
Alternatively, if producers store BlockItems in a shared data structure before immediately returning a response to the
64+
producer, the BlockItem is then immediately available for all consumers to read asynchronously. Consumers can repeatedly
65+
poll the shared data structure for new BlockItems. This approach has the following drawbacks:
66+
67+
1) Busy-waiting consumers will consume more CPU resources polling the shared data structure for new BlockItems.
68+
2) It is difficult to anticipate an optimal polling interval for consumers as the number of consumers scales up or down.
69+
3) While prototyping this approach, it appeared that using a busy-wait on a consumer hijacked the thread from responding
70+
to the responses of the downstream consumer.
71+
72+
### Approach 3: Shared data structure between producer and consumer services. Consumer responses drive BlockItems sent to the consumers.
73+
74+
With this approach, producers will also store BlockItems in a shared data structure before immediately returning a
75+
response to the producer. However, rather than using a busy-wait to poll for new BlockItems, consumers will be triggered
76+
to return all the newest BlockItems from the shared data structure when receiving a response from the downstream consumer.
77+
78+
This approach has the following advantages:
79+
1) It will not consume CPU resources polling.
80+
2) It will not hijack the thread from responding to the downstream consumer. Rather, it uses the interaction with the
81+
consumer to trigger sending the newest BlockItems downstream.
82+
3) The shared data structure will need to be concurrent but, after the initial write operation, all subsequent reads
83+
should not require synchronization.
84+
4) The shared data structure should decouple the producer from the consumers. The producer(s) and consumers should
85+
operate independently of each other and not accrue the same latency issues as Approach 1.
86+
87+
Possible drawbacks:
88+
1) With this approach, BlockItems sent to the consumer are driven by the downstream consumer's responses. When Helidon
89+
invokes `onNext()` with the consumer response, can send the all the latest BlockItems (it will keep track of the last BlockItem sent
90+
as well as the last BlockItem received from the producer).
91+
92+
## Goals
93+
94+
## Design
95+

0 commit comments

Comments
 (0)