diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 8000390..b279769 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -12,10 +12,9 @@ on: jobs: linux: - name: go-linux strategy: matrix: - go: ["1.22", "1.23"] + go: ["1.23", "1.24", "1.25"] runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 @@ -30,10 +29,9 @@ jobs: - run: go vet ./... linux-32bit: - name: go-linux-32bit strategy: matrix: - go: ["1.22"] + go: ["1.23"] runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 @@ -47,10 +45,9 @@ jobs: - run: go test ./... darwin: - name: go-macos strategy: matrix: - go: ["1.22"] + go: ["1.23"] runs-on: macos-15 steps: - uses: actions/checkout@v2 @@ -64,10 +61,9 @@ jobs: - run: go test ./... linux-stress: - name: go-linux-stress strategy: matrix: - go: ["1.22"] + go: ["1.23"] runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 @@ -82,10 +78,9 @@ jobs: - run: go test ./... -exec 'stress -p 2 -maxruns 1000' -v linux-stress-race: - name: go-linux-stress-race strategy: matrix: - go: ["1.22"] + go: ["1.23"] runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 @@ -98,3 +93,37 @@ jobs: - run: go install github.com/cockroachdb/stress@latest - run: go test -tags crlib_invariants ./... -race -exec 'stress -p 1 -maxruns 100' -v - run: go test ./... -race -exec 'stress -p 1 -maxruns 100' -v + + linux-cockroach-go: + runs-on: ubuntu-latest + env: + GO_BRANCH: cockroach-go1.23.12 + + steps: + - uses: actions/checkout@v4 + + # Step 1: Fetch the branch tip SHA for cache key + - name: Get cockroachdb/go commit hash + id: go-sha + run: | + SHA=$(git ls-remote https://github.com/cockroachdb/go.git refs/heads/$GO_BRANCH | cut -f1) + echo "GO_SHA=$SHA" >> $GITHUB_ENV + + # Step 2: Restore cache (per branch + commit SHA) + - name: Cache custom Go toolchain + uses: actions/cache@v4 + with: + path: ~/.cache/cockroachdb-go/${{ env.GO_SHA }} + key: cockroachdb-${{ env.GO_SHA }} + + # Step 3: Install bootstrap Go (needed to build fork) + - name: Install bootstrap Go + uses: actions/setup-go@v5 + with: + go-version: "1.23.x" + + # Step 4: Run tests with custom Go + - run: ./scripts/run-tests-with-custom-go.sh ./... + - run: ./scripts/run-tests-with-custom-go.sh -tags crlib_invariants ./... + - run: ./scripts/run-tests-with-custom-go.sh -race ./... + diff --git a/crsync/counters.go b/crsync/counters.go new file mode 100644 index 0000000..993c76e --- /dev/null +++ b/crsync/counters.go @@ -0,0 +1,190 @@ +// Copyright 2025 The Cockroach Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +// implied. See the License for the specific language governing +// permissions and limitations under the License. + +package crsync + +import ( + "iter" + "sync/atomic" + "unsafe" +) + +// Counter is a single logical counter backed by a sharded implementation +// (Counters) under the hood. +// +// Properties: +// - Thread-safe increments: Add() can be called concurrently from many +// goroutines. +// - Low write contention: Writes are sharded to minimize cache-line +// ping‑pong. +// - Simple reads: Get() aggregates across shards to return the current value. +// - Construction: Use MakeCounter(). The zero value is NOT ready to use. +// - Performance: Add is O(1) with low contention; Get is O(NumShards()). +// - Consistency: Reads are best-effort snapshots without global locking. Each +// shard is read atomically, but the aggregation is not linearizable with +// respect to concurrent Add calls. This is typically acceptable for metrics +// and counters. +// +// Example: +// +// c := MakeCounter() +// c.Add(1) +// c.Add(41) +// fmt.Println(c.Get()) // 42 +type Counter struct { + c Counters +} + +// MakeCounter initializes a new Counter. +func MakeCounter() Counter { + return Counter{ + c: MakeCounters(1), + } +} + +// Add atomically adds delta to the counter. It is safe for concurrent use by +// multiple goroutines; delta may be negative (decrement). +// +// Add is very efficient: a single atomic increment on a mostly uncontended +// cache line. +func (c *Counter) Add(delta int64) { + c.c.Add(0, delta) +} + +// Get the current value of the counter. +// +// It safe to call Get() while there are concurrent Add() calls (but there is no +// guarantee wrt which of those are reflected). +// +// Get is O(NumShards()) so it is more expensive than Add(). +func (c *Counter) Get() int64 { + return c.c.Get(0) +} + +// Counters is a sharded set of logical counters that can be incremented +// concurrently with low contention. +// +// Use when you need N independent counters that are updated from many +// goroutines (e.g., metrics like hits/misses/errors, per-state tallies). +// +// Properties: +// - Thread-safe increments: Add() can be called concurrently from many +// goroutines. +// - Low write contention: Writes are sharded to minimize cache-line +// ping‑pong. +// - Simple reads: Get() aggregates across shards to return the current value. +// - Construction: Use MakeCounter(). The zero value is NOT ready to use. +// - Performance: Add is O(1) with low contention; Get is O(NumShards()); +// - Consistency: Reads are best-effort snapshots without global locking. Each +// shard is read atomically, but the aggregation is not linearizable with +// respect to concurrent Add calls. This is typically acceptable for metrics +// and counters. +type Counters struct { + numShards uint32 + // shardSize is the number of counters per shard. + shardSize uint32 + // counters contains numShards * shardSize counters; shardSize is a multiple + // of countersPerCacheLine to avoid false sharing at shard boundaries. Note + // that there is a high correlation between the current CPU and the chosen + // shard, so different counters inside a shard can share cache lines. + // + // We linearize the array instead of using [][]atomic.Int64 to avoid an extra + // pointer load in the fast path. + counters []atomic.Int64 + numCounters int +} + +// Number of counters per cache line. We assume the typical 64-byte cache line. +// Must be a power of 2. +const countersPerCacheLine = 8 + +// MakeCounters creates a new Counters with the specified number of counters. +func MakeCounters(numCounters int) Counters { + return makeCounters(NumShards(), numCounters) +} + +func makeCounters(numShards, numCounters int) Counters { + // shardSize is the number of counters, rounded up to fill the last cache line + // (to avoid false sharing). + shardSize := (numCounters + countersPerCacheLine - 1) / countersPerCacheLine * countersPerCacheLine + // Allocate all the counters and align the slice to start at a cache line. We + // allocate countersPerCacheLine-1 extra values to allow realignment. + counters := make([]atomic.Int64, shardSize*numShards+countersPerCacheLine-1) + if r := (uintptr(unsafe.Pointer(&counters[0])) / unsafe.Sizeof(atomic.Int64{})) % countersPerCacheLine; r != 0 { + counters = counters[countersPerCacheLine-r:] + } + return Counters{ + numShards: uint32(numShards), + shardSize: uint32(shardSize), + counters: counters, + numCounters: numCounters, + } +} + +// Add atomically adds delta to the specified counter. It is safe for concurrent +// use by multiple goroutines; delta may be negative (decrement). +// +// Add is very efficient: a single atomic increment on a mostly uncontended +// cache line. +func (c *Counters) Add(counter int, delta int64) { + shard := uint32(CPUBiasedInt()) % c.numShards + c.counters[shard*c.shardSize+uint32(counter)].Add(delta) +} + +// Get the current value of the specified counter. +// +// It safe to call Get() while there are concurrent Add() calls (but there is no +// guarantee wrt which of those are reflected). +// +// Get is O(NumShards()) so it is more expensive than Add(). +func (c *Counters) Get(counter int) int64 { + var res int64 + for shard := range c.numShards { + res += c.counters[shard*c.shardSize+uint32(counter)].Load() + } + return res +} + +// All iterates through the current values of all counters (in order). +// +// Complexity is O(NumShards() * numCounters). All is safe for concurrent use, +// but there are no ordering guarantees w.r.t. concurrent updates. +// +// All is designed to minimize disruption to concurrent Add() calls and is +// preferable to multiple Get() calls when all counter values are needed. +func (c *Counters) All() iter.Seq[int64] { + return func(yield func(int64) bool) { + // To access each cache line only once, we calculate countersPerCacheLine + // counters at a time. + var vals [countersPerCacheLine]int64 + for i := 0; i < c.numCounters; i += countersPerCacheLine { + vals = [countersPerCacheLine]int64{} + n := min(c.numCounters-i, countersPerCacheLine) + for s := range c.numShards { + start := int(s*c.shardSize) + i + counters := c.counters[start : start+n] + // Avoid bound checks inside the loop. + _ = vals[len(counters)-1] + for j := range counters { + vals[j] += counters[j].Load() + } + } + for j := range n { + if !yield(vals[j]) { + return + } + } + } + } +} diff --git a/crsync/counters_test.go b/crsync/counters_test.go new file mode 100644 index 0000000..b4216c6 --- /dev/null +++ b/crsync/counters_test.go @@ -0,0 +1,210 @@ +// Copyright 2025 The Cockroach Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +// implied. See the License for the specific language governing +// permissions and limitations under the License. + +package crsync + +import ( + "fmt" + "math/rand/v2" + "reflect" + "runtime" + "slices" + "sync" + "sync/atomic" + "testing" +) + +func TestCounter(t *testing.T) { + c := MakeCounter() + expect := func(expected int64) { + if actual := c.Get(); actual != expected { + t.Helper() + t.Fatalf("expected %d, got %d", expected, actual) + } + } + expect(0) + c.Add(10) + expect(10) + c.Add(20) + c.Add(-5) + expect(25) +} + +func TestCountersAll(t *testing.T) { + c := MakeCounters(4) + c.Add(0, 10) + c.Add(1, 20) + c.Add(0, 100) + c.Add(2, 30) + c.Add(1, 200) + c.Add(3, 40) + expected := []int64{110, 220, 30, 40} + actual := slices.Collect(c.All()) + if !reflect.DeepEqual(expected, actual) { + t.Fatalf("expected %v, got %v", expected, actual) + } +} + +func TestCountersRand(t *testing.T) { + numCounters := 1 + rand.IntN(100) + c := MakeCounters(numCounters) + numWorkers := 1 + rand.IntN(runtime.GOMAXPROCS(0)*10) + allVals := make([][]int64, numWorkers) + var wg sync.WaitGroup + for i := range numWorkers { + i := i + vals := make([]int64, numCounters) + wg.Add(1) + go func() { + defer wg.Done() + for range rand.IntN(1000) { + k := rand.IntN(numCounters) + v := rand.Int64N(1000) + c.Add(k, v) + vals[k] += v + } + allVals[i] = vals + }() + } + wg.Wait() + actual := slices.Collect(c.All()) + for k := range numCounters { + var expected int64 + for i := range numWorkers { + expected += allVals[i][k] + } + if actual[k] != expected { + t.Fatalf("expected %d, got %d", expected, actual[k]) + } + } +} + +// BenchmarkCounters compares the performance of Counters against simple atomic +// counters, and against sharded counters with 4*P shards and random shard +// choice. There are two Counters versions (crsync and crync-cr), depending on +// whether the CockroachDB Go runtime (and cockroach_go tag) is used. +// +// # Benchmark results +// +// ## Apple M1 Pro (10 core) +// +// benchmark simple randshards crsync crsync-cr +// c=1/p=1-10 6.96ns ± 0% 9.69ns ± 0% 12.02ns ± 0% 7.34ns ± 1% +// c=1/p=4-10 169ns ±10% 53ns ± 4% 21ns ±39% 13ns ±26% +// c=1/p=10-10 752ns ± 3% 125ns ± 0% 51ns ±20% 51ns ± 7% * +// c=1/p=40-10 3.04µs ± 2% 0.67µs ±13% 0.26µs ±31% 0.29µs ±16% +// c=10/p=1-10 4.49ns ± 1% 9.72ns ± 0% 12.39ns ± 0% 4.95ns ± 0% +// c=10/p=4-10 147ns ± 5% 49ns ± 3% 27ns ±33% 6ns ± 1% +// c=10/p=10-10 790ns ± 6% 106ns ± 0% 47ns ±20% 8ns ± 4% * +// c=10/p=40-10 3.24µs ± 2% 0.61µs ±11% 0.24µs ± 9% 0.11µs ±28% +// c=100/p=1-10 4.33ns ± 0% 9.76ns ± 0% 12.41ns ± 0% 4.82ns ± 0% +// c=100/p=4-10 73.9ns ± 4% 46.0ns ± 5% 21.9ns ±22% 6.2ns ± 6% +// c=100/p=10-10 197ns ± 1% 94ns ±10% 53ns ±17% 11ns ± 1% * +// c=100/p=40-10 893ns ± 6% 524ns ± 7% 249ns ±19% 125ns ± 8% +// . * one worker per core +// +// ## Intel(R) Xeon(R) CPU @ 2.80GH (24 core, n2-custom-24-32768 on GCE) +// +// benchmark simple randshards crsync crsync-cr +// c=1/p=1-24 14.1ns ± 0% 21.0ns ± 1% 37.7ns ± 0% 13.5ns ± 0% +// c=1/p=4-24 92.9ns ± 1% 50.1ns ± 1% 63.8ns ±29% 13.3ns ± 0% +// c=1/p=24-24 487ns ±18% 178ns ±105% 144ns ±39% 57ns ±60% * +// c=1/p=96-24 1.84µs ± 2% 0.59µs ± 3% 0.52µs ± 6% 0.29µs ± 7% +// c=10/p=1-24 13.8ns ± 0% 21.2ns ± 1% 38.0ns ± 1% 14.1ns ± 3% +// c=10/p=4-24 91.0ns ± 3% 48.6ns ± 1% 63.8ns ±16% 14.0ns ± 2% +// c=10/p=24-24 461ns ± 8% 176ns ±53% 146ns ±36% 110ns ±84% * +// c=10/p=96-24 1.79µs ± 1% 0.55µs ± 8% 0.52µs ± 6% 0.31µs ± 5% +// c=100/p=1-24 13.7ns ± 0% 22.0ns ± 2% 38.0ns ± 0% 14.1ns ±10% +// c=100/p=4-24 63.5ns ± 1% 46.4ns ± 2% 66.7ns ±30% 14.2ns ± 5% +// c=100/p=24-24 295ns ±27% 87ns ± 1% 121ns ±24% 44ns ±71% * +// c=100/p=96-24 1.11µs ± 2% 0.53µs ± 4% 0.52µs ± 8% 0.31µs ± 5% +// . * one worker per core +func BenchmarkCounters(b *testing.B) { + forEach := func(b *testing.B, fn func(b *testing.B, c, p int)) { + for _, c := range []int{1, 10, 100} { + for _, p := range []int{1, 4, runtime.GOMAXPROCS(0), 4 * runtime.GOMAXPROCS(0)} { + b.Run(fmt.Sprintf("c=%d/p=%d", c, p), func(b *testing.B) { + fn(b, c, p) + }) + } + } + } + + // simple uses non-sharded atomic counters. + b.Run("simple", func(b *testing.B) { + forEach(b, func(b *testing.B, c, p int) { + counters := make([]atomic.Int64, c) + incCounter := func(counter int) { + counters[counter].Add(1) + } + runCountersBenchmark(b, c, p, incCounter) + }) + }) + + // randshards uses a 4*N shards with random shard choice. + b.Run("randshards", func(b *testing.B) { + forEach(b, func(b *testing.B, c, p int) { + counters := makeCounters(runtime.GOMAXPROCS(0)*4, c) + incCounter := func(counter int) { + shard := rand.Uint32N(counters.numShards) + counters.counters[shard*counters.shardSize+uint32(counter)].Add(1) + } + runCountersBenchmark(b, c, p, incCounter) + }) + }) + + name := "crsync" + if UsingCockroachGo { + name += "-cr" + } + b.Run(name, func(b *testing.B) { + forEach(b, func(b *testing.B, c, p int) { + counters := MakeCounters(c) + incCounter := func(counter int) { + counters.Add(counter, 1) + } + runCountersBenchmark(b, c, p, incCounter) + }) + }) +} + +func runCountersBenchmark( + b *testing.B, numCounters, parallelism int, incCounter func(counter int), +) { + const batchSize = 1000 + // Each element of ch corresponds to a batch of operations to be performed. + ch := make(chan int, 1+b.N/batchSize) + + var wg sync.WaitGroup + for range parallelism { + wg.Add(1) + go func() { + defer wg.Done() + + rng := rand.New(rand.NewPCG(rand.Uint64(), rand.Uint64())) + for numOps := range ch { + for range numOps { + incCounter(rng.IntN(numCounters)) + } + } + }() + } + + numOps := int64(b.N) * int64(parallelism) + for i := int64(0); i < numOps; i += batchSize { + ch <- int(min(batchSize, numOps-i)) + } + close(ch) + wg.Wait() +} diff --git a/crsync/sharding_crdb.go b/crsync/sharding_crdb.go new file mode 100644 index 0000000..6e69e10 --- /dev/null +++ b/crsync/sharding_crdb.go @@ -0,0 +1,40 @@ +// Copyright 2025 The Cockroach Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +// implied. See the License for the specific language governing +// permissions and limitations under the License. + +//go:build cockroach_go + +package crsync + +import "runtime" + +// CPUBiasedInt returns an arbitrary non-negative integer that has a best-effort +// association with the current CPU. +// +// Specifically, in the common case, the same value is returned on the same CPU; +// and different CPUs return different values. +// +// When the CockroachDB go runtime is used, the returned value is simply the +// index of the current P (between 0 and GOMAXPROCS-1). +func CPUBiasedInt() int { + return runtime.CurrentP() +} + +// NumShards returns the recommended number of shards when CPUBiasedInt is +// used to select a shard. +func NumShards() int { + return runtime.GOMAXPROCS(0) +} + +// UsingCockroachGo is true if the CockroachDB go runtime is in use. +const UsingCockroachGo = true diff --git a/crsync/sharding_vanilla.go b/crsync/sharding_vanilla.go new file mode 100644 index 0000000..4afabc4 --- /dev/null +++ b/crsync/sharding_vanilla.go @@ -0,0 +1,58 @@ +// Copyright 2025 The Cockroach Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +// implied. See the License for the specific language governing +// permissions and limitations under the License. + +//go:build !cockroach_go + +package crsync + +import ( + "math/rand/v2" + "runtime" + "sync" +) + +// CPUBiasedInt returns an arbitrary non-negative integer that has a best-effort +// association with the current CPU. +// +// Specifically, in the common case, the same value is returned on the same CPU; +// and different CPUs return different values. +// +// When the CockroachDB go runtime is used, the returned value is simply the +// index of the current P (between 0 and GOMAXPROCS-1). +func CPUBiasedInt() int { + // We abuse a sync.Pool knowing that in its implementation, each P holds a + // private value. This is inspired from github.com/puzpuzpuz/xsync.Counter. + n := affinityPool.Get().(*int) + value := *n + affinityPool.Put(n) + return value +} + +var affinityPool = sync.Pool{ + New: func() any { + x := rand.Int() + return &x + }, +} + +// NumShards returns the recommended number of shards when CPUBiasedInt is +// used to select a shard. +func NumShards() int { + // In this implementation, we are relying on random numbers; we want a larger + // number of shards to reduce the expected number of collisions. + return runtime.GOMAXPROCS(0) * 4 +} + +// UsingCockroachGo is true if the CockroachDB go runtime is in use. +const UsingCockroachGo = false diff --git a/crsync/typed_atomic.go b/crsync/typed_atomic.go index 9f753a3..efb3870 100644 --- a/crsync/typed_atomic.go +++ b/crsync/typed_atomic.go @@ -16,7 +16,7 @@ package crsync import "sync/atomic" -// TypedAtomicInt64 is a thin wrapper aorund atomic.Int64 that provides type +// TypedAtomicInt64 is a thin wrapper around atomic.Int64 that provides type // safety. type TypedAtomicInt64[T ~int64] struct { v atomic.Int64 diff --git a/go.mod b/go.mod index 7bbe706..c07f1cf 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,3 @@ module github.com/cockroachdb/crlib -go 1.22 +go 1.23 diff --git a/internal/devtools/go.mod b/internal/devtools/go.mod index 0dc0750..d7ce938 100644 --- a/internal/devtools/go.mod +++ b/internal/devtools/go.mod @@ -1,20 +1,20 @@ module github.com/cockroachdb/crlib/internal/devtools -go 1.22.1 +go 1.23 -toolchain go1.22.5 +toolchain go1.24.2 require ( github.com/cockroachdb/crlfmt v0.2.1 - honnef.co/go/tools v0.5.1 + honnef.co/go/tools v0.6.1 ) require ( github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c // indirect github.com/cockroachdb/gostdlib v1.19.0 // indirect golang.org/x/exp/typeparams v0.0.0-20231108232855-2478ac86f678 // indirect - golang.org/x/mod v0.17.0 // indirect - golang.org/x/sync v0.7.0 // indirect - golang.org/x/sys v0.20.0 // indirect - golang.org/x/tools v0.21.1-0.20240531212143-b6235391adb3 // indirect + golang.org/x/mod v0.23.0 // indirect + golang.org/x/sync v0.11.0 // indirect + golang.org/x/sys v0.30.0 // indirect + golang.org/x/tools v0.30.0 // indirect ) diff --git a/internal/devtools/go.sum b/internal/devtools/go.sum index 6c5e8c6..37fd45c 100644 --- a/internal/devtools/go.sum +++ b/internal/devtools/go.sum @@ -6,21 +6,23 @@ github.com/cockroachdb/gostdlib v1.19.0 h1:cSISxkVnTlWhTkyple/T6NXzOi5659FkhxvUg github.com/cockroachdb/gostdlib v1.19.0/go.mod h1:+dqqpARXbE/gRDEhCak6dm0l14AaTymPZUKMfURjBtY= github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= golang.org/x/exp/typeparams v0.0.0-20231108232855-2478ac86f678 h1:1P7xPZEwZMoBoz0Yze5Nx2/4pxj6nw9ZqHWXqP0iRgQ= golang.org/x/exp/typeparams v0.0.0-20231108232855-2478ac86f678/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk= -golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= -golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= -golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= -golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= -golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/tools v0.21.1-0.20240531212143-b6235391adb3 h1:SHq4Rl+B7WvyM4XODon1LXtP7gcG49+7Jubt1gWWswY= -golang.org/x/tools v0.21.1-0.20240531212143-b6235391adb3/go.mod h1:bqv7PJ/TtlrzgJKhOAGdDUkUltQapRik/UEHubLVBWo= +golang.org/x/mod v0.23.0 h1:Zb7khfcRGKk+kqfxFaP5tZqCnDZMjC5VtUBs87Hr6QM= +golang.org/x/mod v0.23.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= +golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w= +golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= +golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/tools v0.30.0 h1:BgcpHewrV5AUp2G9MebG4XPFI1E2W41zU1SaqVA9vJY= +golang.org/x/tools v0.30.0/go.mod h1:c347cR/OJfw5TI+GfX7RUPNMdDRRbjvYTS0jPyvsVtY= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -honnef.co/go/tools v0.5.1 h1:4bH5o3b5ZULQ4UrBmP+63W9r7qIkqJClEA9ko5YKx+I= -honnef.co/go/tools v0.5.1/go.mod h1:e9irvo83WDG9/irijV44wr3tbhcFeRnfpVlRqVwpzMs= +honnef.co/go/tools v0.6.1 h1:R094WgE8K4JirYjBaOpz/AvTyUu/3wbmAoskKN/pxTI= +honnef.co/go/tools v0.6.1/go.mod h1:3puzxxljPCe8RGJX7BIy1plGbxEOZni5mR2aXe3/uk4= diff --git a/scripts/run-tests-with-custom-go.sh b/scripts/run-tests-with-custom-go.sh new file mode 100755 index 0000000..704f8ae --- /dev/null +++ b/scripts/run-tests-with-custom-go.sh @@ -0,0 +1,59 @@ +#!/usr/bin/env bash +# run-tests-with-custom-go.sh +# +# Downloads and builds a custom Go toolchain from cockcroachd/go (specific branch), +# caches it in ~/.cache/cockcroachd-go//, and runs `go test`. +# +# Works in GitHub Actions or when run manually. +# In CI: pass GO_SHA to pin an exact commit. +# Locally: if GO_SHA is unset, script fetches the latest branch tip. + +set -euo pipefail + +GO_REPO="https://github.com/cockroachdb/go.git" +GO_SHA="${GO_SHA:-}" + +if [ -z "$GO_SHA" ]; then + GO_BRANCH="${GO_BRANCH:-cockroach-go1.23.12}" + echo "==> Resolving latest SHA for branch $GO_BRANCH..." + GO_SHA=$(git ls-remote "$GO_REPO" "refs/heads/$GO_BRANCH" | cut -f1) +fi + +# Use GITHUB_WORKSPACE if present (GitHub Actions), else current dir +REPO_ROOT="${GITHUB_WORKSPACE:-$(pwd)}" + +# Cache location (works locally and in CI) +CACHE_BASE="${XDG_CACHE_HOME:-$HOME/.cache}/cockroachdb-go" +CACHE_DIR="$CACHE_BASE/$GO_SHA" +SRC_DIR="$CACHE_DIR/src" + +echo "==> Commit SHA: $GO_SHA" +echo "==> Repository root: $REPO_ROOT" +echo "==> Cache directory: $CACHE_DIR" + +if [ ! -x "$SRC_DIR/go/bin/go" ]; then + echo "==> Building new Go toolchain..." + mkdir -p "$SRC_DIR" + rm -rf "$SRC_DIR/go" # in case of partial/incomplete build + + git clone "$GO_REPO" "$SRC_DIR/go" + cd "$SRC_DIR/go" + git checkout "$GO_SHA" + + cd "$SRC_DIR/go/src" + ./make.bash +else + echo "==> Reusing cached Go toolchain" +fi + +# Point environment to new Go +export GOROOT="$SRC_DIR/go" +export PATH="$GOROOT/bin:$PATH" + +echo "==> Custom Go version:" +go version + +echo "==> Running tests in $REPO_ROOT" +cd "$REPO_ROOT" +echo go test -tags cockroach_go "$@" +go test -tags cockroach_go "$@"