Skip to content

feat(pruner): persist PrunedTxSet across sessions + make cap configurable#873

Closed
freemans13 wants to merge 2 commits into
bsv-blockchain:mainfrom
freemans13:stu/pruner-set-persist-and-config
Closed

feat(pruner): persist PrunedTxSet across sessions + make cap configurable#873
freemans13 wants to merge 2 commits into
bsv-blockchain:mainfrom
freemans13:stu/pruner-set-persist-and-config

Conversation

@freemans13

Copy link
Copy Markdown
Collaborator

Summary

Two changes that together let the PrunedTxSet optimisation from #628 actually bite for the tight-chain workload it was designed for.

  1. Persist the set across sessions. The set is now owned by the Service struct instead of being allocated per PruneWithPartitions call. With pruner_block_trigger=OnBlockMined, every block triggers a new prune session, so a child at height H+1 was never able to recognise its parent at height H — by the time the child's session ran, the parent's session had ended and the set had been thrown away. On dev-scale-1 (image 12b6408adf...) this limited the catch rate to ~6.5% of would-be-wasted parent updates (24M / 371M wasteful updates caught). Persistence lets the common cross-block-boundary case fire.

  2. Make the cap configurable. Replace the hard-coded prunedTxSetMaxEntries = 10_000_000 const with a pruner_utxoPrunedSetMaxEntries setting (default 10M, 0 = unlimited). Persistent sessions accumulate entries across many blocks, so the cap is more likely to saturate; operators tuning for high-throughput / high-fan-out workloads can raise it without rebuilding.

Observability

Two new gauge metrics:

  • utxo_pruner_pruned_set_size — current Len() of the set
  • utxo_pruner_pruned_set_saturated1 if the cap is reached, 0 otherwise

Use these together with utxo_pruner_parents_skipped_pruned_total to confirm whether the cap needs raising.

Out of scope

CheckAndRemove is still destructive — for parents with high fan-out only the first child to look skips the update. That's a separate concern from persistence; if it turns out to matter in production we can swap to Contains + a proper eviction strategy in a follow-up.

Test plan

  • Unit tests pass: go test ./stores/utxo/aerospike/pruner/... ./settings/...
  • go vet clean
  • Deploy to dev-scale-1, watch utxo_pruner_parents_skipped_pruned_total and the new size/saturated gauges; expect the catch rate to climb materially over multiple sessions
  • If saturation kicks in early, tune pruner_utxoPrunedSetMaxEntries upward and re-measure

🤖 Generated with Claude Code

…able

Two changes that together let the PrunedTxSet optimisation actually bite for
the tight-chain workload it was designed for.

1. Persist the set on the Service struct so children whose parents were
   pruned in earlier sessions can still skip the parent-update round-trip.
   In the previous design the set was allocated per `PruneWithPartitions`
   call. With `pruner_block_trigger=OnBlockMined` every block triggers a
   new session, so a child at height H+1 was never able to recognise its
   parent at height H — by then the parent's session had ended and the
   set had been thrown away. On dev-scale-1 this limited the catch rate
   to ~6.5% of would-be-wasted parent updates (24M / 371M).

2. Replace the hard-coded 10M cap with a `pruner_utxoPrunedSetMaxEntries`
   setting (default 10M, 0 = unlimited). Persistent sessions accumulate
   entries across many blocks, so the cap is more likely to saturate;
   operators tuning for high-fan-out workloads can raise it.

Adds two gauge metrics so operators can see what's happening:
  - utxo_pruner_pruned_set_size        — current Len() of the set
  - utxo_pruner_pruned_set_saturated   — 1 if cap reached, 0 otherwise

`CheckAndRemove` remains destructive: for parents with high fan-out only
the first child to look skips the update. That's a separate concern from
persistence and not addressed here.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@github-actions

github-actions Bot commented May 15, 2026

Copy link
Copy Markdown
Contributor

🤖 Claude Code Review

Status: Complete

