Skip to content

[Testing] Fix races and bugs found by flaky tests - #8633

Open
janezpodhostnik wants to merge 5 commits into
masterfrom
janez/fix-flaky-tests-production-races
Open

[Testing] Fix races and bugs found by flaky tests#8633
janezpodhostnik wants to merge 5 commits into
masterfrom
janez/fix-flaky-tests-production-races

Conversation

@janezpodhostnik

@janezpodhostnik janezpodhostnik commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Fixes bugs discovered while stress-running the full unit test suite (500+ full-suite runs plus per-test -race stress runs, continuing the work from #8626).

Changes

  • herocache: Cache.get stopped scanning a bucket when it hit a slot whose value was ejected but whose stale 32-bit id prefix collided with the queried key, reporting a present entity as missing. Now continues scanning. Includes a deterministic regression test (verified to fail before the fix).
  • optimistic sync pipeline: Run unconditionally stored the initial parent state and could overwrite a concurrently delivered OnParentStateUpdated, losing the update and deadlocking the pipeline (it would never observe StateComplete and never persist). Run now initializes with CompareAndSwap from StatePending, and OnParentStateUpdated uses an unconditional store so updates cannot be silently dropped. Includes the pipeline test harness fixes this change interlocks with.
  • grpcserver: when shutdown wins the window between ready() and Serve, Serve returns ErrServerStopped, which was thrown as an irrecoverable error during a normal shutdown. Now excluded explicitly (gRPC returns nil when stopped while serving, so the exclusion is precise).
  • ALSP SpamRecordCache: Get copied record fields and AdjustWithInit read the adjusted penalty after the backend lock was released, racing in-place record mutations. Both now read under the lock.
  • slashing consumer: logOffense mutated the caller's Violation (defaulting MsgType), racing concurrent use of the same violation. Uses a local variable now.
  • gossipsub scoring: silencePeriodStartTime (multi-word time.Time) was written by the startup worker while the score function read it concurrently, now an atomic pointer. AppSpecificScoreCache and SubscriptionRecordCache mutated records in place while Get reads fields outside the lock, both use copy-on-write now.
  • RPC inspector: inspectRpcPublishMessages shuffled the live RPC's publish message slice on worker goroutines while libp2p pubsub still reads the same RPC. Now samples a shallow clone.

All fixes verified with 20-100x -race stress runs per affected package. network/alsp/..., network/p2p/scoring/..., and consensus/hotstuff/integration are now fully race-clean.

Related: #8626, #8629

Follow-up PRs will add dedicated regression tests for paths that still lack deterministic coverage (grpcserver shutdown race, slashing consumer, pipeline initialization race).


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.

Summary by CodeRabbit

  • Bug Fixes
    • Improved synchronization so concurrent state updates are preserved reliably.
    • Fixed cache lookups that could incorrectly report misses after entries were evicted.
    • Improved safety and consistency when updating network scoring and subscription data.
    • Prevented shared message data from being modified during validation.
    • Corrected shutdown handling so normal server stops are not reported as fatal errors.
    • Improved registry startup handling and prevented unintended changes to recorded offense details.
  • Tests
    • Expanded regression coverage for cache collisions, pipeline state transitions, and shutdown behavior.

@janezpodhostnik
janezpodhostnik requested a review from a team as a code owner August 3, 2026 13:54
@coderabbitai

coderabbitai Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The changes address concurrent state updates, cache lookup and record ownership, gRPC shutdown handling, atomic registry startup state, asynchronous test behavior, and slashing message-type handling.

Changes

Optimistic sync pipeline

Layer / File(s) Summary
Pipeline state handling and test synchronization
module/executiondatasync/optimistic_sync/pipeline/*
Parent-state updates now preserve concurrent values and always notify. Tests register asynchronous mock expectations earlier, allow expected cancellation errors, and use longer state and error wait timeouts.

Shared cache and record safety

Layer / File(s) Summary
Hero cache collision lookup
module/mempool/herocache/backdata/*
Lookup continues past stale prefix matches. A regression test covers colliding identifiers in ejected slots.
ALSP backend access
network/alsp/internal/cache.go
Penalty values are captured while locked. Record lookup and copying run inside Backend.Run.
Scoring cache copy-on-write
network/p2p/scoring/internal/*Cache.go
Score and subscription updates replace records instead of mutating shared records or topic slices.
Inspector message sampling
network/p2p/inspector/validation/control_message_validation_inspector.go
Sampling uses a cloned publish-message slice.

Server and registry lifecycle handling

Layer / File(s) Summary
gRPC shutdown error handling
module/grpcserver/server.go
grpc.ErrServerStopped is excluded from fatal serve errors.
Registry startup state
network/p2p/scoring/registry.go
Startup time uses an atomic pointer. Repeated startup is rejected, and silence checks handle an unstarted registry.

Slashing offense logging

Layer / File(s) Summary
Local offense message type
network/slashing/consumer.go
Empty message types default to a local unknown value for logging and metrics without modifying the violation.

Estimated code review effort: 4 (Complex) | ~45 minutes

Possibly related PRs

  • onflow/flow-go#8626: Both PRs update optimistic sync functional tests and test utilities for asynchronous state and error handling.

Suggested reviewers: zhangchiqing, kay-zee

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the pull request's primary focus on fixing race conditions and bugs identified by flaky tests.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch janez/fix-flaky-tests-production-races

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@janezpodhostnik janezpodhostnik changed the title Fix production races and bugs found by flaky test campaigns [Testing] Fix races and bugs found by flaky tests Aug 4, 2026
@janezpodhostnik janezpodhostnik self-assigned this Aug 4, 2026
Base automatically changed from janez/flaky-test-followup-evm-registers to master August 4, 2026 15:25
@github-actions

github-actions Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Dependency Review

✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.

Scanned Files

None

@codecov-commenter

codecov-commenter commented Aug 4, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 80.70175% with 11 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
network/slashing/consumer.go 0.00% 5 Missing ⚠️
network/p2p/scoring/registry.go 62.50% 1 Missing and 2 partials ⚠️
network/alsp/internal/cache.go 89.47% 1 Missing and 1 partial ⚠️
module/grpcserver/server.go 0.00% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@network/alsp/internal/cache.go`:
- Around line 126-129: Replace the panic call in the error handler that logs
"unexpected error while getting spam record from cache" with
irrecoverable.NewExceptionf(...) instead, using the same descriptive error
message. This ensures backend faults in the cache are handled consistently
through Flow's exception mechanism rather than bypassing it with a raw panic.

In `@network/p2p/scoring/registry.go`:
- Around line 192-196: Update the startup guard around silencePeriodStartTime to
use CompareAndSwap(nil, &now) as an atomic one-shot initialization; if the swap
fails, return immediately from the current worker instead of calling
parent.Throw or continuing toward ready(). Preserve the timestamp initialization
and startup behavior for the winning worker.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 5bbe8b0d-3d8c-4daa-a2a0-b6dd5de7c161

📥 Commits

Reviewing files that changed from the base of the PR and between 1d9475f and 14ad9df.

📒 Files selected for processing (13)
  • module/executiondatasync/optimistic_sync/pipeline/pipeline.go
  • module/executiondatasync/optimistic_sync/pipeline/pipeline_functional_test.go
  • module/executiondatasync/optimistic_sync/pipeline/pipeline_test.go
  • module/executiondatasync/optimistic_sync/pipeline/pipeline_test_utils.go
  • module/grpcserver/server.go
  • module/mempool/herocache/backdata/cache.go
  • module/mempool/herocache/backdata/cache_test.go
  • network/alsp/internal/cache.go
  • network/p2p/inspector/validation/control_message_validation_inspector.go
  • network/p2p/scoring/internal/appSpecificScoreCache.go
  • network/p2p/scoring/internal/subscriptionCache.go
  • network/p2p/scoring/registry.go
  • network/slashing/consumer.go

Comment on lines +126 to +129
if err != nil {
// the Run closure above never returns an error; an error here indicates a bug in the
// backend implementation.
panic(fmt.Errorf("unexpected error while getting spam record from cache: %w", err))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== files =="
git ls-files | rg '(^|/)cache\.go$|module/component/component\.go|irrecoverable|docs/agents/(CodingConventions|GoDocs)\.md' || true

echo "== target snippet =="
if [ -f network/alsp/internal/cache.go ]; then
  sed -n '1,220p' network/alsp/internal/cache.go | nl -ba | sed -n '1,220p'
fi

echo "== irrecoverable usage examples =="
rg -n "irrecoverable|panic\(fmt\.Errorf|panic\(" -S --glob '*.go' . | head -200

Repository: onflow/flow-go

Length of output: 1148


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== read docs guidelines =="
for f in docs/agents/CodingConventions.md docs/agents/GoDocs.md; do
  if [ -f "$f" ]; then
    echo "--- $f ---"
    cat -n "$f"
  else
    echo "missing $f"
  fi
done

Repository: onflow/flow-go

Length of output: 28678


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== target file =="
awk '{printf "%6d\t%s\n", NR, $0}' network/alsp/internal/cache.go | sed -n '1,240p'

echo "== irrecoverable package =="
awk '{printf "%6d\t%s\n", NR, $0}' module/irrecoverable/exception.go | sed -n '1,240p'
awk '{printf "%6d\t%s\n", NR, $0}' module/irrecoverable/irrecoverable.go | sed -n '1,240p'

echo "== irrecoverable import/usages in network/alsp/internal/cache.go and neighboring alsp cache files =="
rg -n 'irrecoverable|panic\(fmt\.Errorf|fmt\.Errorf|No error returns are expected|Unexpected error' network/alsp -S network/alsp -S || true

echo "== component interface =="
awk '{printf "%6d\t%s\n", NR, $0}' module/component/component.go | sed -n '1,240p'

Repository: onflow/flow-go

Length of output: 34983


Use irrecoverable exceptions for backend faults.

This cache documents returned errors as irrecoverable bugs, but panic(fmt.Errorf(...)) bypasses Flow’s exception mechanism. Use irrecoverable.NewExceptionf(...) here or refactor the cache methods to return exceptions consistently.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@network/alsp/internal/cache.go` around lines 126 - 129, Replace the panic
call in the error handler that logs "unexpected error while getting spam record
from cache" with irrecoverable.NewExceptionf(...) instead, using the same
descriptive error message. This ensures backend faults in the cache are handled
consistently through Flow's exception mechanism rather than bypassing it with a
raw panic.

Source: Coding guidelines

Comment on lines +192 to +196
if reg.silencePeriodStartTime.Load() != nil {
parent.Throw(fmt.Errorf("gossipsub scoring registry started more than once"))
}
reg.silencePeriodStartTime = time.Now()
now := time.Now()
reg.silencePeriodStartTime.Store(&now)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

rg -n 'go\.uber\.org/atomic v1\.11\.0' go.mod go.sum
rg -n -C 5 'type SignalerContext|Throw\(' module/irrecoverable --glob '*.go'
rg -n -C 3 'CompareAndSwap\(' --glob '*.go'

Repository: onflow/flow-go

Length of output: 5471


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

printf 'Registry outline:\n'
ast-grep outline network/p2p/scoring/registry.go --match Start --view expanded || true

printf '\nRelevant registry lines:\n'
sed -n '160,220p' network/p2p/scoring/registry.go

printf '\nStart call sites/usages:\n'
rg -n -C 3 'gossipsub scoring registry started more than once|scoring.*registry|SilencePeriodStartTime|silencePeriodStartTime' network/p2p/scoring module/irrecoverable --glob '*.go'

printf '\nIrrecoverable implementation:\n'
sed -n '1,80p' module/irrecoverable/irrecoverable.go

printf '\nAtomic API availability snippet from Go stdlib if vendored locally not applicable; run simple Go program unless go unavailable:\n'
if command -v go >/dev/null 2>&1; then
  tmp="$(mktemp -d)"
  cat > "$tmp/cas.go" <<'EOF'
package main
import (
	"fmt"
	"go/ast"
	"go/parser"
	"go/token"
)
func main() {
	fset := token.NewFileSet()
	f, err := parser.ParseFile(fset, "src/go.uber.org/atomic/atomic", `package atomic
type Int64 struct { v int64 }
func (p *Int64) CompareAndSwap(old int64, new int64) { _ = 1 }
`, parser.ParseComments)
	if err != nil {
		fmt.Println("parse")
		return
	}
	for _, s := range f.Pkg.Scope.Objects {
		fmt.Println("obj", s.Name)
	}
}
EOF
  true
else
  echo "go unavailable"
fi

Repository: onflow/flow-go

Length of output: 13612


🌐 Web query:

go.uber.org/atomic Pointer CompareAndSwap nil pointer documentation

💡 Result:

In the go.uber.org/atomic library, the behavior regarding nil pointers when using CompareAndSwap depends on the specific atomic type being used. For atomic.Pointer[T] The atomic.Pointer[T] type is a generic wrapper introduced to provide type-safe atomic operations on pointers [1]. It does not impose a restriction against nil pointers. You can use nil as either the 'old' or 'new' argument in a CompareAndSwap operation [2]. This behavior aligns with standard Go pointer semantics, where nil is a valid value for any pointer type (*T). Contrast with atomic.Value It is important to distinguish this from the atomic.Value type (also provided by go.uber.org/atomic), which shadows the standard library's sync/atomic.Value [3]. The atomic.Value.CompareAndSwap and atomic.Value.Store methods will panic if provided with nil [4][3]. Summary If you are using the modern generic atomic.Pointer[T], nil pointers are permitted [2]. If you are using atomic.Value, passing nil will cause a panic [3].

Citations:


🌐 Web query:

go runtime Goexit inside goroutine continues scheduling

💡 Result:

When runtime.Goexit is called, it terminates the specific goroutine that invoked it, but it does not stop the Go scheduler or affect other running goroutines [1][2]. The Go scheduler continues to manage and execute other goroutines as normal [3][2]. The behavior of Goexit can be understood through the following points: 1. Termination of the Goroutine: Goexit causes the calling goroutine to terminate. It executes all deferred functions registered in that goroutine before it finally exits [1][2]. 2. Scheduler Continuity: The runtime scheduler remains active. If other goroutines are runnable, the scheduler will continue to pick them up and execute them [3][2]. 3. Main Goroutine Special Case: If runtime.Goexit is called from the main goroutine, the main goroutine terminates, but the program itself does not exit immediately [2]. Because the main function has not officially returned, the program continues to run as long as other goroutines are active [2]. If all other goroutines finish or exit, the program will crash with a deadlock error because it cannot complete the main function [2]. 4. Internals: When a goroutine finishes or calls Goexit, the runtime executes a cleanup process (goexit0) that transitions the goroutine state to _Gdead, unbinds it from its M (machine thread), puts the goroutine back into a free list for reuse, and calls the scheduler to pick the next runnable goroutine [4][5]. In summary, Goexit is a targeted termination mechanism for a single goroutine and does not halt the overall scheduling of other tasks in the Go runtime [1][2].

Citations:


Make startup initialization an atomic one-shot operation.

Load followed by Store and runtime.Goexit is not safe for the one-shot startup guard. Concurrent workers can both observe nil, and multiple workers may store different timestamps or call ready(). Use silencePeriodStartTime.CompareAndSwap(nil, &now) and return on the losing worker instead of continuing.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@network/p2p/scoring/registry.go` around lines 192 - 196, Update the startup
guard around silencePeriodStartTime to use CompareAndSwap(nil, &now) as an
atomic one-shot initialization; if the swap fails, return immediately from the
current worker instead of calling parent.Throw or continuing toward ready().
Preserve the timestamp initialization and startup behavior for the winning
worker.

Source: Coding guidelines

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