syncx: import code from sync.Once* proposal (#44653)

This is the code from https://go.dev/cl/451356 renamed into an internal
syncx package. This allows us to use the feature before it lands in the
go stdlib.

As an example I've updated some call sites under cmd/gitserver to
replace usages of sync.Once.

Note: not all sync.Once usages can be replaced, but a majority can be in
our codebase. The exception to this is a lot of our graphql resolvers
use the first context.Context passed in, so wouldn't work with these
utility functions.

Test Plan: go test
This commit is contained in:
Keegan Carruthers-Smith 2022-12-08 19:17:17 +02:00 committed by GitHub
parent a0881361cb
commit c5357634e8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 283 additions and 42 deletions

View File

@ -2,28 +2,20 @@
// to expose the raw system certificates on linux.
package cacert
import "sync"
var (
systemOnce sync.Once
systemCerts [][]byte
systemErr error
import (
"github.com/sourcegraph/sourcegraph/internal/syncx"
)
// System returns PEM encoded system certificates. Note: This function only
// works on Linux. Other operating systems do not rely on PEM files at known
// locations, instead they rely on system calls.
func System() ([][]byte, error) {
systemOnce.Do(func() {
c, err := loadSystemRoots()
if err != nil {
systemErr = err
return
}
systemCerts = c.certs
})
return systemCerts, systemErr
}
var System = syncx.OnceValues(func() ([][]byte, error) {
c, err := loadSystemRoots()
if err != nil {
return nil, err
}
return c.certs, nil
})
// CertPool exists for interaction with x509. Do not use.
type CertPool struct {

View File

@ -54,6 +54,7 @@ import (
"github.com/sourcegraph/sourcegraph/internal/observation"
"github.com/sourcegraph/sourcegraph/internal/ratelimit"
streamhttp "github.com/sourcegraph/sourcegraph/internal/search/streaming/http"
"github.com/sourcegraph/sourcegraph/internal/syncx"
"github.com/sourcegraph/sourcegraph/internal/trace"
"github.com/sourcegraph/sourcegraph/internal/trace/ot"
"github.com/sourcegraph/sourcegraph/internal/types"
@ -1186,6 +1187,10 @@ func (s *Server) handleSearch(w http.ResponseWriter, r *http.Request) {
searchRunning.Inc()
defer searchRunning.Dec()
observeLatency := syncx.OnceFunc(func() {
searchLatency.Observe(time.Since(searchStart).Seconds())
})
eventWriter, err := streamhttp.NewWriter(w)
if err != nil {
tr.SetError(err)
@ -1193,12 +1198,9 @@ func (s *Server) handleSearch(w http.ResponseWriter, r *http.Request) {
return
}
var latencyOnce sync.Once
matchesBuf := streamhttp.NewJSONArrayBuf(8*1024, func(data []byte) error {
tr.AddEvent("flushing data", attribute.Int("data.len", len(data)))
latencyOnce.Do(func() {
searchLatency.Observe(time.Since(searchStart).Seconds())
})
observeLatency()
return eventWriter.EventBytes("matches", data)
})
@ -2607,10 +2609,22 @@ func (s *Server) doBackgroundRepoUpdate(repo api.RepoName, revspec string) error
return nil
}
var (
badRefsOnce sync.Once
badRefs []string
)
// older versions of git do not remove tags case insensitively, so we generate
// every possible case of HEAD (2^4 = 16)
var badRefs = syncx.OnceValue(func() []string {
refs := make([]string, 0, 1<<4)
for bits := uint8(0); bits < (1 << 4); bits++ {
s := []byte("HEAD")
for i, c := range s {
// lowercase if the i'th bit of bits is 1
if bits&(1<<i) != 0 {
s[i] = c - 'A' + 'a'
}
}
refs = append(refs, string(s))
}
return refs
})
// removeBadRefs removes bad refs and tags from the git repo at dir. This
// should be run after a clone or fetch. If your repository contains a ref or
@ -2621,27 +2635,12 @@ var (
//
// Instead we just remove this ref.
func removeBadRefs(ctx context.Context, dir GitDir) {
// older versions of git do not remove tags case insensitively, so we
// generate every possible case of HEAD (2^4 = 16)
badRefsOnce.Do(func() {
for bits := uint8(0); bits < (1 << 4); bits++ {
s := []byte("HEAD")
for i, c := range s {
// lowercase if the i'th bit of bits is 1
if bits&(1<<i) != 0 {
s[i] = c - 'A' + 'a'
}
}
badRefs = append(badRefs, string(s))
}
})
args := append([]string{"branch", "-D"}, badRefs...)
args := append([]string{"branch", "-D"}, badRefs()...)
cmd := exec.CommandContext(ctx, "git", args...)
dir.Set(cmd)
_ = cmd.Run()
args = append([]string{"tag", "-d"}, badRefs...)
args = append([]string{"tag", "-d"}, badRefs()...)
cmd = exec.CommandContext(ctx, "git", args...)
dir.Set(cmd)
_ = cmd.Run()

View File

@ -0,0 +1,91 @@
// Copyright 2022 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// package syncx contains an accepted proposal for the sync package in go1.20.
// See https://github.com/golang/go/issues/56102 and https://go.dev/cl/451356
package syncx
import "sync"
// OnceFunc returns a function that invokes f only once. The returned function
// may be called concurrently.
//
// If f panics, the returned function will panic with the same value on every call.
func OnceFunc(f func()) func() {
var once sync.Once
var valid bool
var p any
return func() {
once.Do(func() {
defer func() {
p = recover()
if !valid {
// Re-panic immediately so on the first call the user gets a
// complete stack trace into f.
panic(p)
}
}()
f()
valid = true // Set only if f does not panic
})
if !valid {
panic(p)
}
}
}
// OnceValue returns a function that invokes f only once and returns the value
// returned by f. The returned function may be called concurrently.
//
// If f panics, the returned function will panic with the same value on every call.
func OnceValue[T any](f func() T) func() T {
var once sync.Once
var valid bool
var p any
var result T
return func() T {
once.Do(func() {
defer func() {
p = recover()
if !valid {
panic(p)
}
}()
result = f()
valid = true
})
if !valid {
panic(p)
}
return result
}
}
// OnceValues returns a function that invokes f only once and returns the values
// returned by f. The returned function may be called concurrently.
//
// If f panics, the returned function will panic with the same value on every call.
func OnceValues[T1, T2 any](f func() (T1, T2)) func() (T1, T2) {
var once sync.Once
var valid bool
var p any
var r1 T1
var r2 T2
return func() (T1, T2) {
once.Do(func() {
defer func() {
p = recover()
if !valid {
panic(p)
}
}()
r1, r2 = f()
valid = true
})
if !valid {
panic(p)
}
return r1, r2
}
}

View File

@ -0,0 +1,159 @@
// Copyright 2022 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package syncx_test
import (
"bytes"
"runtime/debug"
"sync"
"testing"
"github.com/sourcegraph/sourcegraph/internal/syncx"
)
// We assume that the Once.Do tests have already covered parallelism.
func TestOnceFunc(t *testing.T) {
calls := 0
f := syncx.OnceFunc(func() { calls++ })
allocs := testing.AllocsPerRun(10, f)
if calls != 1 {
t.Errorf("want calls==1, got %d", calls)
}
if allocs != 0 {
t.Errorf("want 0 allocations per call, got %v", allocs)
}
}
func TestOnceValue(t *testing.T) {
calls := 0
f := syncx.OnceValue(func() int {
calls++
return calls
})
allocs := testing.AllocsPerRun(10, func() { f() })
value := f()
if calls != 1 {
t.Errorf("want calls==1, got %d", calls)
}
if value != 1 {
t.Errorf("want value==1, got %d", value)
}
if allocs != 0 {
t.Errorf("want 0 allocations per call, got %v", allocs)
}
}
func TestOnceValues(t *testing.T) {
calls := 0
f := syncx.OnceValues(func() (int, int) {
calls++
return calls, calls + 1
})
allocs := testing.AllocsPerRun(10, func() { f() })
v1, v2 := f()
if calls != 1 {
t.Errorf("want calls==1, got %d", calls)
}
if v1 != 1 || v2 != 2 {
t.Errorf("want v1==1 and v2==2, got %d and %d", v1, v2)
}
if allocs != 0 {
t.Errorf("want 0 allocations per call, got %v", allocs)
}
}
func testOncePanic(t *testing.T, calls *int, f func()) {
// Check that the each call to f panics with the same value, but the
// underlying function is only called once.
for _, label := range []string{"first time", "second time"} {
var p any
panicked := true
func() {
defer func() {
p = recover()
}()
f()
panicked = false
}()
if !panicked {
t.Fatalf("%s: f did not panic", label)
}
if p != "x" {
t.Fatalf("%s: want panic %v, got %v", label, "x", p)
}
}
if *calls != 1 {
t.Errorf("want calls==1, got %d", *calls)
}
}
func TestOnceFuncPanic(t *testing.T) {
calls := 0
f := syncx.OnceFunc(func() {
calls++
panic("x")
})
testOncePanic(t, &calls, f)
}
func TestOnceValuePanic(t *testing.T) {
calls := 0
f := syncx.OnceValue(func() int {
calls++
panic("x")
})
testOncePanic(t, &calls, func() { f() })
}
func TestOnceValuesPanic(t *testing.T) {
calls := 0
f := syncx.OnceValues(func() (int, int) {
calls++
panic("x")
})
testOncePanic(t, &calls, func() { f() })
}
func TestOnceFuncPanicTraceback(t *testing.T) {
// Test that on the first invocation of a OnceFunc, the stack trace goes all
// the way to the origin of the panic.
f := syncx.OnceFunc(onceFuncPanic)
defer func() {
if p := recover(); p != "x" {
t.Fatalf("want panic %v, got %v", "x", p)
}
stack := debug.Stack()
want := "syncx_test.onceFuncPanic"
if !bytes.Contains(stack, []byte(want)) {
t.Fatalf("want stack containing %v, got:\n%s", want, string(stack))
}
}()
f()
}
func onceFuncPanic() {
panic("x")
}
func BenchmarkOnceFunc(b *testing.B) {
b.Run("OnceFunc", func(b *testing.B) {
b.ReportAllocs()
f := syncx.OnceFunc(func() {})
for i := 0; i < b.N; i++ {
f()
}
})
// Versus open-coding with Once.Do
b.Run("Once", func(b *testing.B) {
b.ReportAllocs()
var once sync.Once
f := func() {}
for i := 0; i < b.N; i++ {
once.Do(f)
}
})
}