Skip to content

Commit 24e9616

Browse files
test: Add subscriber plugin Tests (#1076)
## Reviewer Notes This PR aim to: - Introduce minor improvements to object checks, allocations and more. - Add unit tests for config and session objects, with enough coverage and cases to ensure that we have adequte confidence in the plugin. - Add E2E covering both negative and positive scenarios for single consumer cases and multiple consumer case. - Fix issue, where we don't close the session and stop the streaming, when we execute succesfully the request. - Enable new plugin as default. Things to consider: - One unit test disabled due to unstable behavior. Passes when run as standalone, fails when run with all others from the same class. - Added check in method `allRequestedBlocksSent()` to ensure that when consumer requests 0 as end block, we continue to stream indefinitely. - Added `close()` session method call in `call()`, when we execute the request, in cases where it's valid and consumer does not want live streaming(0 as `endblock`). Signed-off-by: georgi-l95 <glazarov95@gmail.com> Signed-off-by: Joseph Sinclair <121976561+jsync-swirlds@users.noreply.github.com> Co-authored-by: Joseph Sinclair <121976561+jsync-swirlds@users.noreply.github.com>
1 parent 73c6bda commit 24e9616

File tree

11 files changed

+1002
-68
lines changed

11 files changed

+1002
-68
lines changed

block-node/app/build.gradle.kts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ mainModuleInfo {
5757
runtimeOnly("org.hiero.block.node.messaging")
5858
runtimeOnly("org.hiero.block.node.health")
5959
runtimeOnly("org.hiero.block.node.publisher")
60-
runtimeOnly("org.hiero.block.node.subscriber")
60+
runtimeOnly("org.hiero.block.node.stream.subscriber")
6161
runtimeOnly("org.hiero.block.node.verification")
6262
runtimeOnly("org.hiero.block.node.blocks.cloud.historic")
6363
runtimeOnly("org.hiero.block.node.blocks.files.historic")

block-node/stream-subscriber/build.gradle.kts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,4 +18,7 @@ testModuleInfo {
1818
requires("org.junit.jupiter.api")
1919
requires("org.hiero.block.node.app.test.fixtures")
2020
requires("org.assertj.core.api")
21+
22+
requires("org.mockito")
23+
requires("org.mockito.junit.jupiter")
2124
}

block-node/stream-subscriber/src/main/java/org/hiero/block/node/stream/subscriber/BlockStreamSubscriberSession.java

Lines changed: 73 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// SPDX-License-Identifier: Apache-2.0
22
package org.hiero.block.node.stream.subscriber;
33

4+
import static java.util.Objects.requireNonNull;
45
import static java.util.concurrent.locks.LockSupport.parkNanos;
56
import static org.hiero.block.node.spi.BlockNodePlugin.METRICS_CATEGORY;
67
import static org.hiero.block.node.spi.BlockNodePlugin.UNKNOWN_BLOCK_NUMBER;
@@ -11,10 +12,10 @@
1112
import com.hedera.pbj.runtime.grpc.Pipeline;
1213
import com.swirlds.metrics.api.Counter;
1314
import com.swirlds.metrics.api.Counter.Config;
15+
import edu.umd.cs.findbugs.annotations.NonNull;
1416
import java.lang.System.Logger;
1517
import java.lang.System.Logger.Level;
1618
import java.util.List;
17-
import java.util.Objects;
1819
import java.util.concurrent.ArrayBlockingQueue;
1920
import java.util.concurrent.BlockingQueue;
2021
import java.util.concurrent.Callable;
@@ -111,18 +112,17 @@ public class BlockStreamSubscriberSession implements Callable<BlockStreamSubscri
111112
* @param context The context for the block node
112113
*/
113114
public BlockStreamSubscriberSession(
114-
long clientId,
115-
SubscribeStreamRequest request,
116-
Pipeline<? super SubscribeStreamResponseUnparsed> responsePipeline,
117-
BlockNodeContext context,
118-
final CountDownLatch sessionReadyLatch) {
119-
LOGGER.log(Level.TRACE, request.toString());
115+
final long clientId,
116+
@NonNull final SubscribeStreamRequest request,
117+
@NonNull final Pipeline<? super SubscribeStreamResponseUnparsed> responsePipeline,
118+
@NonNull final BlockNodeContext context,
119+
@NonNull final CountDownLatch sessionReadyLatch) {
120120
this.clientId = clientId;
121121
this.startBlockNumber = request.startBlockNumber();
122122
this.endBlockNumber = request.endBlockNumber();
123-
this.responsePipeline = Objects.requireNonNull(responsePipeline);
124-
this.context = Objects.requireNonNull(context);
125-
this.sessionReadyLatch = Objects.requireNonNull(sessionReadyLatch);
123+
this.responsePipeline = requireNonNull(responsePipeline);
124+
this.context = requireNonNull(context);
125+
this.sessionReadyLatch = requireNonNull(sessionReadyLatch);
126126
latestLiveStreamBlock = new AtomicLong(UNKNOWN_BLOCK_NUMBER - 1);
127127
pluginConfiguration = context.configuration().getConfigData(SubscriberConfig.class);
128128
// Next block to send depends on what was requested and what is available.
@@ -162,7 +162,6 @@ public BlockStreamSubscriberSession call() {
162162
if (validateRequest(
163163
oldestBlockNumber, latestBlockNumber, startBlockNumber, endBlockNumber, clientId, LOGGER)) {
164164
// register us to listen to block items from the block messaging system
165-
LOGGER.log(Level.TRACE, "Registering a block subscriber handler for " + handlerName);
166165
context.blockMessaging().registerNoBackpressureBlockItemHandler(liveBlockHandler, false, handlerName);
167166
sessionReadyLatch.countDown();
168167
// Send blocks forever if requested, otherwise send until we reach the requested end block
@@ -174,10 +173,13 @@ public BlockStreamSubscriberSession call() {
174173
// then process live blocks, if available.
175174
sendLiveBlocksIfAvailable();
176175
}
176+
if (allRequestedBlocksSent()) {
177+
// We've sent all request blocks. Therefore, we close, according to the protocol.
178+
close(SubscribeStreamResponse.Code.READ_STREAM_SUCCESS);
179+
}
177180
}
178181
} catch (RuntimeException | ParseException | InterruptedException e) {
179182
sessionFailedCause = e;
180-
interruptedStream.set(true);
181183
close(SubscribeStreamResponse.Code.READ_STREAM_SUCCESS); // Need an "INCOMPLETE" code...
182184
}
183185
// Need to record a metric here with client ID tag, so we can record
@@ -193,7 +195,8 @@ private void sendHistoricalBlocks() throws ParseException {
193195
// We need to send historical blocks.
194196
// We will only send one block at a time to keep things "smooth".
195197
// Start by getting a block accessor for the next block to send from the historical provider.
196-
BlockAccessor nextBlockAccessor = context.historicalBlockProvider().block(nextBlockToSend);
198+
final BlockAccessor nextBlockAccessor =
199+
context.historicalBlockProvider().block(nextBlockToSend);
197200
if (nextBlockAccessor != null) {
198201
// We have a block to send, so send it.
199202
sendOneBlockItemSet(nextBlockAccessor.blockUnparsed());
@@ -202,7 +205,7 @@ private void sendHistoricalBlocks() throws ParseException {
202205
} else {
203206
// Only give up if this is an historical block, otherwise just
204207
// go back up and see if live has the block.
205-
if (!(nextBlockToSend < 0 || nextBlockToSend >= getLatestKnownBlock())) {
208+
if (!(nextBlockToSend < 0 || nextBlockToSend >= getLatestHistoricalBlock())) {
206209
// We cannot get the block needed, something has failed.
207210
// close the stream with an "unavailable" response.
208211
final String message = "Unable to read historical block {0}.";
@@ -228,7 +231,7 @@ private void sendHistoricalBlocks() throws ParseException {
228231
*/
229232
private boolean isHistoryPermitted() {
230233
return !(interruptedStream.get()
231-
|| latestLiveStreamBlock.get() < nextBlockToSend
234+
|| (latestLiveStreamBlock.get() < nextBlockToSend && latestLiveStreamBlock.get() >= 0)
232235
|| allRequestedBlocksSent()
233236
|| nextBatchIsLive());
234237
}
@@ -271,21 +274,35 @@ private void sendLiveBlocksIfAvailable() throws InterruptedException {
271274
// If we run out, get ahead of live, or have to send historical blocks,
272275
// then we'll also break out of the loop and return to the caller.
273276
while (!liveBlockQueue.isEmpty()) {
274-
// take the block item from the queue and process it
275-
final BlockItems blockItems = liveBlockQueue.poll();
276-
// Live _might_ be behind the next expected block (particularly if
277-
// the requested start block is in the future), skip this block, in that case.
278-
if (blockItems.isStartOfNewBlock() && blockItems.newBlockNumber() != nextBlockToSend) {
277+
// Peek at the block item from the queue and _possibly_ process it
278+
BlockItems blockItems = liveBlockQueue.peek();
279+
// Live _might_ be ahead or behind the next expected block (particularly if
280+
// the requested start block is in the future), don't send this block, in that case.
281+
// If behind, remove that item from the queue; if ahead leave it in the queue.
282+
if (blockItems.isStartOfNewBlock() && blockItems.newBlockNumber() < nextBlockToSend) {
279283
LOGGER.log(Level.TRACE, "Skipping block {0} for client {1}", blockItems.newBlockNumber(), clientId);
280284
skipCurrentBlockInQueue(blockItems);
281-
} else {
285+
} else if (blockItems.newBlockNumber() == nextBlockToSend) {
286+
blockItems = liveBlockQueue.poll();
282287
if (blockItems != null) {
283288
sendOneBlockItemSet(blockItems);
289+
if (blockItems.isEndOfBlock()) {
290+
nextBlockToSend++;
291+
trimBlockItemQueue(nextBlockToSend);
292+
}
284293
}
285-
if (blockItems.isEndOfBlock()) {
286-
nextBlockToSend++;
287-
trimBlockItemQueue(nextBlockToSend);
288-
}
294+
} else if (blockItems.isStartOfNewBlock() && blockItems.newBlockNumber() > nextBlockToSend) {
295+
// This block is _future_, so we need to wait, and try to get the next block from history
296+
// first, then come back to this block.
297+
LOGGER.log(
298+
Level.TRACE,
299+
"Retaining future block {0} for client {1}",
300+
blockItems.newBlockNumber(),
301+
clientId);
302+
} else {
303+
// This is a past or future _partial_ block, so we need to trim the queue.
304+
liveBlockQueue.poll(); // discard _this batch only_.
305+
trimBlockItemQueue(nextBlockToSend);
289306
}
290307
// Note: We depend here on the rule that there are no gaps between blocks.
291308
// The header for block N immediately follows the proof for block N-1.
@@ -338,20 +355,26 @@ private boolean queueFull() {
338355
/**
339356
* Remove the remainder of a block from the queue.
340357
* <p>
341-
* This method assumes that a possible header batch has already been removed
342-
* from the queue (and is provided). The provided item is checked, and if it
343-
* is a header block, the remainder of that block, up to the next header
344-
* batch (which might be the next item) is removed from the queue.<br/>
345-
* Note, the item provided and the next item are not removed from the queue,
346-
* so it is important to only call this method after polling and item, and
347-
* when this method returns, the next item in the queue will be the start of
348-
* a new block (or else the queue will be empty).
358+
* This method assumes that a possible header batch has already been viewed
359+
* but not removed from the queue (and is provided). The provided item is
360+
* checked, and if it is a header block, it is removed and then the
361+
* remainder of that block, up to the next header batch (which might be the
362+
* next item) is removed from the queue.<br/>
363+
* Note, the item provided _may be_ removed from the queue (even if it's not
364+
* a block header), so it is important to only call this method after
365+
* peeking at the item without removing it, and when this method returns,
366+
* the next item in the queue will be the start of a new block (or else
367+
* the queue will be empty).
349368
*/
350369
private void skipCurrentBlockInQueue(BlockItems queueHead) {
351-
// The "head" entry is already removed, remove the rest of its block if it's a block header.
352-
if (queueHead != null && queueHead.isStartOfNewBlock()) {
353-
queueHead = liveBlockQueue.peek();
370+
// The "head" entry is _not_ already removed, remove it, and the rest of
371+
// its block. This also handles a partial block at the head of the queue
372+
// when we cannot process that block (e.g. it's in the future or past from
373+
// the block we currently need to send).
374+
if (queueHead != null) {
375+
liveBlockQueue.poll(); // remove the "head" entry
354376
// Now remove "head" entries until the _next_ item is a block header
377+
queueHead = liveBlockQueue.peek(); // peek at the next item.
355378
while (queueHead != null && !(queueHead.isStartOfNewBlock())) {
356379
liveBlockQueue.poll();
357380
queueHead = liveBlockQueue.peek();
@@ -465,11 +488,6 @@ long getNextBlockToSend() {
465488
return nextBlockToSend;
466489
}
467490

468-
// Visible for testing.
469-
LiveBlockHandler getLiveBlockHandler() {
470-
return liveBlockHandler;
471-
}
472-
473491
/**
474492
* Close this session. This will unregister us from the block messaging system and cancel the subscription.
475493
*/
@@ -480,11 +498,14 @@ synchronized void close(final SubscribeStreamResponse.Code endStreamResponseCode
480498
sessionReadyLatch.countDown();
481499
LOGGER.log(Level.DEBUG, "Session ready latch was not counted down on close, releasing now");
482500
}
483-
// unregister us from the block messaging system, if we are not registered then this is noop
484-
context.blockMessaging().unregisterBlockItemHandler(liveBlockHandler);
485-
final Builder response = SubscribeStreamResponseUnparsed.newBuilder().status(endStreamResponseCode);
486-
responsePipeline.onNext(response.build());
487-
responsePipeline.onComplete();
501+
if (!interruptedStream.get()) {
502+
// unregister us from the block messaging system, if we are not registered then this is noop
503+
context.blockMessaging().unregisterBlockItemHandler(liveBlockHandler);
504+
final Builder response =
505+
SubscribeStreamResponseUnparsed.newBuilder().status(endStreamResponseCode);
506+
responsePipeline.onNext(response.build());
507+
responsePipeline.onComplete();
508+
}
488509
if (subscription != null) {
489510
subscription.cancel();
490511
subscription = null;
@@ -494,10 +515,8 @@ synchronized void close(final SubscribeStreamResponse.Code endStreamResponseCode
494515
}
495516

496517
private void sendOneBlockItemSet(final BlockUnparsed nextBlock) throws ParseException {
497-
LOGGER.log(Level.TRACE, "Sending full block {0} to {1}", nextBlockToSend, handlerName);
498-
BlockHeader header =
518+
final BlockHeader header =
499519
BlockHeader.PROTOBUF.parse(nextBlock.blockItems().getFirst().blockHeader());
500-
BlockItems blockBatch = new BlockItems(nextBlock.blockItems(), header.number());
501520
if (header.number() == nextBlockToSend) {
502521
sendOneBlockItemSet(nextBlock.blockItems());
503522
} else {
@@ -541,11 +560,11 @@ private static class LiveBlockHandler implements NoBackPressureBlockItemHandler
541560
private final String clientId;
542561

543562
private LiveBlockHandler(
544-
final BlockingQueue<BlockItems> liveBlockQueue,
545-
final AtomicLong latestLiveStreamBlock,
563+
@NonNull final BlockingQueue<BlockItems> liveBlockQueue,
564+
@NonNull final AtomicLong latestLiveStreamBlock,
546565
final String clientId) {
547-
this.liveBlockQueue = liveBlockQueue;
548-
this.latestLiveStreamBlock = latestLiveStreamBlock;
566+
this.liveBlockQueue = requireNonNull(liveBlockQueue);
567+
this.latestLiveStreamBlock = requireNonNull(latestLiveStreamBlock);
549568
this.clientId = clientId;
550569
}
551570

@@ -557,10 +576,9 @@ public void onTooFarBehindError() {
557576
}
558577

559578
@Override
560-
public void handleBlockItemsReceived(final BlockItems blockItems) {
579+
public void handleBlockItemsReceived(@NonNull final BlockItems blockItems) {
561580
if (blockItems.newBlockNumber() > latestLiveStreamBlock.get()) {
562581
latestLiveStreamBlock.set(blockItems.newBlockNumber());
563-
LOGGER.log(Level.TRACE, "Updated latest block to {0}.", latestLiveStreamBlock);
564582
}
565583
// Blocking so that the client thread has a chance to pull items
566584
// off the head when it's full.

0 commit comments

Comments
 (0)