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

Replace conditional variable with semaphore #24

Closed
wants to merge 16 commits into from
24 changes: 24 additions & 0 deletions context.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package puddle

import (
"context"
"time"
)

// valueCancelCtx combines two contexts into one. One context is used for values and the other is used for cancellation.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why? Contexts compose, so you shouldn't have to create your own type. (But maybe I'm missing something?)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is just code relocation.

But to answer your question: There is no way how to combine values of one context and cancellation of another context. Not without 3rd party libraries.

type valueCancelCtx struct {
valueCtx context.Context
cancelCtx context.Context
}

func (ctx *valueCancelCtx) Deadline() (time.Time, bool) { return ctx.cancelCtx.Deadline() }
func (ctx *valueCancelCtx) Done() <-chan struct{} { return ctx.cancelCtx.Done() }
func (ctx *valueCancelCtx) Err() error { return ctx.cancelCtx.Err() }
func (ctx *valueCancelCtx) Value(key any) any { return ctx.valueCtx.Value(key) }

func newValueCancelCtx(valueCtx, cancelContext context.Context) context.Context {
return &valueCancelCtx{
valueCtx: valueCtx,
cancelCtx: cancelContext,
}
}
9 changes: 9 additions & 0 deletions export_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package puddle

import "context"

func (p *Pool[T]) AcquireRaw(ctx context.Context) (*Resource[T], error) {
return p.acquire(ctx)
}

var AcquireSemAll = acquireSemAll
4 changes: 2 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
module github.com/jackc/puddle/v2

go 1.18
go 1.19

require github.com/stretchr/testify v1.8.0

require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/stretchr/objx v0.4.0 // indirect
golang.org/x/sync v0.0.0-20220923202941-7f9b1623fab7
gopkg.in/yaml.v3 v3.0.1 // indirect
)
7 changes: 3 additions & 4 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,17 +1,16 @@
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/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
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/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0 h1:M2gUjqZET1qApGOWNSnZ49BAIMX4F/1plDv3+l31EJ4=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
golang.org/x/sync v0.0.0-20220923202941-7f9b1623fab7 h1:ZrnxWX62AgTKOSagEqxvb3ffipvEDX2pl7E1TdqLqIc=
golang.org/x/sync v0.0.0-20220923202941-7f9b1623fab7/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
Expand Down
85 changes: 85 additions & 0 deletions internal/genstack/gen_stack.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package genstack

// GenStack implements a generational stack.
//
// GenStack works as common stack except for the fact that all elements in the
// older generation are guaranteed to be popped before any element in the newer
// generation. New elements are always pushed to the current (newest)
// generation.
//
// We could also say that GenStack behaves as a stack in case of a single
// generation, but it behaves as a queue of individual generation stacks.
type GenStack[T any] struct {
// We can represent arbitrary number of generations using 2 stacks. The
// new stack stores all new pushes and the old stack serves all reads.
// Old stack can represent multiple generations. If old == new, then all
// elements pushed in previous (not current) generations have already
// been popped.

old *stack[T]
new *stack[T]
}

// NewGenStack creates a new empty GenStack.
func NewGenStack[T any]() *GenStack[T] {
s := &stack[T]{}
return &GenStack[T]{
old: s,
new: s,
}
}

func (s *GenStack[T]) Pop() (T, bool) {
// Pushes always append to the new stack, so if the old once becomes
// empty, it will remail empty forever.
if s.old.len() == 0 && s.old != s.new {
s.old = s.new
}

if s.old.len() == 0 {
var zero T
return zero, false
}

return s.old.pop(), true
}

// Push pushes a new element at the top of the stack.
func (s *GenStack[T]) Push(v T) { s.new.push(v) }

// NextGen starts a new stack generation.
func (s *GenStack[T]) NextGen() {
if s.old == s.new {
s.new = &stack[T]{}
return
}

// We need to pop from the old stack to the top of the new stack. Let's
// have an example:
//
// Old: <bottom> 4 3 2 1
// New: <bottom> 8 7 6 5
// PopOrder: 1 2 3 4 5 6 7 8
//
//
// To preserve pop order, we have to take all elements from the old
// stack and push them to the top of new stack:
//
// New: 8 7 6 5 4 3 2 1
//
s.new.push(s.old.takeAll()...)

// We have the old stack allocated and empty, so why not to reuse it as
// new new stack.
s.old, s.new = s.new, s.old
}

// Len returns number of elements in the stack.
func (s *GenStack[T]) Len() int {
l := s.old.len()
if s.old != s.new {
l += s.new.len()
}

return l
}
90 changes: 90 additions & 0 deletions internal/genstack/gen_stack_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
package genstack

import (
"testing"

"github.com/stretchr/testify/require"
)

func requirePopEmpty[T any](t testing.TB, s *GenStack[T]) {
v, ok := s.Pop()
require.False(t, ok)
require.Zero(t, v)
}

func requirePop[T any](t testing.TB, s *GenStack[T], expected T) {
v, ok := s.Pop()
require.True(t, ok)
require.Equal(t, expected, v)
}

