Skip to content

Commit 5fde52e

Browse files
LukaszRozmejclaude
andauthored
debug_traceCallMany: extend timeout CTS lifetime to enumerator disposal (#11429)
* debug_traceCallMany: extend timeout CTS lifetime to enumerator disposal Bind the timeout CancellationTokenSource lifetime to enumerator disposal in DebugRpcModule.TraceCallMany's simple path. The previous `using` scope disposed the CTS before the lazy bundle pipeline was enumerated, leaving the cancellation token unusable to downstream consumers (e.g. WaitHandle throws ObjectDisposedException). Mirrors the iterator-owns-CTS pattern already used by EthRpcModule.GetLogs. Streaming is preserved -- no per-bundle or per-trace buffering is introduced. Add two regression tests covering the disposed-CTS bug and guarding against a future eager-materialization regression. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Simplify debug_traceCallMany regression test Collapse the two regression tests into one that exercises both invariants: the first inner sequence touches WaitHandle (catches premature CTS disposal), the second bundle throws unconditionally (catches eager materialization). Drops the local-mock factory and revertes the CreateDebugRpcModule overload -- shared mocks already configured for similar tests in this fixture work fine here. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Guard CTS against exceptions before iterator handoff Mirror EthRpcModule.eth_getFilterLogs: dispose the timeout CTS in a catch block if anything between creation and handoff to StreamBundleTraces throws. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 0d6d5eb commit 5fde52e

2 files changed

Lines changed: 68 additions & 8 deletions

File tree

src/Nethermind/Nethermind.JsonRpc.Test/Modules/DebugModuleTests.cs

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,43 @@ public class DebugModuleTests
5353
_blockFinder
5454
);
5555

56+
[Test]
57+
public void Debug_traceCallMany_streams_under_live_cancellation_token()
58+
{
59+
BlockHeader header = Build.A.BlockHeader.WithNumber(1).TestObject;
60+
_blockFinder.Head.Returns(Build.A.Block.WithHeader(header).TestObject);
61+
_blockFinder.FindHeader(Arg.Any<BlockParameter>()).ReturnsForAnyArgs(header);
62+
_blockchainBridge.HasStateForBlock(Arg.Any<BlockHeader>()).Returns(true);
63+
_debugBridge
64+
.GetBundleTraces(Arg.Any<TransactionBundle[]>(), Arg.Any<BlockParameter>(), Arg.Any<CancellationToken>(), Arg.Any<GethTraceOptions>())
65+
.Returns(static c => StreamBundles(c.ArgAt<CancellationToken>(2)));
66+
67+
DebugRpcModule rpcModule = CreateDebugRpcModule(_debugBridge);
68+
TransactionBundle bundle = new() { Transactions = [new LegacyTransactionForRpc { To = TestItem.AddressC }] };
69+
70+
ResultWrapper<IEnumerable<IEnumerable<GethLikeTxTrace>>> result =
71+
rpcModule.debug_traceCallMany([bundle, bundle], BlockParameter.Latest);
72+
73+
// The first inner sequence touches WaitHandle (throws ObjectDisposedException if the
74+
// timeout CTS has been disposed). The second bundle throws unconditionally, so the
75+
// call only succeeds if the result is a deferred sequence and we stop after the first.
76+
using IEnumerator<IEnumerable<GethLikeTxTrace>> outer = result.Data.GetEnumerator();
77+
outer.MoveNext().Should().BeTrue();
78+
outer.Current.Count().Should().Be(1);
79+
}
80+
81+
private static IEnumerable<IEnumerable<GethLikeTxTrace>> StreamBundles(CancellationToken token)
82+
{
83+
yield return YieldTrace(token);
84+
throw new InvalidOperationException("second bundle should not be enumerated — streaming was lost");
85+
}
86+
87+
private static IEnumerable<GethLikeTxTrace> YieldTrace(CancellationToken token)
88+
{
89+
_ = token.WaitHandle;
90+
yield return new GethLikeTxTrace();
91+
}
92+
5693
[Test]
5794
public async Task Get_from_db()
5895
{

src/Nethermind/Nethermind.JsonRpc/Modules/DebugModule/DebugRpcModule.cs

Lines changed: 31 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -465,19 +465,42 @@ private ResultWrapper<IEnumerable<IEnumerable<GethLikeTxTrace>>> TraceCallMany(T
465465
{
466466
PrepareTransactions(bundles, header);
467467

468-
using CancellationTokenSource timeout = BuildTimeoutCancellationTokenSource();
469-
CancellationToken cancellationToken = timeout.Token;
468+
CancellationTokenSource timeout = BuildTimeoutCancellationTokenSource();
469+
try
470+
{
471+
IEnumerable<IEnumerable<GethLikeTxTrace>> bundleTraces = debugBridge
472+
.GetBundleTraces(bundles, blockParameter, timeout.Token, options);
470473

471-
IEnumerable<IEnumerable<GethLikeTxTrace>> bundleTraces = debugBridge
472-
.GetBundleTraces(bundles, blockParameter, cancellationToken, options);
474+
if (_logger.IsTrace)
475+
{
476+
int totalTransactions = bundles.Sum(b => b.Transactions?.Length ?? 0);
477+
_logger.Trace($"{nameof(debug_traceCallMany)} completed: {bundles.Length} bundles, {totalTransactions} transactions via simple path");
478+
}
473479

474-
if (_logger.IsTrace)
480+
return ResultWrapper<IEnumerable<IEnumerable<GethLikeTxTrace>>>.Success(StreamBundleTraces(bundleTraces, timeout));
481+
}
482+
catch
475483
{
476-
int totalTransactions = bundles.Sum(b => b.Transactions?.Length ?? 0);
477-
_logger.Trace($"{nameof(debug_traceCallMany)} completed: {bundles.Length} bundles, {totalTransactions} transactions via simple path");
484+
timeout.Dispose();
485+
throw;
478486
}
487+
}
479488

480-
return ResultWrapper<IEnumerable<IEnumerable<GethLikeTxTrace>>>.Success(bundleTraces);
489+
// Bind the timeout CTS lifetime to enumerator disposal so the lazy bundle pipeline
490+
// can keep using the cancellation token after this method returns (JSON-RPC serializes
491+
// the result lazily). Disposing the CTS earlier breaks downstream token consumers
492+
// (e.g. WaitHandle access throws ObjectDisposedException).
493+
private static IEnumerable<IEnumerable<GethLikeTxTrace>> StreamBundleTraces(
494+
IEnumerable<IEnumerable<GethLikeTxTrace>> bundleTraces,
495+
CancellationTokenSource cancellationTokenSource)
496+
{
497+
using (cancellationTokenSource)
498+
{
499+
foreach (IEnumerable<GethLikeTxTrace> traces in bundleTraces)
500+
{
501+
yield return traces;
502+
}
503+
}
481504
}
482505

483506
private ResultWrapper<IEnumerable<IEnumerable<GethLikeTxTrace>>> TraceCallManyWithOverrides(TransactionBundle[] bundles, GethTraceOptions? options, BlockHeader header)

0 commit comments

Comments
 (0)