diff --git a/.github/workflows/atomicgo.yml b/.github/workflows/atomicgo.yml index f8d3e91..1a9de94 100644 --- a/.github/workflows/atomicgo.yml +++ b/.github/workflows/atomicgo.yml @@ -1,6 +1,9 @@ -on: push - name: AtomicGo + +on: + push: + branches: [ main ] + 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/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..800caea --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,17 @@ +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 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: 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 diff --git a/README.md b/README.md index b53d339..ef208cd 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ -Unit test count +Unit test count @@ -69,55 +69,53 @@ 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 - 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.
Example

@@ -141,37 +139,7 @@ object1

-### func [NewThreadSafeLoadbalancer]() - -```go -func NewThreadSafeLoadbalancer[T any](items []T) *Loadbalancer[T] -``` - -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. - -
Example -

- -```go -{ - set := []string{"object1", "object2", "object3"} - lb := NewThreadSafeLoadbalancer(set) - - fmt.Println(lb.Current()) - -} -``` - -#### Output - -``` -object1 -``` - -

-
- -### func \(\*Loadbalancer\[T\]\) [AddItems]() +### func \(\*Loadbalancer\[T\]\) [AddItems]() ```go func (l *Loadbalancer[T]) AddItems(items ...T) @@ -203,7 +171,7 @@ AddItems adds items to the Loadbalancer.

-### func \(\*Loadbalancer\[T\]\) [Current]() +### func \(\*Loadbalancer\[T\]\) [Current]() ```go func (l *Loadbalancer[T]) Current() T @@ -233,7 +201,7 @@ Current returns the current item in the slice, without advancing the Loadbalance

-### func \(\*Loadbalancer\[T\]\) [Next]() +### func \(\*Loadbalancer\[T\]\) [Next]() ```go func (l *Loadbalancer[T]) Next() T @@ -274,7 +242,7 @@ Next returns the next item in the slice. When the end of the slice is reached, i

-### func \(\*Loadbalancer\[T\]\) [Reset]() +### func \(\*Loadbalancer\[T\]\) [Reset]() ```go func (l *Loadbalancer[T]) Reset() 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..ffbbb81 100644 --- a/examples_test.go +++ b/examples_test.go @@ -1,20 +1,13 @@ -package robin +package robin_test -import "fmt" +import ( + "atomicgo.dev/robin" + "fmt" +) func ExampleNewLoadbalancer() { set := []string{"object1", "object2", "object3"} - lb := NewLoadbalancer(set) - - fmt.Println(lb.Current()) - - // Output: - // object1 -} - -func ExampleNewThreadSafeLoadbalancer() { - set := []string{"object1", "object2", "object3"} - lb := NewThreadSafeLoadbalancer(set) + lb := robin.NewLoadbalancer(set) fmt.Println(lb.Current()) @@ -24,7 +17,7 @@ func ExampleNewThreadSafeLoadbalancer() { func ExampleLoadbalancer_Current() { set := []int{1, 2, 3} - lb := NewLoadbalancer(set) + lb := robin.NewLoadbalancer(set) fmt.Println(lb.Current()) @@ -34,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) @@ -46,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() @@ -62,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()) 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()) } }