func TestGenStack_Empty(t *testing.T) {
s := NewGenStack[int]()
requirePopEmpty(t, s)
}

func TestGenStack_SingleGen(t *testing.T) {
r := require.New(t)
s := NewGenStack[int]()

s.Push(1)
s.Push(2)
r.Equal(2, s.Len())

requirePop(t, s, 2)
requirePop(t, s, 1)
requirePopEmpty(t, s)
}

func TestGenStack_TwoGen(t *testing.T) {
r := require.New(t)
s := NewGenStack[int]()

s.Push(3)
s.Push(4)
s.Push(5)
r.Equal(3, s.Len())
s.NextGen()
r.Equal(3, s.Len())
s.Push(6)
s.Push(7)
r.Equal(5, s.Len())

requirePop(t, s, 5)
requirePop(t, s, 4)
requirePop(t, s, 3)
requirePop(t, s, 7)
requirePop(t, s, 6)
requirePopEmpty(t, s)
}

func TestGenStack_MuptiGen(t *testing.T) {
r := require.New(t)
s := NewGenStack[int]()

s.Push(10)
s.Push(11)
s.Push(12)
r.Equal(3, s.Len())
s.NextGen()
r.Equal(3, s.Len())
s.Push(13)
s.Push(14)
r.Equal(5, s.Len())
s.NextGen()
r.Equal(5, s.Len())
s.Push(15)
s.Push(16)
s.Push(17)
r.Equal(8, s.Len())

requirePop(t, s, 12)
requirePop(t, s, 11)
requirePop(t, s, 10)
requirePop(t, s, 14)
requirePop(t, s, 13)
requirePop(t, s, 17)
requirePop(t, s, 16)
requirePop(t, s, 15)
requirePopEmpty(t, s)
}
39 changes: 39 additions & 0 deletions internal/genstack/stack.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package genstack

// stack is a wrapper around an array implementing a stack.
//
// We cannot use slice to represent the stack because append might change the
// pointer value of the slice. That would be an issue in GenStack
// implementation.
type stack[T any] struct {
arr []T
}

// push pushes a new element at the top of a stack.
func (s *stack[T]) push(vs ...T) { s.arr = append(s.arr, vs...) }

// pop pops the stack top-most element.
//
// If stack length is zero, this method panics.
func (s *stack[T]) pop() T {
idx := s.len() - 1
val := s.arr[idx]

// Avoid memory leak
var zero T
s.arr[idx] = zero

s.arr = s.arr[:idx]
return val
}

// takeAll returns all elements in the stack in order as they are stored - i.e.
// the top-most stack element is the last one.
func (s *stack[T]) takeAll() []T {
arr := s.arr
s.arr = nil
return arr
}

// len returns number of elements in the stack.
func (s *stack[T]) len() int { return len(s.arr) }
12 changes: 0 additions & 12 deletions internal_test.go

This file was deleted.

32 changes: 32 additions & 0 deletions log.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package puddle

import "unsafe"

type ints interface {
int | int8 | int16 | int32 | int64 | uint | uint8 | uint16 | uint32 | uint64
}

// log2Int returns log2 of an integer. This function panics if val < 0. For val
// == 0, returns 0.
func log2Int[T ints](val T) uint8 {
if val <= 0 {
panic("log2 of non-positive number does not exist")
}

return log2IntRange(val, 0, uint8(8*unsafe.Sizeof(val)))
}

func log2IntRange[T ints](val T, begin, end uint8) uint8 {
length := end - begin
if length == 1 {
return begin
}

delim := begin + length/2
mask := T(1) << delim
if mask > val {
return log2IntRange(val, begin, delim)
} else {
return log2IntRange(val, delim, end)
}
}
49 changes: 49 additions & 0 deletions log_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package puddle

import (
"math"
"math/rand"
"testing"
"time"

"github.com/stretchr/testify/require"
)

func TestLog2Uint(t *testing.T) {
r := require.New(t)

r.Equal(uint8(0), log2Int(1))
r.Equal(uint8(0), log2Int[uint64](1))
r.Equal(uint8(1), log2Int[uint32](2))
r.Equal(uint8(7), log2Int[uint8](math.MaxUint8))
r.Equal(uint8(15), log2Int[uint16](math.MaxUint16))
r.Equal(uint8(31), log2Int[uint32](math.MaxUint32))
r.Equal(uint8(63), log2Int[uint64](math.MaxUint64))

r.Panics(func() { log2Int[uint64](0) })
r.Panics(func() { log2Int[int64](-1) })
}

func FuzzLog2Uint(f *testing.F) {
const cnt = 1000

rand := rand.New(rand.NewSource(time.Now().UnixNano()))
for i := 0; i < cnt; i++ {
val := uint64(rand.Int63())
// val + 1 not to test val == 0.
f.Add(val + 1)
}

f.Fuzz(func(t *testing.T, val uint64) {
var mx uint8
for i := 63; i >= 0; i-- {
mask := uint64(1) << i
if mask&val != 0 {
mx = uint8(i)
break
}
}

require.Equal(t, mx, log2Int(val))
})
}
Loading