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

connection pooling for duckdb #1405

Merged
merged 43 commits into from
Jan 4, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
1ffb6e8
connection pooling for duckdb
pjain1 Dec 8, 2022
c12984a
close channel
pjain1 Dec 8, 2022
5c9fa5b
set pool size
pjain1 Dec 8, 2022
a4158dc
fix test
pjain1 Dec 8, 2022
79f465d
fix stopping of pw
pjain1 Dec 8, 2022
4571c4c
formatting
pjain1 Dec 8, 2022
515f840
start mulitple jobs are getting unpaused
pjain1 Dec 8, 2022
a4b8dbf
conn pool dequeue
pjain1 Dec 8, 2022
da0d5e7
fix test
pjain1 Dec 8, 2022
ef1aa5f
better concurrency test of priority worker
pjain1 Dec 9, 2022
b5eccec
comment
pjain1 Dec 9, 2022
3be68ad
Merging with context affinity
begelundmuller Dec 15, 2022
d25fca0
Pass pool size through DSN
begelundmuller Dec 15, 2022
a83af54
Formatting
begelundmuller Dec 15, 2022
3692201
Added a priority semaphore
begelundmuller Dec 16, 2022
feceaf1
Update duckdb driver to use priority semaphore
begelundmuller Dec 16, 2022
189184d
Maybe fix failing test case
begelundmuller Dec 16, 2022
7ead16a
Document priorityqueue.Semaphore
begelundmuller Dec 16, 2022
c565dca
Bind time series to one connection
begelundmuller Dec 16, 2022
dd3b4d2
Use sqlx.Conn + fix hanging errors
begelundmuller Dec 16, 2022
f285a1b
Temporarily committing spending benchmark in examples
begelundmuller Dec 16, 2022
ecb1a6a
fix failing test
pjain1 Dec 17, 2022
b24142b
formatting
pjain1 Dec 17, 2022
ca82b58
Commit js benchmark
begelundmuller Dec 21, 2022
597b41f
Removing benchmarks
begelundmuller Dec 22, 2022
2248870
duckdb driver fix, tests with pool size > 1, separate meta connection
pjain1 Dec 22, 2022
2592da0
use conn from pool in migrate
pjain1 Dec 23, 2022
3f8224e
use single conn in migrate
pjain1 Dec 23, 2022
da6960a
use built in connection pool
pjain1 Dec 27, 2022
e50f987
fix conn release race condition
pjain1 Dec 28, 2022
7fa8f9d
fix linter errors
pjain1 Dec 28, 2022
36e207c
fmt
pjain1 Dec 28, 2022
e056910
gofmt fix
pjain1 Dec 28, 2022
0466f77
gofmt fix
pjain1 Dec 28, 2022
f6a49c3
fix tests
pjain1 Dec 28, 2022
52a80ee
upgrade duckdb driver
pjain1 Dec 29, 2022
7a10d70
Meta and OLAP sems; ensuring safe release
begelundmuller Jan 3, 2023
daeed05
Merge branch 'main' into conn_pool
begelundmuller Jan 3, 2023
42f1c5f
Use WithConnection for temporary tables
begelundmuller Jan 3, 2023
c3b9e6c
Merge branch 'main' into conn_pool
pjain1 Jan 4, 2023
3d544ac
formatting
pjain1 Jan 4, 2023
92f3cd0
Merge branch 'main' into conn_pool
begelundmuller Jan 4, 2023
ab7265d
Review
begelundmuller Jan 4, 2023
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
Prev Previous commit
Next Next commit
Added a priority semaphore
  • Loading branch information
begelundmuller authored and pjain1 committed Dec 23, 2022
commit 36922010d6fc718c83477f4ea6ea900b4980d187
87 changes: 87 additions & 0 deletions runtime/pkg/priorityqueue/priorityqueue.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package priorityqueue

import (
"container/heap"
)

// Item is a value in PriorityQueue
type Item[V any] struct {
Value V
priority int
index int
}

// PriorityQueue is a generic priority queue.
// It returns items with a higher priority first. It is not concurrency safe.
type PriorityQueue[V any] struct {
heap priorityHeap[V]
}

func New[V any]() *PriorityQueue[V] {
pq := &PriorityQueue[V]{}
heap.Init(&pq.heap)
return pq
}

func (pq *PriorityQueue[V]) Push(val V, priority int) *Item[V] {
itm := &Item[V]{
Value: val,
priority: priority,
index: -1,
}
heap.Push(&pq.heap, itm)
return itm
}

func (pq *PriorityQueue[V]) Pop() V {
itm := heap.Pop(&pq.heap).(*Item[V])
return itm.Value
}

func (pq *PriorityQueue[V]) Remove(itm *Item[V]) {
if itm.index >= 0 {
heap.Remove(&pq.heap, itm.index)
}
}

func (pq *PriorityQueue[V]) Contains(itm *Item[V]) bool {
return itm.index >= 0
}

func (pq *PriorityQueue[V]) Len() int {
return pq.heap.Len()
}

// priorityHeap implements heap.Interface to serve as a priority queue.
// See the heap docs for usage details: https://pkg.go.dev/container/heap#example-package-PriorityQueue
type priorityHeap[V any] []*Item[V]

func (pq priorityHeap[V]) Len() int { return len(pq) }

func (pq priorityHeap[V]) Less(i, j int) bool {
// We use greater than here so that Pop gives us the highest priority item (not lowest)
return pq[i].priority > pq[j].priority
}

func (pq priorityHeap[V]) Swap(i, j int) {
pq[i], pq[j] = pq[j], pq[i]
pq[i].index = i
pq[j].index = j
}

