Add sequencer package
This commit is contained in:
parent
3765b4240b
commit
f291855f97
|
@ -0,0 +1,128 @@
|
||||||
|
// Copyright (C) 2025 Opsmate, Inc.
|
||||||
|
//
|
||||||
|
// This Source Code Form is subject to the terms of the Mozilla
|
||||||
|
// Public License, v. 2.0. If a copy of the MPL was not distributed
|
||||||
|
// with this file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
|
//
|
||||||
|
// This software is distributed WITHOUT A WARRANTY OF ANY KIND.
|
||||||
|
// See the Mozilla Public License for details.
|
||||||
|
|
||||||
|
package sequencer
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"slices"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
type seqWriter struct {
|
||||||
|
seqNbr uint64
|
||||||
|
ready chan<- struct{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Channel[T] is a multi-producer, single-consumer channel of items with monotonicaly-increasing sequence numbers.
|
||||||
|
// Items can be sent in any order, but they are always received in order of their sequence number.
|
||||||
|
type Channel[T any] struct {
|
||||||
|
mu sync.Mutex
|
||||||
|
next uint64
|
||||||
|
buf []*T
|
||||||
|
writers []seqWriter
|
||||||
|
readWaiting bool
|
||||||
|
readReady chan struct{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func New[T any](initialSequenceNumber uint64, capacity uint64) *Channel[T] {
|
||||||
|
return &Channel[T]{
|
||||||
|
buf: make([]*T, capacity),
|
||||||
|
next: initialSequenceNumber,
|
||||||
|
readReady: make(chan struct{}, 1),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (seq *Channel[T]) parkWriter(seqNbr uint64) <-chan struct{} {
|
||||||
|
ready := make(chan struct{})
|
||||||
|
seq.writers = append(seq.writers, seqWriter{seqNbr: seqNbr, ready: ready})
|
||||||
|
return ready
|
||||||
|
}
|
||||||
|
|
||||||
|
func (seq *Channel[T]) signalWriter(seqNbr uint64) {
|
||||||
|
for i := range seq.writers {
|
||||||
|
if seq.writers[i].seqNbr == seqNbr {
|
||||||
|
close(seq.writers[i].ready)
|
||||||
|
seq.writers = slices.Delete(seq.writers, i, i+1)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (seq *Channel[T]) forgetWriter(seqNbr uint64) {
|
||||||
|
for i := range seq.writers {
|
||||||
|
if seq.writers[i].seqNbr == seqNbr {
|
||||||
|
seq.writers = slices.Delete(seq.writers, i, i+1)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (seq *Channel[T]) Cap() uint64 {
|
||||||
|
return uint64(len(seq.buf))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (seq *Channel[T]) index(seqNbr uint64) int {
|
||||||
|
return int(seqNbr % seq.Cap())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send an item with the given sequence number. Blocks if the channel does not have capacity for the item.
|
||||||
|
// It is undefined behavior to send a sequence number that has previously been sent.
|
||||||
|
func (seq *Channel[T]) Add(ctx context.Context, sequenceNumber uint64, item *T) error {
|
||||||
|
seq.mu.Lock()
|
||||||
|
if sequenceNumber >= seq.next+seq.Cap() {
|
||||||
|
ready := seq.parkWriter(sequenceNumber)
|
||||||
|
seq.mu.Unlock()
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
seq.mu.Lock()
|
||||||
|
seq.forgetWriter(sequenceNumber)
|
||||||
|
seq.mu.Unlock()
|
||||||
|
return ctx.Err()
|
||||||
|
case <-ready:
|
||||||
|
}
|
||||||
|
seq.mu.Lock()
|
||||||
|
}
|
||||||
|
seq.buf[seq.index(sequenceNumber)] = item
|
||||||
|
if sequenceNumber == seq.next && seq.readWaiting {
|
||||||
|
seq.readReady <- struct{}{}
|
||||||
|
}
|
||||||
|
seq.mu.Unlock()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return the item with the next sequence number, blocking if necessary.
|
||||||
|
// Not safe to call concurrently with other Next calls.
|
||||||
|
func (seq *Channel[T]) Next(ctx context.Context) (*T, error) {
|
||||||
|
seq.mu.Lock()
|
||||||
|
if seq.buf[seq.index(seq.next)] == nil {
|
||||||
|
seq.readWaiting = true
|
||||||
|
seq.mu.Unlock()
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
seq.mu.Lock()
|
||||||
|
select {
|
||||||
|
case <-seq.readReady:
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
seq.readWaiting = false
|
||||||
|
seq.mu.Unlock()
|
||||||
|
return nil, ctx.Err()
|
||||||
|
case <-seq.readReady:
|
||||||
|
}
|
||||||
|
seq.mu.Lock()
|
||||||
|
seq.readWaiting = false
|
||||||
|
}
|
||||||
|
item := seq.buf[seq.index(seq.next)]
|
||||||
|
seq.buf[seq.index(seq.next)] = nil
|
||||||
|
seq.signalWriter(seq.next + seq.Cap())
|
||||||
|
seq.next++
|
||||||
|
seq.mu.Unlock()
|
||||||
|
return item, nil
|
||||||
|
}
|
|
@ -0,0 +1,148 @@
|
||||||
|
// Copyright (C) 2025 Opsmate, Inc.
|
||||||
|
//
|
||||||
|
// This Source Code Form is subject to the terms of the Mozilla
|
||||||
|
// Public License, v. 2.0. If a copy of the MPL was not distributed
|
||||||
|
// with this file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
|
//
|
||||||
|
// This software is distributed WITHOUT A WARRANTY OF ANY KIND.
|
||||||
|
// See the Mozilla Public License for details.
|
||||||
|
|
||||||
|
package sequencer
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
mathrand "math/rand/v2"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSequencerBasic(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
seq := New[uint64](0, 100)
|
||||||
|
go func() {
|
||||||
|
for i := range uint64(10_000) {
|
||||||
|
err := seq.Add(ctx, i, &i)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("%d: seq.Add returned unexpected error %v", i, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
for i := range uint64(10_000) {
|
||||||
|
next, err := seq.Next(ctx)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("%d: seq.Next returned unexpected error %v", i, err)
|
||||||
|
}
|
||||||
|
if *next != i {
|
||||||
|
t.Fatalf("%d: got unexpected value %d", i, *next)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSequencerNonZeroStart(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
seq := New[uint64](10, 100)
|
||||||
|
go func() {
|
||||||
|
for i := range uint64(10_000) {
|
||||||
|
err := seq.Add(ctx, i+10, &i)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("%d: seq.Add returned unexpected error %v", i, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
for i := range uint64(10_000) {
|
||||||
|
next, err := seq.Next(ctx)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("%d: seq.Next returned unexpected error %v", i, err)
|
||||||
|
}
|
||||||
|
if *next != i {
|
||||||
|
t.Fatalf("%d: got unexpected value %d", i, *next)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSequencerCapacity1(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
seq := New[uint64](0, 1)
|
||||||
|
go func() {
|
||||||
|
for i := range uint64(10_000) {
|
||||||
|
err := seq.Add(ctx, i, &i)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("%d: seq.Add returned unexpected error %v", i, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
for i := range uint64(10_000) {
|
||||||
|
next, err := seq.Next(ctx)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("%d: seq.Next returned unexpected error %v", i, err)
|
||||||
|
}
|
||||||
|
if *next != i {
|
||||||
|
t.Fatalf("%d: got unexpected value %d", i, *next)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSequencerTimeout(t *testing.T) {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
seq := New[uint64](0, 10_000)
|
||||||
|
go func() {
|
||||||
|
var i uint64
|
||||||
|
for {
|
||||||
|
newI := i
|
||||||
|
err := seq.Add(ctx, i, &newI)
|
||||||
|
if err != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
var i uint64
|
||||||
|
for {
|
||||||
|
next, err := seq.Next(ctx)
|
||||||
|
if err != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if *next != i {
|
||||||
|
t.Fatalf("%d: got unexpected value %d", i, *next)
|
||||||
|
}
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSequencerOutOfOrder(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
seq := New[uint64](0, 100)
|
||||||
|
ch := make(chan uint64)
|
||||||
|
go func() {
|
||||||
|
for i := range uint64(10_000) {
|
||||||
|
ch <- i
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
for range 4 {
|
||||||
|
go func() {
|
||||||
|
for i := range ch {
|
||||||
|
time.Sleep(mathrand.N(10*time.Millisecond))
|
||||||
|
//t.Logf("seq.Add %d", i)
|
||||||
|
err := seq.Add(ctx, i, &i)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("%d: seq.Add returned unexpected error %v", i, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
for i := range uint64(10_000) {
|
||||||
|
next, err := seq.Next(ctx)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("%d: seq.Next returned unexpected error %v", i, err)
|
||||||
|
}
|
||||||
|
if *next != i {
|
||||||
|
t.Fatalf("%d: got unexpected value %d", i, *next)
|
||||||
|
}
|
||||||
|
//t.Logf("seq.Next %d", i)
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue