Repository navigation
fix(linter): make Context.Nodes safe for concurrent rule Check [W6-05a] - #928
Conversation
Nodes() filled its cache without synchronisation, so rules sharing one Context raced, contradicting the documented concurrency contract. Guard the cache with an RWMutex held only for pointer reads/writes, walk the tree outside the lock with a Visitor (no per-statement closures), and add a -race test running three rules concurrently on one Context. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DnbRnTFErhoMJdAjVEeCg2
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Claude finished @ajitpratap0's task in 22s —— View job Review of PR #928: make
|
…napshot [W6-05a] Replace the process-wide nodesMu RWMutex with an atomically published immutable *nodeCollector per Context, removing cross-Context contention (rules phase +74% at 8 goroutines, Lint +23% with the global lock). Add BenchmarkLintParallel to the matrix and gate suite. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DnbRnTFErhoMJdAjVEeCg2
|
Claude finished @ajitpratap0's task in 23s —— View job Review of PR #928: make
|
Perf gate: PASS (shadow mode: report only, this check never fails)Base Runner CPU:
Rules: time regresses at >= +3% with p < 0.01 (Mann-Whitney U), or a suite geomean >= +1.5% (per-round geomeans, same test); allocs/op must not increase; B/op must not rise more than 2%; instructions/op must not rise more than 1.5%. An A/A control with more than 10% of benchmarks moving over 3% is noisy: re-run once, then inconclusive (neutral). Gated suite (GOGC=off, GOMAXPROCS=1)
A/A control: 4 of 50 benchmarks moved more than 3%. New in head, no base to compare (not judged): Instructions per operation (cmd/perfdriver)
Secondary metric: default GC, GOMAXPROCS=1 (informational)
Environmentbenchstat (primary metric) |
Resolve CHANGELOG (and gate-suite) conflicts by placing this PR's entries in main's Unreleased sections. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DnbRnTFErhoMJdAjVEeCg2
|
Claude finished @ajitpratap0's task in 19s —— View job Review of PR #928: lock-free
|
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DnbRnTFErhoMJdAjVEeCg2
|
Claude finished @ajitpratap0's task in 15s —— View job Review of PR #928: lock-free
|
|
Claude finished @ajitpratap0's task in 24s —— View job Review of PR #928: lock-free
|
Summary
Follow-up to merged #906 (review finding, Medium).
Context.Nodes()filled its cache lazily without synchronisation, so several rules callingCheckon one sharedContextraced (confirmed under-racewith SelectStar, DeleteWithoutWhere, ImplicitCrossJoin), contradicting pkg/linter/doc.go and rule.go.Fix: lock-free. The nodes/nodesAST/nodesBuilt fields are replaced by one
nodesCache unsafe.Pointerholding an immutable*nodeCollector(with aroot *ast.ASTfield).Nodes()loads it atomically and returns it ifroot == c.AST; otherwise it walks (outside any lock) and publishes withatomic.StorePointer(last writer wins, contents identical).WithASTstores nil. There is no process-wide lock: an earlier revision used a package-levelsync.RWMutex, which review measured as a cross-Context contention regression (see below), so it was dropped. Context stays copyable and vet-clean; no new allocations (the collector was already allocated). Same preorder as ast.Inspect; no pruning. Also corrects #906's Test Plan nit: merged head was 71bc2c5 (fully green), not b1bbae6.Added
BenchmarkLintParallel/{rules,lint}(b.RunParallel over all RW dialects) to the matrix andscripts/perf/gate-suite.txt. Limitation: the CI perf gate pins each benchmark to one CPU (-cpu 1, taskset), so on CIRunParallelruns a single goroutine and the gate line guards the serial cost of these paths only. It cannot see cross-Context contention. That is covered by the local multi-CPU A/B below (-cpu 8), which is what caught the RWMutex revision; a multi-CPU gate leg is a follow-up for the perf-gate work.Test Plan
TestContextConcurrentRuleCheck(3 rules x 8 goroutines x 50 fresh Contexts): 74-77 DATA RACE reports on the base Nodes(), passes with the fix.TestLintContextHasNoCachedStatefield list updated tonodesCache.go test -race -count=1 ./pkg/linter/...: 9/9 packages pass;go vet ./pkg/linter/...clean; staticcheck ./pkg/linter/... clean;task checkexit 0;task test:cbindingpass;go test -count=1 ./pkg/compatibility/...pass.allocs/op -0.01% on all four; B/op +0.36% (rules), -0.14% to -0.22% (lint). Serial LintRW was +1% with the mutex revision; the atomic load removes that too.
5b0818f8, AMD EPYC 7763: PASS. LintRW all 8 dialects -0.7% to -3.3% (none significant), LintRule L007/L010 neutral, ParseError neutral; allocs/op flat or -0.1%. Single-CPU, so it says nothing about contention (see Summary).nodesCachecomment now records why it isunsafe.Pointerrather thanatomic.Pointer[nodeCollector]:atomic.PointerembedsnoCopy, andContextis copied by value, so vet copylocks would flag callers.Breaking Changes
None. Behaviour is unchanged for single-goroutine use; concurrent use that was previously racy is now supported, as documented.
🤖 Generated with Claude Code
https://claude.ai/code/session_01DnbRnTFErhoMJdAjVEeCg2