Skip to content

fix(errors): prevent infinite recursion in (*Error).As causing 100% CPU#459

Merged
freemans13 merged 2 commits intobsv-blockchain:mainfrom
freemans13:stu/wrapped-error-high-cpu-fix
Feb 5, 2026
Merged

fix(errors): prevent infinite recursion in (*Error).As causing 100% CPU#459
freemans13 merged 2 commits intobsv-blockchain:mainfrom
freemans13:stu/wrapped-error-high-cpu-fix

Conversation

@freemans13
Copy link
Collaborator

@freemans13 freemans13 commented Jan 29, 2026

Summary

  • Fix infinite recursion / O(n²) CPU consumption in errors.(*Error).As method

Problem

During catchup sync, teranode was getting stuck at 100% CPU on all cores. CPU profiling showed 99% of CPU was spent in errors.(*Error).As:

   20750ms 65.15%    github.com/bsv-blockchain/teranode/errors.(*Error).As
    4880ms 15.32%    errors.Unwrap
    2090ms  6.56%    github.com/bsv-blockchain/teranode/errors.(*Error).Unwrap
    1840ms  5.78%    reflect.Value.IsNil

The call chain was:

subtreevalidation.blessMissingTransaction
→ validator.ValidateWithOptions  
→ errors.Is
→ isGRPCWrappedError
→ status.FromError
→ errors.As
→ (*Error).As  ← infinite/exponential loop here

Root Cause

The (*Error).As method was manually traversing wrapped errors by calling .As(target) on them:

// Old code - problematic
if err := e.wrappedErr; err != nil && !reflect.ValueOf(err).IsNil() {
    if as, ok := err.(interface{ As(interface{}) bool }); ok && as.As(target) {
        return true
    }
}

When the wrapped error was also an *Error, this created infinite recursion because:

  1. Go's errors.As calls e1.As(target)
  2. e1.As(target) manually calls e1.wrappedErr.As(target) = e2.As(target)
  3. e2.As(target) manually calls e2.wrappedErr.As(target) = ...
  4. Meanwhile, Go's errors.As also traverses via Unwrap(), causing double/exponential traversal

Additionally, the reflect.ValueOf(err).IsNil() check was expensive (5.78% of CPU).

Fix

Skip manual traversal when the wrapped error is *Error - Go's errors.As handles this automatically via Unwrap():

// New code - fixed
if e.wrappedErr != nil {
    // Skip if wrapped error is *Error to avoid recursive loop
    if _, isError := e.wrappedErr.(*Error); !isError {
        if as, ok := e.wrappedErr.(interface{ As(interface{}) bool }); ok && as.As(target) {
            return true
        }
    }
}

This also removes the expensive reflect.ValueOf(err).IsNil() call.

Test plan

  • All 436 errors package tests pass with race detection
  • Manual testing: restart teranode and verify catchup sync completes without 100% CPU

🤖 Generated with Claude Code

@freemans13 freemans13 self-assigned this Jan 29, 2026
@freemans13 freemans13 changed the title wrapped error high cpu fix (was not committed to previous PR) fix(errors): prevent infinite recursion in (*Error).As causing 100% CPU Jan 29, 2026
@github-actions
Copy link
Contributor

github-actions bot commented Jan 29, 2026

🤖 Claude Code Review

Status: Complete

No issues found. The fix correctly prevents infinite recursion in (*Error).As that was causing 100% CPU during catchup sync.

Analysis:

  • Root cause correctly identified: Manual traversal of wrapped errors in As() combined with Go's automatic traversal via Unwrap() created exponential/infinite loops when *Error wraps *Error
  • Fix is correct: Skipping manual As() call when wrapped error is *Error type allows Go's errors.As to handle traversal automatically via Unwrap()
  • Performance improvement: Removed expensive reflect.ValueOf(err).IsNil() check (was 5.78% of CPU in profile)
  • No functionality loss: The removed third path (errors.Unwrap(e)) was redundant since Unwrap() just returns wrappedErr
  • Test coverage: Existing TestError_As test cases cover the key scenarios including wrapped errors with As() method

@freemans13 freemans13 requested a review from liam February 4, 2026 22:23
@sonarqubecloud
Copy link

sonarqubecloud bot commented Feb 4, 2026

@freemans13 freemans13 merged commit 2980a04 into bsv-blockchain:main Feb 5, 2026
17 of 19 checks passed
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.

2 participants