Current Review:
No issues found. The PR implements persistence for the PrunedTxSet optimization correctly:

  • Thread-safe implementation with proper atomic operations
  • Configurable capacity via pruner_utxoPrunedSetMaxEntries setting
  • Comprehensive documentation matching implementation
  • New observability metrics for monitoring saturation
  • Idempotent operations safe for concurrent/retry scenarios
  • Follows CLAUDE.md conventions (minimal changes, clear purpose)

The change enables the optimization to work across block boundaries as intended, with proper memory bounds and monitoring.

@sonarqubecloud

Copy link
Copy Markdown

@github-actions

github-actions Bot commented May 15, 2026

Copy link
Copy Markdown
Contributor

Benchmark Comparison Report

Baseline: main (unknown)

Current: PR-873 (b67c31f)

Summary

  • Regressions: 0
  • Improvements: 0
  • Unchanged: 142
  • Significance level: p < 0.05
All benchmark results (sec/op)
Benchmark Baseline Current Change p-value
_NewBlockFromBytes-4 1.579µ 1.569µ ~ 0.700
SplitSyncedParentMap_SetIfNotExists/256_buckets-4 71.19n 71.22n ~ 1.000
SplitSyncedParentMap_SetIfNotExists/16_buckets-4 71.10n 71.41n ~ 0.700
SplitSyncedParentMap_SetIfNotExists/1_bucket-4 71.14n 71.31n ~ 1.000
SplitSyncedParentMap_ConcurrentSetIfNotExists/256_buckets... 33.79n 37.08n ~ 0.100
SplitSyncedParentMap_ConcurrentSetIfNotExists/16_buckets_... 58.37n 58.16n ~ 1.000
SplitSyncedParentMap_ConcurrentSetIfNotExists/1_bucket_pa... 140.8n 153.6n ~ 0.200
MiningCandidate_Stringify_Short-4 217.2n 217.5n ~ 0.800
MiningCandidate_Stringify_Long-4 1.610µ 1.604µ ~ 0.200
MiningSolution_Stringify-4 842.1n 841.9n ~ 0.700
BlockInfo_MarshalJSON-4 1.729µ 1.722µ ~ 1.000
NewFromBytes-4 125.7n 146.3n ~ 1.000
Mine_EasyDifficulty-4 65.12µ 65.99µ ~ 0.100
Mine_WithAddress-4 6.929µ 6.832µ ~ 0.100
BlockAssembler_AddTx-4 0.02797n 0.02546n ~ 0.400
AddNode-4 10.83 11.00 ~ 0.400
AddNodeWithMap-4 11.78 11.86 ~ 1.000
DirectSubtreeAdd/4_per_subtree-4 58.26n 58.14n ~ 1.000
DirectSubtreeAdd/64_per_subtree-4 28.59n 31.94n ~ 0.100
DirectSubtreeAdd/256_per_subtree-4 27.46n 30.53n ~ 0.100
DirectSubtreeAdd/1024_per_subtree-4 26.36n 29.09n ~ 0.100
DirectSubtreeAdd/2048_per_subtree-4 25.97n 28.65n ~ 0.100
SubtreeProcessorAdd/4_per_subtree-4 282.9n 282.8n ~ 1.000
SubtreeProcessorAdd/64_per_subtree-4 287.2n 282.6n ~ 0.400
SubtreeProcessorAdd/256_per_subtree-4 281.6n 280.6n ~ 1.000
SubtreeProcessorAdd/1024_per_subtree-4 272.3n 272.1n ~ 1.000
SubtreeProcessorAdd/2048_per_subtree-4 271.2n 272.4n ~ 0.500
SubtreeProcessorRotate/4_per_subtree-4 275.6n 277.1n ~ 0.700
SubtreeProcessorRotate/64_per_subtree-4 274.9n 275.1n ~ 0.300
SubtreeProcessorRotate/256_per_subtree-4 277.7n 274.0n ~ 0.700
SubtreeProcessorRotate/1024_per_subtree-4 277.5n 275.2n ~ 0.100
SubtreeNodeAddOnly/4_per_subtree-4 55.28n 55.87n ~ 0.200
SubtreeNodeAddOnly/64_per_subtree-4 34.50n 34.58n ~ 0.200
SubtreeNodeAddOnly/256_per_subtree-4 33.54n 33.73n ~ 0.100
SubtreeNodeAddOnly/1024_per_subtree-4 32.76n 33.13n ~ 0.100
SubtreeCreationOnly/4_per_subtree-4 116.6n 116.1n ~ 1.000
SubtreeCreationOnly/64_per_subtree-4 418.7n 416.1n ~ 0.700
SubtreeCreationOnly/256_per_subtree-4 1.394µ 1.389µ ~ 0.700
SubtreeCreationOnly/1024_per_subtree-4 4.444µ 4.431µ ~ 0.200
SubtreeCreationOnly/2048_per_subtree-4 8.497µ 8.513µ ~ 0.700
SubtreeProcessorOverheadBreakdown/64_per_subtree-4 275.3n 274.6n ~ 1.000
SubtreeProcessorOverheadBreakdown/1024_per_subtree-4 275.3n 278.0n ~ 0.100
ParallelGetAndSetIfNotExists/1k_nodes-4 2.223m 2.215m ~ 1.000
ParallelGetAndSetIfNotExists/10k_nodes-4 5.465m 5.339m ~ 0.100
ParallelGetAndSetIfNotExists/50k_nodes-4 7.566m 7.244m ~ 0.100
ParallelGetAndSetIfNotExists/100k_nodes-4 10.36m 10.23m ~ 0.100
SequentialGetAndSetIfNotExists/1k_nodes-4 1.962m 1.947m ~ 0.400
SequentialGetAndSetIfNotExists/10k_nodes-4 4.398m 4.375m ~ 1.000
SequentialGetAndSetIfNotExists/50k_nodes-4 12.24m 12.27m ~ 1.000
SequentialGetAndSetIfNotExists/100k_nodes-4 22.15m 21.92m ~ 0.200
ProcessOwnBlockSubtreeNodesParallel/1k_nodes-4 2.280m 2.263m ~ 0.200
ProcessOwnBlockSubtreeNodesParallel/10k_nodes-4 8.300m 8.214m ~ 0.400
ProcessOwnBlockSubtreeNodesParallel/100k_nodes-4 13.33m 13.08m ~ 0.100
ProcessOwnBlockSubtreeNodesSequential/1k_nodes-4 1.974m 1.982m ~ 1.000
ProcessOwnBlockSubtreeNodesSequential/10k_nodes-4 7.599m 7.540m ~ 0.700
ProcessOwnBlockSubtreeNodesSequential/100k_nodes-4 40.21m 40.09m ~ 0.400
DiskTxMap_SetIfNotExists-4 3.781µ 3.703µ ~ 0.400
DiskTxMap_SetIfNotExists_Parallel-4 3.582µ 3.559µ ~ 0.700
DiskTxMap_ExistenceOnly-4 318.7n 316.3n ~ 0.400
Queue-4 189.1n 184.9n ~ 0.100
AtomicPointer-4 3.979n 3.779n ~ 0.100
ReorgOptimizations/DedupFilterPipeline/Old/10K-4 822.2µ 796.2µ ~ 0.100
ReorgOptimizations/DedupFilterPipeline/New/10K-4 803.9µ 753.7µ ~ 0.100
ReorgOptimizations/AllMarkFalse/Old/10K-4 106.7µ 101.9µ ~ 0.100
ReorgOptimizations/AllMarkFalse/New/10K-4 64.30µ 64.33µ ~ 1.000
ReorgOptimizations/HashSlicePool/Old/10K-4 52.56µ 49.99µ ~ 0.100
ReorgOptimizations/HashSlicePool/New/10K-4 11.01µ 10.91µ ~ 0.100
ReorgOptimizations/NodeFlags/Old/10K-4 4.984µ 4.425µ ~ 0.100
ReorgOptimizations/NodeFlags/New/10K-4 1.815µ 1.519µ ~ 0.100
ReorgOptimizations/DedupFilterPipeline/Old/100K-4 10.471m 9.182m ~ 0.100
ReorgOptimizations/DedupFilterPipeline/New/100K-4 10.17m 10.24m ~ 1.000
ReorgOptimizations/AllMarkFalse/Old/100K-4 1.088m 1.080m ~ 0.100
ReorgOptimizations/AllMarkFalse/New/100K-4 708.3µ 706.2µ ~ 0.400
ReorgOptimizations/HashSlicePool/Old/100K-4 476.5µ 477.0µ ~ 0.700
ReorgOptimizations/HashSlicePool/New/100K-4 204.0µ 202.8µ ~ 0.100
ReorgOptimizations/NodeFlags/Old/100K-4 50.75µ 48.27µ ~ 0.100
ReorgOptimizations/NodeFlags/New/100K-4 17.45µ 16.67µ ~ 0.100
TxMapSetIfNotExists-4 46.73n 46.78n ~ 1.000
TxMapSetIfNotExistsDuplicate-4 38.94n 38.58n ~ 0.400
ChannelSendReceive-4 585.4n 592.9n ~ 0.400
CalcBlockWork-4 496.7n 499.9n ~ 0.800
CalculateWork-4 681.1n 673.5n ~ 0.100
BuildBlockLocatorString_Helpers/Size_10-4 1.338µ 1.350µ ~ 0.400
BuildBlockLocatorString_Helpers/Size_100-4 15.21µ 15.33µ ~ 1.000
BuildBlockLocatorString_Helpers/Size_1000-4 126.3µ 127.8µ ~ 0.100
CatchupWithHeaderCache-4 104.3m 104.4m ~ 1.000
_BufferPoolAllocation/16KB-4 4.900µ 3.871µ ~ 0.100
_BufferPoolAllocation/32KB-4 8.648µ 7.601µ ~ 0.100
_BufferPoolAllocation/64KB-4 17.43µ 18.96µ ~ 0.100
_BufferPoolAllocation/128KB-4 36.60µ 28.49µ ~ 0.100
_BufferPoolAllocation/512KB-4 106.9µ 111.0µ ~ 0.100
_BufferPoolConcurrent/32KB-4 19.35µ 18.90µ ~ 0.700
_BufferPoolConcurrent/64KB-4 30.53µ 29.41µ ~ 0.200
_BufferPoolConcurrent/512KB-4 154.5µ 145.1µ ~ 0.100
_SubtreeDeserializationWithBufferSizes/16KB-4 654.3µ 619.2µ ~ 0.100
_SubtreeDeserializationWithBufferSizes/32KB-4 656.2µ 615.2µ ~ 0.100
_SubtreeDeserializationWithBufferSizes/64KB-4 655.5µ 606.7µ ~ 0.100
_SubtreeDeserializationWithBufferSizes/128KB-4 620.2µ 597.9µ ~ 0.100
_SubtreeDeserializationWithBufferSizes/512KB-4 625.7µ 611.6µ ~ 0.100
_SubtreeDataDeserializationWithBufferSizes/16KB-4 36.81m 36.55m ~ 0.400
_SubtreeDataDeserializationWithBufferSizes/32KB-4 36.59m 36.41m ~ 0.700
_SubtreeDataDeserializationWithBufferSizes/64KB-4 36.50m 36.73m ~ 0.700
_SubtreeDataDeserializationWithBufferSizes/128KB-4 36.80m 36.68m ~ 0.400
_SubtreeDataDeserializationWithBufferSizes/512KB-4 36.14m 36.22m ~ 0.700
_PooledVsNonPooled/Pooled-4 739.4n 746.3n ~ 0.700
_PooledVsNonPooled/NonPooled-4 8.156µ 7.482µ ~ 0.100
_MemoryFootprint/Current_512KB_32concurrent-4 6.691µ 6.713µ ~ 0.700
_MemoryFootprint/Proposed_32KB_32concurrent-4 9.552µ 9.327µ ~ 0.200
_MemoryFootprint/Alternative_64KB_32concurrent-4 9.478µ 9.257µ ~ 0.700
_prepareTxsPerLevel-4 400.8m 410.0m ~ 0.700
_prepareTxsPerLevelOrdered-4 3.679m 3.725m ~ 1.000
_prepareTxsPerLevel_Comparison/Original-4 404.0m 401.4m ~ 0.400
_prepareTxsPerLevel_Comparison/Optimized-4 3.495m 3.677m ~ 0.100
SubtreeSizes/10k_tx_4_per_subtree-4 1.358m 1.312m ~ 0.700
SubtreeSizes/10k_tx_16_per_subtree-4 306.8µ 302.6µ ~ 0.700
SubtreeSizes/10k_tx_64_per_subtree-4 72.60µ 72.96µ ~ 1.000
SubtreeSizes/10k_tx_256_per_subtree-4 18.06µ 17.90µ ~ 0.400
SubtreeSizes/10k_tx_512_per_subtree-4 8.903µ 8.973µ ~ 0.700
SubtreeSizes/10k_tx_1024_per_subtree-4 4.453µ 4.524µ ~ 0.700
SubtreeSizes/10k_tx_2k_per_subtree-4 2.197µ 2.218µ ~ 0.100
BlockSizeScaling/10k_tx_64_per_subtree-4 70.06µ 71.02µ ~ 0.400
BlockSizeScaling/10k_tx_256_per_subtree-4 17.74µ 17.50µ ~ 0.300
BlockSizeScaling/10k_tx_1024_per_subtree-4 4.352µ 4.382µ ~ 0.200
BlockSizeScaling/50k_tx_64_per_subtree-4 368.0µ 378.9µ ~ 0.200
BlockSizeScaling/50k_tx_256_per_subtree-4 87.33µ 88.33µ ~ 0.400
BlockSizeScaling/50k_tx_1024_per_subtree-4 21.67µ 21.64µ ~ 0.700
SubtreeAllocations/small_subtrees_exists_check-4 148.4µ 149.5µ ~ 0.400
SubtreeAllocations/small_subtrees_data_fetch-4 159.0µ 161.1µ ~ 0.400
SubtreeAllocations/small_subtrees_full_validation-4 309.4µ 304.1µ ~ 0.100
SubtreeAllocations/medium_subtrees_exists_check-4 9.008µ 8.947µ ~ 0.400
SubtreeAllocations/medium_subtrees_data_fetch-4 9.302µ 9.351µ ~ 0.700
SubtreeAllocations/medium_subtrees_full_validation-4 17.50µ 17.48µ ~ 1.000
SubtreeAllocations/large_subtrees_exists_check-4 2.126µ 2.107µ ~ 0.100
SubtreeAllocations/large_subtrees_data_fetch-4 2.230µ 2.218µ ~ 0.400
SubtreeAllocations/large_subtrees_full_validation-4 4.387µ 4.330µ ~ 0.200
StoreBlock_Sequential/BelowCSVHeight-4 326.0µ 319.9µ ~ 0.700
StoreBlock_Sequential/AboveCSVHeight-4 329.6µ 332.2µ ~ 0.700
GetUtxoHashes-4 263.9n 265.0n ~ 0.700
GetUtxoHashes_ManyOutputs-4 45.26µ 44.92µ ~ 0.700
_NewMetaDataFromBytes-4 242.2n 238.1n ~ 0.700
_Bytes-4 640.0n 626.8n ~ 0.100
_MetaBytes-4 584.6n 576.5n ~ 0.100

Threshold: >10% with p < 0.05 | Generated: 2026-05-18 13:49 UTC

@freemans13 freemans13 self-assigned this May 18, 2026
@freemans13 freemans13 closed this May 19, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant