Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use sync.OnceValue for lazy regexp initialization #1279

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions lazy.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
//go:build go1.21

package validator

import (
"regexp"
"sync"
)

func lazyRegexCompile(str string) func() *regexp.Regexp {
return sync.OnceValue(func() *regexp.Regexp {
return regexp.MustCompile(str)
})
}
46 changes: 46 additions & 0 deletions lazy_compat.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
//go:build !go1.21

package validator

import (
"regexp"
"sync"
)

// Copied and adapted from go1.21 stdlib's sync.OnceValue for backwards compatibility:
// 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(f func() *regexp.Regexp) func() *regexp.Regexp {
var (
once sync.Once
valid bool
p interface{}
result *regexp.Regexp
)
g := func() {
defer func() {
p = recover()
if !valid {
panic(p)
}
}()
result = f()
f = nil
valid = true
}
return func() *regexp.Regexp {
once.Do(g)
if !valid {
panic(p)
}
return result
}
}

func lazyRegexCompile(str string) func() *regexp.Regexp {
return onceValue(func() *regexp.Regexp {
return regexp.MustCompile(str)
})
}
83 changes: 83 additions & 0 deletions lazy_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package validator

import (
"regexp"
"sync"
"testing"
)

// TestLazyRegexCompile_Basic tests that lazyRegexCompile compiles the regex only once and caches the result.
func TestLazyRegexCompile_Basic(t *testing.T) {
alphaRegexString := "^[a-zA-Z]+$"
alphaRegex := lazyRegexCompile(alphaRegexString)

callCount := 0
originalFunc := alphaRegex
alphaRegex = func() *regexp.Regexp {
callCount++
return originalFunc()
}

// Call the function multiple times
for i := 0; i < 10; i++ {
result := alphaRegex()
if result == nil {
t.Fatalf("Expected non-nil result")
}
if !result.MatchString("test") {
t.Fatalf("Expected regex to match 'test'")
}
}

if callCount != 10 {
t.Fatalf("Expected call count to be 10, got %d", callCount)
}
}

// TestLazyRegexCompile_Concurrent tests that lazyRegexCompile works correctly when called concurrently.
func TestLazyRegexCompile_Concurrent(t *testing.T) {
alphaRegexString := "^[a-zA-Z]+$"
alphaRegex := lazyRegexCompile(alphaRegexString)

var wg sync.WaitGroup
const numGoroutines = 100

// Use a map to ensure all results point to the same instance
results := make(map[*regexp.Regexp]bool)
var mu sync.Mutex

// Call the function concurrently
for i := 0; i < numGoroutines; i++ {
wg.Add(1)
go func() {
defer wg.Done()
result := alphaRegex()
if result == nil {
t.Errorf("Expected non-nil result")
}
mu.Lock()
results[result] = true
mu.Unlock()
}()
}
wg.Wait()

if len(results) != 1 {
t.Fatalf("Expected one unique regex instance, got %d", len(results))
}
}

// TestLazyRegexCompile_Panic tests that if the regex compilation panics, the panic value is propagated consistently.
func TestLazyRegexCompile_Panic(t *testing.T) {
faultyRegexString := "[a-z"
alphaRegex := lazyRegexCompile(faultyRegexString)

defer func() {
if r := recover(); r == nil {
t.Fatalf("Expected a panic, but none occurred")
}
}()

// Call the function, which should panic
alphaRegex()
}
16 changes: 0 additions & 16 deletions regexes.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,5 @@
package validator

import (
"regexp"
"sync"
)

const (
alphaRegexString = "^[a-zA-Z]+$"
alphaNumericRegexString = "^[a-zA-Z0-9]+$"
Expand Down Expand Up @@ -79,17 +74,6 @@ const (
spicedbTypeRegexString = "^([a-z][a-z0-9_]{1,61}[a-z0-9]/)?[a-z][a-z0-9_]{1,62}[a-z0-9]$"
)

func lazyRegexCompile(str string) func() *regexp.Regexp {
var regex *regexp.Regexp
var once sync.Once
return func() *regexp.Regexp {
once.Do(func() {
regex = regexp.MustCompile(str)
})
return regex
}
}

var (
alphaRegex = lazyRegexCompile(alphaRegexString)
alphaNumericRegex = lazyRegexCompile(alphaNumericRegexString)
Expand Down
Loading