func (pq *priorityHeap[V]) Push(x any) {
n := len(*pq)
itm := x.(*Item[V])
itm.index = n
*pq = append(*pq, itm)
}

func (pq *priorityHeap[V]) Pop() any {
old := *pq
n := len(old)
itm := old[n-1]
old[n-1] = nil // avoid memory leak
itm.index = -1 // for safety
*pq = old[0 : n-1]
return itm
}
23 changes: 23 additions & 0 deletions runtime/pkg/priorityqueue/priorityqueue_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package priorityqueue

import (
"testing"

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

func TestPriorityQueue(t *testing.T) {
pq := New[int]()
require.Equal(t, 0, pq.Len())
pq.Push(1, 1)
pq.Push(2, 2)
itm := pq.Push(3, 3)
require.Equal(t, 3, pq.Len())
require.True(t, pq.Contains(itm))
pq.Remove(itm)
require.False(t, pq.Contains(itm))
require.Equal(t, 2, pq.Pop())
require.Equal(t, 1, pq.Len())
require.Equal(t, 1, pq.Pop())
require.Equal(t, 0, pq.Len())
}
87 changes: 87 additions & 0 deletions runtime/pkg/priorityqueue/semaphore.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package priorityqueue

import (
"context"
"sync"
)

type Semaphore struct {
mu sync.Mutex
pq *PriorityQueue[chan struct{}]
size int
cur int
}

func NewSemaphore(size int) *Semaphore {
return &Semaphore{
mu: sync.Mutex{},
pq: New[chan struct{}](),
size: size,
cur: 0,
}
}

func (s *Semaphore) Acquire(ctx context.Context, priority int) error {
s.mu.Lock()
if s.size-s.cur >= 1 && s.pq.Len() == 0 {
s.cur += 1
s.mu.Unlock()
return nil
}

readyCh := make(chan struct{})
itm := s.pq.Push(readyCh, priority)
s.mu.Unlock()

select {
case <-readyCh:
return nil
case <-ctx.Done():
s.mu.Lock()
if !s.pq.Contains(itm) {
// Cancelled and acquired at the same time. Easiest to pretend it was acquired first.
s.mu.Unlock()
return nil
}
s.pq.Remove(itm)
s.mu.Unlock()
return ctx.Err()
}
}

func (s *Semaphore) TryAcquire() bool {
s.mu.Lock()
ok := s.size-s.cur >= 1 && s.pq.Len() == 0
if ok {
s.cur += 1
}
s.mu.Unlock()
return ok
}

func (s *Semaphore) Release() {
s.mu.Lock()
s.cur -= 1
if s.cur < 0 {
s.mu.Unlock()
panic("semaphore released more times than acquired")
}
s.notifyWaiters()
s.mu.Unlock()
}

func (s *Semaphore) notifyWaiters() {
for {
if s.pq.Len() == 0 {
break
}

if s.cur == s.size {
break
}

readyCh := s.pq.Pop()
s.cur++
close(readyCh)
}
}
125 changes: 125 additions & 0 deletions runtime/pkg/priorityqueue/semaphore_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
package priorityqueue

import (
"context"
"testing"
"time"

"github.com/stretchr/testify/require"
"golang.org/x/sync/errgroup"
)

func TestSemaphoreSimple(t *testing.T) {
s := NewSemaphore(1)
err := s.Acquire(context.Background(), 1)
require.NoError(t, err)
s.Release()
err = s.Acquire(context.Background(), 1)
require.NoError(t, err)
s.Release()
require.True(t, s.TryAcquire())
require.False(t, s.TryAcquire())
s.Release()

s = NewSemaphore(2)
err = s.Acquire(context.Background(), 1)
require.NoError(t, err)
err = s.Acquire(context.Background(), 1)
require.NoError(t, err)
s.Release()
s.Release()
require.Panics(t, func() {
s.Release()
})
}

func TestSemaphorePriority(t *testing.T) {
// Prepare
n := 10
var results []int
var g errgroup.Group
s := NewSemaphore(1)

// Acquire to block
err := s.Acquire(context.Background(), 1)
require.NoError(t, err)
require.False(t, s.TryAcquire())

// Fill up queue
for i := 0; i <= n; i++ {
priority := i
g.Go(func() error {
err := s.Acquire(context.Background(), priority)
require.NoError(t, err)
results = append(results, priority)
s.Release()
return nil
})
}

// Wait a bit to ensure queue fills up
time.Sleep(time.Second)
s.Release()

// Wait for processing
err = g.Wait()
require.NoError(t, err)

// Check results evaluated in priority order
for i := 0; i <= n; i++ {
require.Equal(t, i, results[n-i])
}
}

func TestSemaphoreCancel(t *testing.T) {
// Prepare
n := 100
size := 4
cancelIdx := 50
results := make(chan int, n-1)
var g errgroup.Group
s := NewSemaphore(size)

// Acquire up to size
for i := 0; i < size; i++ {
err := s.Acquire(context.Background(), 1)
require.NoError(t, err)
}
require.False(t, s.TryAcquire())

// Fill up queue
for i := 0; i < n; i++ {
priority := i
g.Go(func() error {
ctx := context.Background()
if priority == cancelIdx {
cctx, cancel := context.WithCancel(ctx)
ctx = cctx
cancel()
}

err := s.Acquire(ctx, priority)
if priority == cancelIdx {
require.Error(t, err)
return nil
}

results <- priority
s.Release()
return nil
})
}

// Release to unblock for processing
for i := 0; i < size; i++ {
s.Release()
}

// Wait for processing
err := g.Wait()
require.NoError(t, err)

for i := 0; i < n-1; i++ {
require.NotEqual(t, cancelIdx, <-results)
}
}