refactor(plugins): wire TraceStore/OpcodeTracing tracers via DI#12144
Conversation
Add a generic IBlockTracer seam in MainProcessingContext that seeds plugin-registered tracers (contributed through an IMainProcessingModule) into the main BlockchainProcessor. Convert TraceStorePlugin and OpcodeTracingPlugin to register their block tracers through it and delete their InitNetworkProtocol overrides. OpcodeTraceRecorder becomes a container-owned DI singleton with constructor-injected dependencies; its RealTime attach and retrospective background tracing are driven when the main processor resolves its registered tracers. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Claude finished @asdacap's task in 9m 34s —— View job Review: refactor(plugins): wire TraceStore/OpcodeTracing tracers via DI
This is a clean, well-tested refactor. Replacing the mutable I found one issue that will break CI on this PR. 🔴 High —
|
| private class TraceStoreBlockTracer( | ||
| [KeyFilter(DbName)] IDb db, | ||
| ITraceStoreConfig traceStoreConfig, | ||
| ILogManager logManager) | ||
| : DbPersistingBlockTracer<ParityLikeTxTrace, ParityLikeTxTracer>( | ||
| new ParityLikeBlockTracer(traceStoreConfig.TraceTypes), | ||
| db, | ||
| new ParityLikeTraceSerializer(logManager, traceStoreConfig.MaxDepth, traceStoreConfig.VerifySerialized), | ||
| logManager); |
There was a problem hiding this comment.
The PR description states the tracer resolves "the shared ITraceSerializer from the root container so the tracer, pruner, and RPC module share one instance," but TraceStoreBlockTracer constructs a fresh new ParityLikeTraceSerializer(...) inline, and InitRpcModules uses yet another instance (_traceSerializer created in Init). So there are actually two/three independent serializer instances, not one shared one.
This is not a correctness bug — ParityLikeTraceSerializer is effectively stateless and both are built from the same MaxDepth/VerifySerialized config, so the persisted/served formats stay compatible. But it contradicts the stated intent and duplicates an EthereumJsonSerializer allocation. If sharing is desired, register ITraceSerializer<ParityLikeTxTrace> as a singleton and inject it into both the tracer and TraceStoreModuleFactory.
| .AddSingleton<IBlockTracer>(ctx => | ||
| { | ||
| OpcodeTraceRecorder recorder = ctx.Resolve<OpcodeTraceRecorder>(); | ||
| return recorder.Prepare() ? recorder.AttachRealTime() : NullBlockTracer.Instance; | ||
| }); |
There was a problem hiding this comment.
Behavioral timing change worth confirming: this factory runs when the main processor resolves IEnumerable<IBlockTracer>, i.e. during MainProcessingContext construction (InitializeBlockchain), whereas the old Attach ran in InitNetworkProtocol (later). AttachRealTime() reads api.BlockTree?.Head?.Number to recompute the RecentBlocks range and resolves api.SyncModeSelector. SyncModeSelector resolves on demand so that's fine, but please confirm the BlockTree head is fully loaded at this earlier point so the RealTime RecentBlocks range isn't computed against a tip = 0. The PR's behavioral note asserts this; just flagging it as the one observable difference from the move.
StatsAnalyzer was the last non-test user of the removed BlockchainProcessor.Tracers API; wire its Call/Pattern tracers as IBlockTracer via an IMainProcessingModule (matching TraceStore and OpcodeTracing) and drop InitNetworkProtocol so StatsAnalyzer.slnx compiles again. Also remove the unnecessary System.Threading/System.Threading.Tasks usings in StartOpcodeRetrospectiveTracing (covered by ImplicitUsings) that broke the code-lint check (IDE0005). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
|
||
| if (tracer is not null) | ||
| { | ||
| blockchainProcessor.Tracers.Add(tracer); |
There was a problem hiding this comment.
There might be multiple tracers added from multiple plugins, does this take that into account?
There was a problem hiding this comment.
It checked it and only two come up. Until the buidl faiure that is..
| BlockchainProcessor.Options.Default, | ||
| Substitute.For<IProcessingStats>()); | ||
| Substitute.For<IProcessingStats>(), | ||
| []); |
There was a problem hiding this comment.
Can we avoid passing empty arrays - keep this parameter as optional - either params or null by default?
| Options options, | ||
| IProcessingStats processingStats) | ||
| IProcessingStats processingStats, | ||
| IEnumerable<IBlockTracer> blockTracers) |
There was a problem hiding this comment.
params or null by default?
| _stats = processingStats; | ||
| _loopCancellationSource = new CancellationTokenSource(); | ||
| _stats.NewProcessingStatistics += OnNewProcessingStatistics; | ||
| _compositeBlockTracer.AddRange(blockTracers.ToArray()); |
There was a problem hiding this comment.
Just have overload with IEnumerable?
| public async Task PendingWrites_reaches_zero_after_flush(int count) | ||
| { | ||
| for (int i = 0; i < count; i++) _queue.Enqueue(CreateTrace(i)); | ||
| for (int i = 0; i < count; i++) _queue.Enqueue(CreateTrace((ulong)i)); |
There was a problem hiding this comment.
| for (int i = 0; i < count; i++) _queue.Enqueue(CreateTrace((ulong)i)); | |
| for (ulong i = 0; i < (ulong)count; i++) _queue.Enqueue(CreateTrace(i)); |
| } | ||
| catch (Exception) | ||
| { | ||
| // PrepareAsync already logged the failure; disable tracing instead of crashing the node on bad config. |
There was a problem hiding this comment.
I think we do wanted crashing on bad config, that was by design
| _logger.Error($"Failed to attach tracer: {ex.Message}", ex); | ||
| } | ||
| throw; | ||
| return NullBlockTracer.Instance; |
| public Task Execute(CancellationToken cancellationToken) | ||
| { | ||
| // Fire-and-forget: the background loop handles its own errors and shuts down via the recorder's disposal. | ||
| if (recorder.Prepare()) _ = recorder.ExecuteTracingAsync(); |
There was a problem hiding this comment.
Why use Prepare at all when you can await PrepareAsync here?
| .AddSingleton<IBlockTracer>(ctx => | ||
| { | ||
| OpcodeTraceRecorder recorder = ctx.Resolve<OpcodeTraceRecorder>(); | ||
| return recorder.Prepare() ? recorder.AttachRealTime() : NullBlockTracer.Instance; |
There was a problem hiding this comment.
We are adding completely not necessary NullBlockTracer, which might cause worse performance?
Addresses LukaszRozmej's review on PR #12144: - BlockchainProcessor takes `params IBlockTracer[]` instead of a required `IEnumerable<IBlockTracer>`, so callers/tests no longer pass `[]` and the array flows straight into CompositeBlockTracer.AddRange without a ToArray. MainProcessingContext resolves `IBlockTracer[]` for the seam. - OpcodeTracing now fails fast on bad config (by design) instead of logging-and-disabling: dropped the non-throwing Prepare() wrapper; PrepareAsync drops its redundant api parameter (recorder owns it) and throws on invalid config; the RealTime DI factory and the retrospective step (now MustInitialize) await/resolve it so a misconfiguration aborts startup; AttachRealTime returns the live tracer or throws (no NullBlockTracer contributed to the seam). - Test loop counts with ulong directly. Tests updated to assert the throw-on-invalid-config behavior. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
| /// <see cref="MustInitialize"/> is <see langword="true"/> so that an invalid tracing configuration (which makes | ||
| /// <see cref="OpcodeTraceRecorder.PrepareAsync"/> throw) aborts startup rather than silently running with tracing off. |
There was a problem hiding this comment.
avoid obvious comments like this one
| .AddModule(mainProcessingModules) | ||
|
|
||
| .AddScoped<BlockchainProcessor, IBranchProcessor, IProcessingStats>((branchProcessor, processingStats) => | ||
| .AddScoped<BlockchainProcessor, IBranchProcessor, IProcessingStats, IBlockTracer[]>((branchProcessor, processingStats, blockTracers) => |
There was a problem hiding this comment.
This would still allocate internally. Just change it to enumerable.
- Use IEnumerable<IBlockTracer> for the seam instead of an array: the BlockchainProcessor ctor takes an optional IEnumerable, MainProcessingContext resolves IEnumerable, and CompositeBlockTracer gains an IEnumerable AddRange overload (params delegates to it) so nothing materializes an array. - OpcodeTraceRecorder no longer depends on INethermindApi; it injects the concrete services it needs (IBlockTree, ISpecProvider, IEthereumEcdsa, ISyncModeSelector, ILogManager). Drops the now-dead null checks. - Trim the obvious doc/comments on StartOpcodeRetrospectiveTracing. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- StartOpcodeRetrospectiveTracing now declares [RunnerStepDependencies( typeof(InitializeBlockchain))] so it runs after the block tree (and state, for RetrospectiveExecution) is initialized instead of reading it implicitly. Adds the Nethermind.Init project reference for the step type. - Fix an outdated comment in BlockchainTestBase (no tracer removal anymore). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The plugin's new ProjectReference to Nethermind.Init changed its dependency set, breaking `dotnet restore --locked-mode` (NU1004). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Changes
Removes two of the remaining non-test users of the
INethermindPlugin.InitNetworkProtocol()lifecycle hook —TraceStorePluginandOpcodeTracingPlugin— by wiring their block tracers through dependency injection instead of imperatively pushing ontoMainProcessingContext.BlockchainProcessor.Tracers.IBlockTracerDI seam.MainProcessingContextnow seeds the mainBlockchainProcessorwith everyIBlockTracerregistered into its inner scope (constructor-injectedIEnumerable<IBlockTracer>). Plugins contribute a tracer by registering anIMainProcessingModule, soIBlockTraceris only ever resolvable inside the main block-processing scope — never from the root container, RPC, or block production.ITracerBag/BlockchainProcessor.Tracersproperty (and the corresponding members onIBlockchainProcessor,OneTimeChainProcessor, andCompositeBlockTracer); the only callers were the two plugins'InitNetworkProtocoloverrides.InitNetworkProtocol; itsTraceStoreBlockTraceris now registered via anIMainProcessingModule, resolving the keyed DB and the sharedITraceSerializerfrom the root container so the tracer, pruner, and RPC module share one instance.InitNetworkProtocoland the plugin's manual recorder lifecycle.OpcodeTraceRecorderis now a container-owned singleton (IDisposable/IAsyncDisposable, idempotent teardown); RealTime mode contributes its tracer via anIMainProcessingModule, and Retrospective mode runs via a newStartOpcodeRetrospectiveTracingstep. The recorder's originalPrepareAsyncis unchanged; a thin non-throwingPrepare()wrapper adapts it for the DI wiring.Ethereum.Test.Baseregisters its test tracer through the new seam instead of mutatingTracersat runtime.Behavioral note: the tracer attach point moves from
InitNetworkProtocoltoMainProcessingContextconstruction — still after the BlockTree head is loaded and before the main processor runs a block, so what gets traced is unchanged (in fact slightly earlier, with no window where a block could process un-traced).Types of changes
What types of changes does your code introduce?
Testing
Requires testing
If yes, did you write tests?
Notes on testing
New tests cover each piece of the new wiring: the seam (
MainProcessingContextTests), TraceStore tracer registration and serializer sharing (TraceStoreModuleTests), and the Opcode module/recorder including the graceful-degradation path (OpcodeTracingModuleTests,OpcodeTraceRecorderTests).Documentation
Requires documentation update
Requires explanation in Release Notes