From 9343173d6f6f50cb90285df61dcf16a4880a6360 Mon Sep 17 00:00:00 2001 From: MarvinJWendt Date: Wed, 7 Jun 2023 15:43:23 +0200 Subject: [PATCH 1/7] perf: replaced mutexes with atomic operations --- benchmarks_test.go | 9 -------- doc.go | 17 +++++++------- examples_test.go | 10 --------- robin.go | 56 +++++++++++----------------------------------- robin_test.go | 43 +++++++++++++---------------------- 5 files changed, 37 insertions(+), 98 deletions(-) diff --git a/benchmarks_test.go b/benchmarks_test.go index bcc2247..8964707 100644 --- a/benchmarks_test.go +++ b/benchmarks_test.go @@ -10,12 +10,3 @@ func BenchmarkLoadbalancer_Next(b *testing.B) { lb.Next() } } - -func BenchmarkLoadbalancer_Next_ThreadSafe(b *testing.B) { - set := []int{1, 2, 3} - lb := NewThreadSafeLoadbalancer(set) - b.ResetTimer() - for i := 0; i < b.N; i++ { - lb.Next() - } -} diff --git a/doc.go b/doc.go index 3265cad..50353ab 100644 --- a/doc.go +++ b/doc.go @@ -1,20 +1,19 @@ /* -Package robin is a simple, generic round-robin load balancer for Go. +Package robin is a simple, generic and thread-safe round-robin load balancer for Go. It can be used to load balance any type of data. It is not limited to HTTP requests. Robin takes any slice as an input and returns the next item in the slice. When the end of the slice is reached, it starts again from the beginning. -There are two versions of Robin: a thread-safe version (NewThreadSafeLoadbalancer) and a non-thread-safe (NewLoadbalancer) version. -The thread-safe version is slower than the non-thread-safe version, but it is guaranteed that two concurrent calls to Loadbalancer.Next will not return the same item, if the slice contains more than one item. +Thread-safety is achieved by using atomic operations amd guarantees that two concurrent calls to Loadbalancer.Next will not return the same item, if the slice contains more than one item. Benchmark: - BenchmarkLoadbalancer_Next 225866620 5.274 ns/op - BenchmarkLoadbalancer_Next-2 227712583 5.285 ns/op - BenchmarkLoadbalancer_Next-32 228792201 5.273 ns/op - BenchmarkLoadbalancer_Next_ThreadSafe 100000000 10.15 ns/op - BenchmarkLoadbalancer_Next_ThreadSafe-2 100000000 10.02 ns/op - BenchmarkLoadbalancer_Next_ThreadSafe-32 100000000 10.06 ns/op + BenchmarkLoadbalancer_Next 251751190 4.772 ns/op + BenchmarkLoadbalancer_Next-2 250728889 4.834 ns/op + BenchmarkLoadbalancer_Next-4 253328150 4.773 ns/op + BenchmarkLoadbalancer_Next-8 248147372 4.783 ns/op + BenchmarkLoadbalancer_Next-16 249468267 4.773 ns/op + BenchmarkLoadbalancer_Next-32 247134729 4.802 ns/op */ package robin diff --git a/examples_test.go b/examples_test.go index 1e89d0c..6128636 100644 --- a/examples_test.go +++ b/examples_test.go @@ -12,16 +12,6 @@ func ExampleNewLoadbalancer() { // object1 } -func ExampleNewThreadSafeLoadbalancer() { - set := []string{"object1", "object2", "object3"} - lb := NewThreadSafeLoadbalancer(set) - - fmt.Println(lb.Current()) - - // Output: - // object1 -} - func ExampleLoadbalancer_Current() { set := []int{1, 2, 3} lb := NewLoadbalancer(set) diff --git a/robin.go b/robin.go index 3e8d015..cd871ec 100644 --- a/robin.go +++ b/robin.go @@ -1,73 +1,43 @@ package robin -import "sync" +import ( + "sync/atomic" +) -// Loadbalancer is a simple, generic round-robin load balancer for Go. +// Loadbalancer is a simple, generic and thread-safe round-robin load balancer for Go. type Loadbalancer[T any] struct { - Items []T - ThreadSafe bool + Items []T - idx int - mu sync.Mutex + idx uint64 } // NewLoadbalancer creates a new Loadbalancer. -// For maximum speed, this is not thread-safe. Use NewThreadSafeLoadbalancer if you need thread-safety. -// If two goroutines call Loadbalancer.Next at the exact same time, it can happen that they both return the same item. +// It is guaranteed that two concurrent calls to Loadbalancer.Next will not return the same item, if the slice contains more than one item. func NewLoadbalancer[T any](items []T) *Loadbalancer[T] { return &Loadbalancer[T]{ Items: items, } } -// NewThreadSafeLoadbalancer creates a new Loadbalancer. -// This is thread-safe, but slower than NewLoadbalancer. -// It is guaranteed that two concurrent calls to Loadbalancer.Next will not return the same item, if the slice contains more than one item. -func NewThreadSafeLoadbalancer[T any](items []T) *Loadbalancer[T] { - return &Loadbalancer[T]{ - Items: items, - ThreadSafe: true, - } -} - // Current returns the current item in the slice, without advancing the Loadbalancer. func (l *Loadbalancer[T]) Current() T { - if l.ThreadSafe { - l.mu.Lock() - defer l.mu.Unlock() - } - return l.Items[l.idx] + idx := atomic.LoadUint64(&l.idx) + return l.Items[idx%uint64(len(l.Items))] } // Next returns the next item in the slice. When the end of the slice is reached, it starts again from the beginning. func (l *Loadbalancer[T]) Next() T { - var item T - if l.ThreadSafe { - l.mu.Lock() - item = l.Items[l.idx] - l.idx = (l.idx + 1) % len(l.Items) - l.mu.Unlock() - } else { - item = l.Items[l.idx] - l.idx = (l.idx + 1) % len(l.Items) - } - return item + idx := atomic.AddUint64(&l.idx, 1) - 1 + return l.Items[idx%uint64(len(l.Items))] } // Reset resets the Loadbalancer to its initial state. func (l *Loadbalancer[T]) Reset() { - if l.ThreadSafe { - l.mu.Lock() - defer l.mu.Unlock() - } - l.idx = 0 + atomic.StoreUint64(&l.idx, 0) } // AddItems adds items to the Loadbalancer. func (l *Loadbalancer[T]) AddItems(items ...T) { - if l.ThreadSafe { - l.mu.Lock() - defer l.mu.Unlock() - } + // This part is not thread-safe and should be called only from a single goroutine l.Items = append(l.Items, items...) } diff --git a/robin_test.go b/robin_test.go index 543a5b6..95ae305 100644 --- a/robin_test.go +++ b/robin_test.go @@ -24,7 +24,7 @@ func TestLoadbalancer_Next_ThreadSafe(t *testing.T) { set = append(set, i) } - lb := NewThreadSafeLoadbalancer(set) + lb := NewLoadbalancer(set) var wg sync.WaitGroup @@ -45,47 +45,36 @@ func TestLoadbalancer_Next_ThreadSafe(t *testing.T) { func TestLoadbalancer_AddItems(t *testing.T) { set := []int{1, 2, 3} + lb := NewLoadbalancer(set) + lb.AddItems(4, 5, 6) - lbs := []Loadbalancer[int]{*NewLoadbalancer(set), *NewThreadSafeLoadbalancer(set)} - - for lbi := range lbs { - lbs[lbi].AddItems(4, 5, 6) - - if lbs[lbi].Items[5] != 6 { - t.Errorf("expected %d, got %d", 6, lbs[lbi].Items[5]) - } + if lb.Items[5] != 6 { + t.Errorf("expected %d, got %d", 6, lb.Items[5]) } } func TestLoadbalancer_Reset(t *testing.T) { set := []int{1, 2, 3} + lb := NewLoadbalancer(set) - lbs := []Loadbalancer[int]{*NewLoadbalancer(set), *NewThreadSafeLoadbalancer(set)} - - for lbi := range lbs { - - for i := 0; i < 10; i++ { - if lbs[lbi].Next() != set[i%len(set)] { - t.Errorf("expected %d, got %d", set[i%len(set)], lbs[lbi].Next()) - } + for i := 0; i < 10; i++ { + if lb.Next() != set[i%len(set)] { + t.Errorf("expected %d, got %d", set[i%len(set)], lb.Next()) } + } - lbs[lbi].Reset() + lb.Reset() - if lbs[lbi].idx != 0 { - t.Errorf("expected %d, got %d", 0, lbs[lbi].idx) - } + if lb.idx != 0 { + t.Errorf("expected %d, got %d", 0, lb.idx) } } func TestLoadbalancer_Current(t *testing.T) { set := []int{1, 2, 3} + lb := NewLoadbalancer(set) - lbs := []Loadbalancer[int]{*NewLoadbalancer(set), *NewThreadSafeLoadbalancer(set)} - - for lbi := range lbs { - if lbs[lbi].Current() != set[0] { - t.Errorf("expected %d, got %d", set[0], lbs[lbi].Current()) - } + if lb.Current() != set[0] { + t.Errorf("expected %d, got %d", set[0], lb.Current()) } } From 69c563ef995d0e7fe9cfb839bc57500c05384bf3 Mon Sep 17 00:00:00 2001 From: MarvinJWendt Date: Wed, 7 Jun 2023 13:44:33 +0000 Subject: [PATCH 2/7] docs: autoupdate --- README.md | 142 +++++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 119 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index b6c9238..ef208cd 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ -Unit test count +Unit test count @@ -69,65 +69,77 @@ import "atomicgo.dev/robin" ``` -Package robin is a simple, generic round\-robin load balancer for Go. +Package robin is a simple, generic and thread\-safe round\-robin load balancer for Go. It can be used to load balance any type of data. It is not limited to HTTP requests. Robin takes any slice as an input and returns the next item in the slice. When the end of the slice is reached, it starts again from the beginning. -There are two versions of Robin: a thread\-safe version \(NewThreadSafeLoadbalancer\) and a non\-thread\-safe \(NewLoadbalancer\) version. The thread\-safe version is slower than the non\-thread\-safe version, but it is guaranteed that two concurrent calls to Loadbalancer.Next will not return the same item, if the slice contains more than one item. +Thread\-safety is achieved by using atomic operations amd guarantees that two concurrent calls to Loadbalancer.Next will not return the same item, if the slice contains more than one item. Benchmark: ``` -BenchmarkLoadbalancer_Next 225866620 5.274 ns/op -BenchmarkLoadbalancer_Next-2 227712583 5.285 ns/op -BenchmarkLoadbalancer_Next-32 228792201 5.273 ns/op -BenchmarkLoadbalancer_Next_ThreadSafe 100000000 10.15 ns/op -BenchmarkLoadbalancer_Next_ThreadSafe-2 100000000 10.02 ns/op -BenchmarkLoadbalancer_Next_ThreadSafe-32 100000000 10.06 ns/op +BenchmarkLoadbalancer_Next 251751190 4.772 ns/op +BenchmarkLoadbalancer_Next-2 250728889 4.834 ns/op +BenchmarkLoadbalancer_Next-4 253328150 4.773 ns/op +BenchmarkLoadbalancer_Next-8 248147372 4.783 ns/op +BenchmarkLoadbalancer_Next-16 249468267 4.773 ns/op +BenchmarkLoadbalancer_Next-32 247134729 4.802 ns/op ``` ## Index - [type Loadbalancer](<#type-loadbalancer>) - [func NewLoadbalancer[T any](items []T) *Loadbalancer[T]](<#func-newloadbalancer>) - - [func NewThreadSafeLoadbalancer[T any](items []T) *Loadbalancer[T]](<#func-newthreadsafeloadbalancer>) - [func (l *Loadbalancer[T]) AddItems(items ...T)](<#func-loadbalancert-additems>) + - [func (l *Loadbalancer[T]) Current() T](<#func-loadbalancert-current>) - [func (l *Loadbalancer[T]) Next() T](<#func-loadbalancert-next>) - [func (l *Loadbalancer[T]) Reset()](<#func-loadbalancert-reset>) -## type [Loadbalancer]() +## type [Loadbalancer]() -Loadbalancer is a simple, generic round\-robin load balancer for Go. +Loadbalancer is a simple, generic and thread\-safe round\-robin load balancer for Go. ```go type Loadbalancer[T any] struct { - Items []T - CurrentIndex int - ThreadSafe bool + Items []T // contains filtered or unexported fields } ``` -### func [NewLoadbalancer]() +### func [NewLoadbalancer]() ```go func NewLoadbalancer[T any](items []T) *Loadbalancer[T] ``` -NewLoadbalancer creates a new Loadbalancer. For maximum speed, this is not thread\-safe. Use NewThreadSafeLoadbalancer if you need thread\-safety. If two goroutines call Loadbalancer.Next at the exact same time, it can happen that they both return the same item. +NewLoadbalancer creates a new Loadbalancer. It is guaranteed that two concurrent calls to Loadbalancer.Next will not return the same item, if the slice contains more than one item. -### func [NewThreadSafeLoadbalancer]() +
Example +

```go -func NewThreadSafeLoadbalancer[T any](items []T) *Loadbalancer[T] +{ + set := []string{"object1", "object2", "object3"} + lb := NewLoadbalancer(set) + + fmt.Println(lb.Current()) + +} ``` -NewThreadSafeLoadbalancer creates a new Loadbalancer. This is thread\-safe, but slower than NewLoadbalancer. It is guaranteed that two concurrent calls to Loadbalancer.Next will not return the same item, if the slice contains more than one item. +#### Output -### func \(\*Loadbalancer\[T\]\) [AddItems]() +``` +object1 +``` + +

+
+ +### func \(\*Loadbalancer\[T\]\) [AddItems]() ```go func (l *Loadbalancer[T]) AddItems(items ...T) @@ -135,12 +147,68 @@ func (l *Loadbalancer[T]) AddItems(items ...T) AddItems adds items to the Loadbalancer. -### func \(\*Loadbalancer\[T\]\) [Next]() +
Example +

+ +```go +{ + set := []int{1, 2, 3} + lb := NewLoadbalancer(set) + + lb.AddItems(4, 5, 6) + + fmt.Println(lb.Items) + +} +``` + +#### Output + +``` +[1 2 3 4 5 6] +``` + +

+
+ +### func \(\*Loadbalancer\[T\]\) [Current]() + +```go +func (l *Loadbalancer[T]) Current() T +``` + +Current returns the current item in the slice, without advancing the Loadbalancer. + +
Example +

+ +```go +{ + set := []int{1, 2, 3} + lb := NewLoadbalancer(set) + + fmt.Println(lb.Current()) + +} +``` + +#### Output + +``` +1 +``` + +

+
+ +### func \(\*Loadbalancer\[T\]\) [Next]() ```go func (l *Loadbalancer[T]) Next() T ``` +Next returns the next item in the slice. When the end of the slice is reached, it starts again from the beginning. +
Example

@@ -174,7 +242,7 @@ func (l *Loadbalancer[T]) Next() T

-### func \(\*Loadbalancer\[T\]\) [Reset]() +### func \(\*Loadbalancer\[T\]\) [Reset]() ```go func (l *Loadbalancer[T]) Reset() @@ -182,6 +250,34 @@ func (l *Loadbalancer[T]) Reset() Reset resets the Loadbalancer to its initial state. +
Example +

+ +```go +{ + set := []int{1, 2, 3, 4, 5, 6} + lb := NewLoadbalancer(set) + + lb.Next() + lb.Next() + lb.Next() + + lb.Reset() + + fmt.Println(lb.Current()) + +} +``` + +#### Output + +``` +1 +``` + +

+
+ Generated by [gomarkdoc]() From d1470d5829d145e049d8241b770cce23622586b2 Mon Sep 17 00:00:00 2001 From: MarvinJWendt Date: Wed, 7 Jun 2023 15:46:04 +0200 Subject: [PATCH 3/7] ci: stricter golangci config --- .golangci.yml | 51 +++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 39 insertions(+), 12 deletions(-) diff --git a/.golangci.yml b/.golangci.yml index d5a1a2e..7f8348d 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -17,54 +17,81 @@ linters-settings: - ptrToRefParam - paramTypeCombine - unnamedResult - misspell: - locale: US linters: disable-all: true enable: + # default linters - errcheck - gosimple - govet - ineffassign - staticcheck + - typecheck + - unused + # additional linters + - asasalint - asciicheck + - bidichk - bodyclose + - containedctx + - contextcheck + - decorder - dupl - durationcheck + - errchkjson + - errname - errorlint - exhaustive - - gci - - gocognit + - exhaustruct + - exportloopref + - forcetypeassert + - gocheckcompilerdirectives - gocritic - godot - godox - goerr113 - gofmt - - goimports - goprintffuncname - - misspell + - gosec + - gosmopolitan + - importas + - ireturn + - nakedret + - nestif - nilerr - - noctx + - nilnil - prealloc - predeclared + - revive + - rowserrcheck + - tagalign + - tenv - thelper + - tparallel - unconvert - unparam + - usestdlibvars - wastedassign + - whitespace - wrapcheck + - wsl issues: - # Excluding configuration per-path, per-linter, per-text and per-source + include: + - EXC0012 + - EXC0014 exclude-rules: - path: _test\.go linters: + - gocyclo - errcheck - dupl + - gosec - gocritic - - wrapcheck - - goerr113 - # https://github.com/go-critic/go-critic/issues/926 - linters: - gocritic text: "unnecessaryDefer:" + - linters: + - gocritic + text: "preferDecodeRune:" service: - golangci-lint-version: 1.39.x # use the fixed version to not introduce new linters unexpectedly + golangci-lint-version: 1.53.x From 57288a869c3b44610960eaf9de2766cad5c426a5 Mon Sep 17 00:00:00 2001 From: MarvinJWendt Date: Wed, 7 Jun 2023 15:48:12 +0200 Subject: [PATCH 4/7] ci: added reviewdog --- .github/workflows/golangci.yml | 20 -------------------- .github/workflows/lint.yml | 16 ++++++++++++++++ 2 files changed, 16 insertions(+), 20 deletions(-) delete mode 100644 .github/workflows/golangci.yml create mode 100644 .github/workflows/lint.yml diff --git a/.github/workflows/golangci.yml b/.github/workflows/golangci.yml deleted file mode 100644 index 2ae8bd6..0000000 --- a/.github/workflows/golangci.yml +++ /dev/null @@ -1,20 +0,0 @@ -name: golangci-lint -on: [ push, pull_request ] -jobs: - golangci: - name: lint - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - - name: Set up Go - uses: actions/setup-go@v3 - with: - go-version: ^1 - - - run: go version - - - name: golangci-lint - uses: golangci/golangci-lint-action@v3 - with: - version: latest diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 0000000..3c770ed --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,16 @@ +name: Code analysis + +on: [pull_request] +jobs: + golangci-lint: + runs-on: ubuntu-latest + steps: + - name: Check out code into the Go module directory + uses: actions/checkout@v3 + with: + fetch-depth: 0 + - name: Linting with golangci-lint + uses: reviewdog/action-golangci-lint@v2 + with: + github_token: ${{ secrets.ACCESS_TOKEN }} + reporter: github-pr-review From f786c147a39932082b626e6df8eb8a80213bab88 Mon Sep 17 00:00:00 2001 From: MarvinJWendt Date: Wed, 7 Jun 2023 15:52:01 +0200 Subject: [PATCH 5/7] ci: added reviewdog --- .github/workflows/atomicgo.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/atomicgo.yml b/.github/workflows/atomicgo.yml index f8d3e91..6d0119e 100644 --- a/.github/workflows/atomicgo.yml +++ b/.github/workflows/atomicgo.yml @@ -1,4 +1,6 @@ -on: push +on: + push: + branches: [ main ] name: AtomicGo jobs: From d06aea004d8e8a91821ed89030dd8409d3b1806c Mon Sep 17 00:00:00 2001 From: MarvinJWendt Date: Wed, 7 Jun 2023 16:01:36 +0200 Subject: [PATCH 6/7] chore: include template updates --- .github/workflows/atomicgo.yml | 3 ++- .github/workflows/go.yml | 1 - .github/workflows/lint.yml | 1 + .github/workflows/tweet-release.yml | 2 +- 4 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/atomicgo.yml b/.github/workflows/atomicgo.yml index 6d0119e..1a9de94 100644 --- a/.github/workflows/atomicgo.yml +++ b/.github/workflows/atomicgo.yml @@ -1,8 +1,9 @@ +name: AtomicGo + on: push: branches: [ main ] -name: AtomicGo jobs: docs: if: "!contains(github.event.head_commit.message, 'autoupdate')" diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 597968d..d258e38 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -4,7 +4,6 @@ on: push: branches: [ main ] pull_request: - branches: [ main ] jobs: build: diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 3c770ed..800caea 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -1,6 +1,7 @@ name: Code analysis on: [pull_request] + jobs: golangci-lint: runs-on: ubuntu-latest diff --git a/.github/workflows/tweet-release.yml b/.github/workflows/tweet-release.yml index b126c82..d5c8596 100644 --- a/.github/workflows/tweet-release.yml +++ b/.github/workflows/tweet-release.yml @@ -1,4 +1,4 @@ -name: tweet-release +name: Tweet release # Listen to the `release` event on: From 0e6966d9ca59637044c0156f66945a35dadf3d12 Mon Sep 17 00:00:00 2001 From: MarvinJWendt Date: Wed, 7 Jun 2023 17:29:44 +0200 Subject: [PATCH 7/7] examples: rename example package to robin_test --- examples_test.go | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/examples_test.go b/examples_test.go index 6128636..ffbbb81 100644 --- a/examples_test.go +++ b/examples_test.go @@ -1,10 +1,13 @@ -package robin +package robin_test -import "fmt" +import ( + "atomicgo.dev/robin" + "fmt" +) func ExampleNewLoadbalancer() { set := []string{"object1", "object2", "object3"} - lb := NewLoadbalancer(set) + lb := robin.NewLoadbalancer(set) fmt.Println(lb.Current()) @@ -14,7 +17,7 @@ func ExampleNewLoadbalancer() { func ExampleLoadbalancer_Current() { set := []int{1, 2, 3} - lb := NewLoadbalancer(set) + lb := robin.NewLoadbalancer(set) fmt.Println(lb.Current()) @@ -24,7 +27,7 @@ func ExampleLoadbalancer_Current() { func ExampleLoadbalancer_AddItems() { set := []int{1, 2, 3} - lb := NewLoadbalancer(set) + lb := robin.NewLoadbalancer(set) lb.AddItems(4, 5, 6) @@ -36,7 +39,7 @@ func ExampleLoadbalancer_AddItems() { func ExampleLoadbalancer_Reset() { set := []int{1, 2, 3, 4, 5, 6} - lb := NewLoadbalancer(set) + lb := robin.NewLoadbalancer(set) lb.Next() lb.Next() @@ -52,7 +55,7 @@ func ExampleLoadbalancer_Reset() { func ExampleLoadbalancer_Next() { set := []int{1, 2, 3} - lb := NewLoadbalancer(set) + lb := robin.NewLoadbalancer(set) for i := 0; i < 10; i++ { fmt.Println(lb.Next())