Skip to content

EVM-18: EvmTransactionBatchService #158

@Puneethkumarck

Description

@Puneethkumarck

Context

The EvmTransactionBatchService buffers incoming EVM transactions and flushes them in batches to the EvmTransactionProcessor. It uses the same dual-trigger mechanism as the existing Solana TransactionBatchService: flush when either the batch reaches a size threshold (200 transactions) or a time threshold (100ms) elapses, whichever comes first. This batching strategy reduces database round-trips while maintaining low latency. The unbounded LinkedTransferQueue prevents gRPC/WebSocket backpressure disconnection.

Specification

File

prism/src/main/java/com/stablebridge/prism/domain/service/EvmTransactionBatchService.java

Constructor dependencies

  • EvmTransactionProcessor — the delegate for batch processing
  • Batch size: 200 (constant)
  • Batch timeout: 100ms (constant)

Implementation pattern

Follow TransactionBatchService.java exactly:

@Singleton
@RequiredArgsConstructor
@Slf4j
public class EvmTransactionBatchService implements Lifecycle {

    private static final int BATCH_SIZE = 200;
    private static final Duration BATCH_TIMEOUT = Duration.ofMillis(100);

    private final EvmTransactionProcessor processor;
    private final LinkedTransferQueue<EvmTransactionWithBlock> queue = new LinkedTransferQueue<>();
    private volatile boolean running;
    private Thread drainThread;

    public void enqueue(EvmTransaction transaction, EvmBlock block) {
        queue.add(new EvmTransactionWithBlock(transaction, block));
    }

    @Override
    public void start() {
        running = true;
        drainThread = Thread.ofVirtual().name("evm-tx-batch-drain").start(this::drainLoop);
    }

    @Override
    public void stop() {
        running = false;
        if (drainThread != null) drainThread.interrupt();
    }

    private void drainLoop() {
        var batch = new ArrayList<EvmTransactionWithBlock>(BATCH_SIZE);
        while (running) {
            try {
                // Wait for first item with timeout
                var first = queue.poll(BATCH_TIMEOUT.toMillis(), TimeUnit.MILLISECONDS);
                if (first != null) {
                    batch.add(first);
                    queue.drainTo(batch, BATCH_SIZE - 1);
                }
                if (!batch.isEmpty()) {
                    flush(batch);
                    batch.clear();
                }
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                break;
            }
        }
        // Final drain on shutdown
        queue.drainTo(batch);
        if (!batch.isEmpty()) flush(batch);
    }

    private void flush(List<EvmTransactionWithBlock> batch) {
        // Group by block, delegate to processor
        // ...
    }

    private record EvmTransactionWithBlock(EvmTransaction transaction, EvmBlock block) {}
}

Design decisions

  • LinkedTransferQueue (unbounded) — same as Solana batch service, prevents backpressure disconnection
  • Dual-trigger: 200 tx OR 100ms, whichever fires first
  • Virtual thread for the drain loop (Thread.ofVirtual())
  • Implements Lifecycle interface for clean start/stop
  • Final drain on shutdown ensures no data loss
  • Groups transactions by block before delegating to processor (since processor needs the block)
  • volatile boolean running for thread-safe stop signal
  • @Singleton (Avaje DI), @RequiredArgsConstructor (Lombok), @Slf4j (logging)

Test class

prism/src/test/java/com/stablebridge/prism/domain/service/EvmTransactionBatchServiceTest.java

Test cases:

  • Single enqueued transaction is flushed after timeout
  • Batch of 200 transactions is flushed immediately (size trigger)
  • Mixed enqueue rates trigger both size and time flushes
  • Stop drains remaining items
  • Empty queue does not trigger processor
  • Processor is called with correct transaction-block grouping

Acceptance Criteria

  • EvmTransactionBatchService exists with LinkedTransferQueue and dual-trigger batching
  • Batch size threshold is 200 transactions
  • Batch timeout threshold is 100ms
  • Uses virtual thread for drain loop
  • Implements Lifecycle with clean start/stop
  • Final drain on shutdown ensures no data loss
  • Follows existing TransactionBatchService pattern exactly
  • All test cases pass
  • ./gradlew build passes

Dependencies

References

Metadata

Metadata

Labels

Projects

No projects

Relationships

None yet

Development

No branches or pull requests

Issue actions