Add projects

This commit is contained in:
Alois 2026-08-24 00:10:41 +02:00
commit 8b607dd700
Signed by: alois
SSH key fingerprint: SHA256:GBzT2DXvAuGV9XIV5W3WrzVpjU54FThmxHXdbz95J24
1802 changed files with 503346 additions and 2 deletions

View file

@ -0,0 +1,85 @@
package executionregistry
import (
"sync"
"testing"
)
type recordingReleaseSink struct {
mu sync.Mutex
sequences map[ReleaseGroup]int64
}
func (s *recordingReleaseSink) MarkDirty(group ReleaseGroup, sequence int64) {
s.mu.Lock()
defer s.mu.Unlock()
if s.sequences == nil {
s.sequences = make(map[ReleaseGroup]int64)
}
if sequence > s.sequences[group] {
s.sequences[group] = sequence
}
}
func (s *recordingReleaseSink) Sequence(credentialID, model string) int64 {
s.mu.Lock()
defer s.mu.Unlock()
return s.sequences[ReleaseGroup{CredentialID: credentialID, Model: model}]
}
func installAccountedScope(t *testing.T, registry *Registry, credentialID, model string) *Scope {
t.Helper()
pending, errBegin := registry.BeginDispatch()
if errBegin != nil {
t.Fatal(errBegin)
}
scope, errInstall := registry.Install(pending, ScopeSpec{CredentialID: credentialID, Model: model, Accounted: true})
if errInstall != nil {
t.Fatal(errInstall)
}
return scope
}
func TestRegistryEndMarksOneDirtyGroup(t *testing.T) {
sink := &recordingReleaseSink{}
registry := New()
registry.SetReleaseSink(sink.MarkDirty)
scope := installAccountedScope(t, registry, "cred-1", "gpt")
scope.End("complete")
scope.End("duplicate")
if got := sink.Sequence("cred-1", "gpt"); got != 1 {
t.Fatalf("release sequence = %d, want 1", got)
}
}
func TestUnaccountedScopeDoesNotRelease(t *testing.T) {
sink := &recordingReleaseSink{}
registry := New()
registry.SetReleaseSink(sink.MarkDirty)
pending, errBegin := registry.BeginDispatch()
if errBegin != nil {
t.Fatal(errBegin)
}
scope, errInstall := registry.Install(pending, ScopeSpec{CredentialID: "cred-1", Model: "gpt", Accounted: false})
if errInstall != nil {
t.Fatal(errInstall)
}
scope.End("observation_complete")
if got := sink.Sequence("cred-1", "gpt"); got != 0 {
t.Fatalf("release sequence = %d, want 0", got)
}
}
func TestSetReleaseSinkReplaysExistingSequences(t *testing.T) {
registry := New()
installAccountedScope(t, registry, "cred-1", "gpt").End("complete")
sink := &recordingReleaseSink{}
registry.SetReleaseSink(sink.MarkDirty)
if got := sink.Sequence("cred-1", "gpt"); got != 1 {
t.Fatalf("replayed release sequence = %d, want 1", got)
}
}

View file

@ -0,0 +1,74 @@
package executionregistry
import "time"
// Observation is an immutable in-flight execution snapshot entry.
type Observation struct {
RequestID string
CredentialID string
Model string
RequestKind string
StartedAt time.Time
Accounted bool
}
// Freeze is an immutable in-flight execution snapshot.
type Freeze struct {
Revision int64
BarrierRevision int64
Executions []Observation
}
// ObserveBarrier records the latest Home observation barrier.
func (r *Registry) ObserveBarrier(revision int64) {
if r == nil || revision <= 0 {
return
}
r.mu.Lock()
defer r.mu.Unlock()
if revision > r.observedBarrier {
r.observedBarrier = revision
r.pendingBarrierSequence = r.next
}
}
// FreezeInFlight copies all active executions into an immutable snapshot.
func (r *Registry) FreezeInFlight(_ time.Time) Freeze {
if r == nil {
return Freeze{}
}
r.mu.Lock()
defer r.mu.Unlock()
if r.observedBarrier > r.publishedBarrier {
blocked := false
for sequence := range r.pending {
if sequence <= r.pendingBarrierSequence {
blocked = true
break
}
}
if !blocked {
r.publishedBarrier = r.observedBarrier
}
}
r.snapshotRevision++
freeze := Freeze{
Revision: r.snapshotRevision,
BarrierRevision: r.publishedBarrier,
Executions: make([]Observation, 0, len(r.scopes)),
}
for _, scope := range r.scopes {
freeze.Executions = append(freeze.Executions, Observation{
RequestID: scope.spec.RequestID,
CredentialID: scope.spec.CredentialID,
Model: scope.spec.Model,
RequestKind: scope.spec.Kind,
StartedAt: scope.spec.StartedAt,
Accounted: scope.spec.Accounted,
})
}
return freeze
}

View file

@ -0,0 +1,45 @@
package executionregistry
import (
"testing"
"time"
)
func TestFreezeInFlightWaitsForPendingBarrierAndCopiesScopes(t *testing.T) {
registry := New()
pending, errBegin := registry.BeginDispatch()
if errBegin != nil {
t.Fatal(errBegin)
}
registry.ObserveBarrier(14)
before := registry.FreezeInFlight(time.Unix(12, 0).UTC())
if before.BarrierRevision != 0 {
t.Fatalf("barrier before install = %d", before.BarrierRevision)
}
scope, errInstall := registry.Install(pending, ScopeSpec{
RequestID: "req-a", CredentialID: "cred", Model: "gpt-5",
Kind: "http", StartedAt: time.Unix(10, 0).UTC(), Accounted: true,
})
if errInstall != nil {
t.Fatal(errInstall)
}
after := registry.FreezeInFlight(time.Unix(13, 0).UTC())
if after.BarrierRevision != 14 || len(after.Executions) != 1 || !after.Executions[0].Accounted {
t.Fatalf("freeze after install = %#v", after)
}
after.Executions[0].RequestID = "mutated"
copied := registry.FreezeInFlight(time.Unix(13, 0).UTC())
if len(copied.Executions) != 1 || copied.Executions[0].RequestID != "req-a" {
t.Fatalf("freeze did not copy scope = %#v", copied)
}
scope.End("completed")
ended := registry.FreezeInFlight(time.Unix(14, 0).UTC())
if len(ended.Executions) != 0 || ended.Revision <= after.Revision {
t.Fatalf("freeze after end = %#v", ended)
}
}

View file

@ -0,0 +1,470 @@
// Package executionregistry tracks Home-dispatched executions for one subscriber lifetime.
package executionregistry
import (
"context"
"errors"
"sync"
"sync/atomic"
"time"
log "github.com/sirupsen/logrus"
)
var (
ErrRegistryNotAccepting = errors.New("execution registry is not accepting dispatches")
ErrRegistryClosed = errors.New("execution registry is closed")
ErrInvalidPendingDispatch = errors.New("invalid pending dispatch")
ErrInvalidExecutionResource = errors.New("invalid execution resource")
ErrExecutionResourceAlreadyBound = errors.New("execution resource is already bound")
)
// State is the lifecycle state of a Registry.
type State uint32
const (
StateAccepting State = iota
StateDraining
StateClosed
)
// Registry owns all dispatches accepted during one Home subscriber lifetime.
type Registry struct {
state atomic.Uint32
mu sync.Mutex
next uint64
snapshotRevision int64
observedBarrier int64
pendingBarrierSequence uint64
publishedBarrier int64
pending map[uint64]*PendingDispatch
scopes map[uint64]*Scope
releaseSequences map[ReleaseGroup]int64
releaseSink ReleaseSink
changed chan struct{}
closeMu sync.Mutex
closeStarted bool
closeDone chan struct{}
closeErr error
}
// PendingDispatch reserves an execution slot until it is installed or ended.
type PendingDispatch struct {
id uint64
registry *Registry
mu sync.Mutex
once sync.Once
}
// ScopeSpec describes a Home-dispatched execution.
type ScopeSpec struct {
RequestID string
CredentialID string
Model string
Kind string
StartedAt time.Time
Accounted bool
}
// ReleaseGroup identifies the cumulative release sequence for one accounted credential and model.
type ReleaseGroup struct {
CredentialID string
Model string
}
// ReleaseTicket completes after Home acknowledges a cumulative release sequence.
type ReleaseTicket struct {
Group ReleaseGroup
Sequence int64
done <-chan struct{}
}
// NewReleaseTicket creates a ticket backed by done. A nil done channel represents
// a release sink that does not support acknowledgements.
func NewReleaseTicket(group ReleaseGroup, sequence int64, done <-chan struct{}) *ReleaseTicket {
if sequence <= 0 || done == nil {
return nil
}
return &ReleaseTicket{Group: group, Sequence: sequence, done: done}
}
// Wait blocks until Home acknowledges the release or ctx expires.
func (t *ReleaseTicket) Wait(ctx context.Context) error {
if t == nil || t.done == nil {
return nil
}
if ctx == nil {
ctx = context.Background()
}
select {
case <-t.done:
return nil
case <-ctx.Done():
return ctx.Err()
}
}
// ReleaseSink receives the latest cumulative sequence for a release group and
// optionally returns an acknowledgement ticket.
type ReleaseSink func(ReleaseGroup, int64) *ReleaseTicket
// Scope owns the resource for one installed execution.
type Scope struct {
id uint64
registry *Registry
spec ScopeSpec
mu sync.Mutex
closeFn func() error
closeDone chan struct{}
releaseTicket *ReleaseTicket
active bool
ended sync.Once
}
// New creates an accepting registry.
func New() *Registry {
registry := &Registry{
pending: make(map[uint64]*PendingDispatch),
scopes: make(map[uint64]*Scope),
releaseSequences: make(map[ReleaseGroup]int64),
changed: make(chan struct{}),
}
registry.state.Store(uint32(StateAccepting))
return registry
}
// BeginDispatch reserves a dispatch token while the registry accepts traffic.
func (r *Registry) BeginDispatch() (*PendingDispatch, error) {
if r == nil || State(r.state.Load()) != StateAccepting {
return nil, ErrRegistryNotAccepting
}
r.mu.Lock()
defer r.mu.Unlock()
if State(r.state.Load()) != StateAccepting {
return nil, ErrRegistryNotAccepting
}
r.next++
pending := &PendingDispatch{id: r.next, registry: r}
r.pending[pending.id] = pending
return pending, nil
}
// WaitPending waits until every dispatch with an unresolved Home response has ended or been installed.
func (r *Registry) WaitPending(ctx context.Context) error {
if r == nil {
return ErrRegistryClosed
}
if ctx == nil {
ctx = context.Background()
}
r.mu.Lock()
for len(r.pending) != 0 {
changed := r.changed
r.mu.Unlock()
select {
case <-ctx.Done():
return ctx.Err()
case <-changed:
}
r.mu.Lock()
}
r.mu.Unlock()
return nil
}
// End releases a dispatch token that was not installed.
func (p *PendingDispatch) End() {
if p == nil || p.registry == nil {
return
}
p.mu.Lock()
defer p.mu.Unlock()
p.once.Do(func() {
p.registry.mu.Lock()
delete(p.registry.pending, p.id)
p.registry.signalLocked()
p.registry.mu.Unlock()
})
}
// Install atomically turns a pending dispatch token into an active execution scope.
func (r *Registry) Install(pending *PendingDispatch, spec ScopeSpec) (*Scope, error) {
if r == nil || pending == nil || pending.registry != r {
return nil, ErrInvalidPendingDispatch
}
pending.mu.Lock()
defer pending.mu.Unlock()
r.mu.Lock()
defer r.mu.Unlock()
if State(r.state.Load()) != StateAccepting {
pending.once.Do(func() {})
delete(r.pending, pending.id)
r.signalLocked()
return nil, ErrRegistryNotAccepting
}
if _, exists := r.pending[pending.id]; !exists {
return nil, ErrInvalidPendingDispatch
}
pending.once.Do(func() {})
delete(r.pending, pending.id)
scope := &Scope{id: pending.id, registry: r, spec: spec, active: true}
r.scopes[scope.id] = scope
r.signalLocked()
return scope, nil
}
// SetReleaseSink replaces the cumulative release sink and replays every known group.
// Legacy callbacks remain supported but cannot provide acknowledgement tickets.
func (r *Registry) SetReleaseSink(rawSink any) {
if r == nil {
return
}
var sink ReleaseSink
switch typed := rawSink.(type) {
case nil:
case ReleaseSink:
sink = typed
case func(ReleaseGroup, int64) *ReleaseTicket:
sink = ReleaseSink(typed)
case func(ReleaseGroup, int64):
sink = func(group ReleaseGroup, sequence int64) *ReleaseTicket {
typed(group, sequence)
return nil
}
default:
return
}
r.mu.Lock()
r.releaseSink = sink
sequences := make(map[ReleaseGroup]int64, len(r.releaseSequences))
for group, sequence := range r.releaseSequences {
sequences[group] = sequence
}
r.mu.Unlock()
if sink == nil {
return
}
for group, sequence := range sequences {
if sequence > 0 {
sink(group, sequence)
}
}
}
// Bind attaches the execution resource. A scope accepts exactly one resource.
func (s *Scope) Bind(closeFn func() error) error {
if s == nil || s.registry == nil || closeFn == nil {
return ErrInvalidExecutionResource
}
s.registry.mu.Lock()
defer s.registry.mu.Unlock()
if State(s.registry.state.Load()) != StateAccepting || !s.active {
return ErrRegistryNotAccepting
}
s.mu.Lock()
defer s.mu.Unlock()
if s.closeFn != nil || s.closeDone != nil {
return ErrExecutionResourceAlreadyBound
}
s.closeFn = closeFn
return nil
}
// End closes the bound resource and releases this execution scope exactly once.
func (s *Scope) End(reason string) {
_ = s.EndWithRelease(reason)
}
// EndWithRelease closes the scope and returns the release acknowledgement ticket.
// The release sink is invoked without the registry mutex held.
func (s *Scope) EndWithRelease(_ string) *ReleaseTicket {
if s == nil || s.registry == nil {
return nil
}
var ticket *ReleaseTicket
s.ended.Do(func() {
s.registry.mu.Lock()
s.mu.Lock()
s.active = false
s.mu.Unlock()
s.registry.mu.Unlock()
s.waitForBoundResourceClose()
s.registry.mu.Lock()
releaseSink, releaseGroup, releaseSequence := s.registry.markReleasedLocked(s)
s.registry.mu.Unlock()
if releaseSink != nil && releaseSequence > 0 {
ticket = releaseSink(releaseGroup, releaseSequence)
}
s.mu.Lock()
s.releaseTicket = ticket
s.mu.Unlock()
s.registry.mu.Lock()
delete(s.registry.scopes, s.id)
s.registry.signalLocked()
s.registry.mu.Unlock()
})
s.mu.Lock()
ticket = s.releaseTicket
s.mu.Unlock()
return ticket
}
func (r *Registry) markReleasedLocked(scope *Scope) (ReleaseSink, ReleaseGroup, int64) {
if scope == nil || !scope.spec.Accounted {
return nil, ReleaseGroup{}, 0
}
group := ReleaseGroup{CredentialID: scope.spec.CredentialID, Model: scope.spec.Model}
r.releaseSequences[group]++
return r.releaseSink, group, r.releaseSequences[group]
}
func (s *Scope) startBoundResourceClose() <-chan struct{} {
s.mu.Lock()
defer s.mu.Unlock()
if s.closeDone != nil {
return s.closeDone
}
closeFn := s.closeFn
if closeFn == nil {
return nil
}
closeDone := make(chan struct{})
s.closeFn = nil
s.closeDone = closeDone
go func() {
s.closeResource(closeFn)
close(closeDone)
}()
return closeDone
}
func (s *Scope) waitForBoundResourceClose() {
if closeDone := s.startBoundResourceClose(); closeDone != nil {
<-closeDone
}
}
func (s *Scope) closeResource(closeFn func() error) {
if closeFn == nil {
return
}
if errClose := closeFn(); errClose != nil {
log.WithError(errClose).Warn("Home execution resource close failed")
}
}
// Drain rejects new work, cancels active resources, and waits for all owners to end.
func (r *Registry) Drain(ctx context.Context) error {
if r == nil {
return ErrRegistryClosed
}
if ctx == nil {
ctx = context.Background()
}
if !r.state.CompareAndSwap(uint32(StateAccepting), uint32(StateDraining)) && State(r.state.Load()) != StateDraining {
return ErrRegistryClosed
}
r.mu.Lock()
scopes := make([]*Scope, 0, len(r.scopes))
for _, scope := range r.scopes {
scopes = append(scopes, scope)
}
r.mu.Unlock()
for _, scope := range scopes {
scope.startBoundResourceClose()
}
r.mu.Lock()
for len(r.pending) != 0 || len(r.scopes) != 0 {
changed := r.changed
r.mu.Unlock()
select {
case <-ctx.Done():
return ctx.Err()
case <-changed:
}
r.mu.Lock()
}
r.state.Store(uint32(StateClosed))
r.mu.Unlock()
return nil
}
// Close permanently rejects new work and closes every currently bound resource.
func (r *Registry) Close() error {
if r == nil {
return ErrRegistryClosed
}
r.closeMu.Lock()
if r.closeStarted {
closeDone := r.closeDone
r.closeMu.Unlock()
<-closeDone
r.closeMu.Lock()
errClose := r.closeErr
r.closeMu.Unlock()
return errClose
}
if State(r.state.Load()) == StateClosed {
r.closeMu.Unlock()
return nil
}
r.closeStarted = true
r.closeDone = make(chan struct{})
closeDone := r.closeDone
r.closeMu.Unlock()
for {
state := State(r.state.Load())
if state == StateClosed || r.state.CompareAndSwap(uint32(state), uint32(StateClosed)) {
break
}
}
r.mu.Lock()
scopes := make([]*Scope, 0, len(r.scopes))
for _, scope := range r.scopes {
scopes = append(scopes, scope)
}
r.mu.Unlock()
for _, scope := range scopes {
scope.waitForBoundResourceClose()
}
r.closeMu.Lock()
errClose := r.closeErr
close(closeDone)
r.closeMu.Unlock()
return errClose
}
func (r *Registry) signalLocked() {
close(r.changed)
r.changed = make(chan struct{})
}

View file

@ -0,0 +1,385 @@
package executionregistry
import (
"context"
"errors"
"sync/atomic"
"testing"
"time"
)
func TestDrainRejectsLateInstallAndCancelsBoundScopes(t *testing.T) {
registry := New()
pending, errBegin := registry.BeginDispatch()
if errBegin != nil {
t.Fatal(errBegin)
}
scope, errInstall := registry.Install(pending, ScopeSpec{RequestID: "req-1", CredentialID: "cred-1", Model: "gpt", Kind: "http", StartedAt: time.Now()})
if errInstall != nil {
t.Fatal(errInstall)
}
closed := atomic.Int32{}
if errBind := scope.Bind(func() error {
closed.Add(1)
go scope.End("canceled")
return nil
}); errBind != nil {
t.Fatal(errBind)
}
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
if errDrain := registry.Drain(ctx); errDrain != nil {
t.Fatal(errDrain)
}
if closed.Load() != 1 {
t.Fatalf("close calls = %d", closed.Load())
}
if _, errLate := registry.BeginDispatch(); !errors.Is(errLate, ErrRegistryNotAccepting) {
t.Fatalf("late dispatch error = %v", errLate)
}
}
func TestScopeEndIsExactlyOnce(t *testing.T) {
registry := New()
pending, errBegin := registry.BeginDispatch()
if errBegin != nil {
t.Fatal(errBegin)
}
scope, errInstall := registry.Install(pending, ScopeSpec{})
if errInstall != nil {
t.Fatal(errInstall)
}
closed := atomic.Int32{}
if errBind := scope.Bind(func() error {
closed.Add(1)
return nil
}); errBind != nil {
t.Fatal(errBind)
}
done := make(chan struct{})
go func() {
scope.End("complete")
close(done)
}()
scope.End("duplicate")
<-done
if closed.Load() != 1 {
t.Fatalf("close calls = %d, want 1", closed.Load())
}
}
func TestDrainWaitsForPendingDispatch(t *testing.T) {
registry := New()
pending, errBegin := registry.BeginDispatch()
if errBegin != nil {
t.Fatal(errBegin)
}
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
done := make(chan error, 1)
go func() { done <- registry.Drain(ctx) }()
select {
case errDrain := <-done:
t.Fatalf("Drain() returned before pending dispatch ended: %v", errDrain)
case <-time.After(20 * time.Millisecond):
}
pending.End()
if errDrain := <-done; errDrain != nil {
t.Fatalf("Drain() error = %v", errDrain)
}
}
func TestWaitPendingDoesNotDrainActiveScope(t *testing.T) {
registry := New()
activePending, errBegin := registry.BeginDispatch()
if errBegin != nil {
t.Fatal(errBegin)
}
scope, errInstall := registry.Install(activePending, ScopeSpec{})
if errInstall != nil {
t.Fatal(errInstall)
}
defer scope.End("test cleanup")
pending, errBegin := registry.BeginDispatch()
if errBegin != nil {
t.Fatal(errBegin)
}
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
done := make(chan error, 1)
go func() { done <- registry.WaitPending(ctx) }()
select {
case errWait := <-done:
t.Fatalf("WaitPending() returned before pending dispatch ended: %v", errWait)
case <-time.After(20 * time.Millisecond):
}
pending.End()
if errWait := <-done; errWait != nil {
t.Fatalf("WaitPending() error = %v", errWait)
}
nextPending, errNext := registry.BeginDispatch()
if errNext != nil {
t.Fatalf("WaitPending() stopped registry acceptance: %v", errNext)
}
nextPending.End()
}
func TestDrainReturnsWhenBlockingResourceCloseExceedsContext(t *testing.T) {
registry := New()
pending, errBegin := registry.BeginDispatch()
if errBegin != nil {
t.Fatal(errBegin)
}
scope, errInstall := registry.Install(pending, ScopeSpec{})
if errInstall != nil {
t.Fatal(errInstall)
}
started := make(chan struct{})
release := make(chan struct{})
if errBind := scope.Bind(func() error {
close(started)
<-release
return nil
}); errBind != nil {
t.Fatal(errBind)
}
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Millisecond)
defer cancel()
errDrain := registry.Drain(ctx)
if !errors.Is(errDrain, context.DeadlineExceeded) {
t.Fatalf("Drain() error = %v, want context deadline exceeded", errDrain)
}
select {
case <-started:
default:
t.Fatal("Drain() did not start closing the bound resource")
}
if state := State(registry.state.Load()); state != StateDraining {
t.Fatalf("registry state = %v, want draining", state)
}
ended := make(chan struct{})
go func() {
scope.End("canceled")
close(ended)
}()
close(release)
select {
case <-ended:
case <-time.After(time.Second):
t.Fatal("Scope.End() did not wait for resource close completion")
}
if errDrain = registry.Drain(context.Background()); errDrain != nil {
t.Fatalf("Drain() after resource close = %v", errDrain)
}
}
func TestDrainWaitsForBlockingResourceClose(t *testing.T) {
registry := New()
pending, errBegin := registry.BeginDispatch()
if errBegin != nil {
t.Fatal(errBegin)
}
scope, errInstall := registry.Install(pending, ScopeSpec{})
if errInstall != nil {
t.Fatal(errInstall)
}
started := make(chan struct{})
release := make(chan struct{})
if errBind := scope.Bind(func() error {
close(started)
<-release
return nil
}); errBind != nil {
t.Fatal(errBind)
}
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
done := make(chan error, 1)
go func() { done <- registry.Drain(ctx) }()
select {
case <-started:
case <-time.After(time.Second):
t.Fatal("Drain() did not close the bound resource")
}
go scope.End("canceled")
select {
case errDrain := <-done:
t.Fatalf("Drain() returned before the resource close completed: %v", errDrain)
case <-time.After(20 * time.Millisecond):
}
close(release)
if errDrain := <-done; errDrain != nil {
t.Fatalf("Drain() error = %v", errDrain)
}
}
func TestConcurrentDrainWaitsForBlockingResourceClose(t *testing.T) {
registry := New()
pending, errBegin := registry.BeginDispatch()
if errBegin != nil {
t.Fatal(errBegin)
}
scope, errInstall := registry.Install(pending, ScopeSpec{})
if errInstall != nil {
t.Fatal(errInstall)
}
started := make(chan struct{})
release := make(chan struct{})
if errBind := scope.Bind(func() error {
close(started)
<-release
return nil
}); errBind != nil {
t.Fatal(errBind)
}
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
firstDrain := make(chan error, 1)
go func() { firstDrain <- registry.Drain(ctx) }()
select {
case <-started:
case <-time.After(time.Second):
t.Fatal("first Drain() did not close the bound resource")
}
ended := make(chan struct{})
go func() {
scope.End("canceled")
close(ended)
}()
select {
case <-ended:
t.Fatal("Scope.End() returned before the resource close completed")
case <-time.After(20 * time.Millisecond):
}
secondDrain := make(chan error, 1)
go func() { secondDrain <- registry.Drain(ctx) }()
select {
case errDrain := <-secondDrain:
t.Fatalf("second Drain() returned before resource close completed: %v", errDrain)
case <-time.After(20 * time.Millisecond):
}
close(release)
select {
case <-ended:
case <-time.After(time.Second):
t.Fatal("Scope.End() did not complete after the resource close")
}
if errDrain := <-firstDrain; errDrain != nil {
t.Fatalf("first Drain() error = %v", errDrain)
}
if errDrain := <-secondDrain; errDrain != nil {
t.Fatalf("second Drain() error = %v", errDrain)
}
}
func TestConcurrentCloseWaitsForBlockingResourceClose(t *testing.T) {
registry := New()
pending, errBegin := registry.BeginDispatch()
if errBegin != nil {
t.Fatal(errBegin)
}
scope, errInstall := registry.Install(pending, ScopeSpec{})
if errInstall != nil {
t.Fatal(errInstall)
}
started := make(chan struct{})
release := make(chan struct{})
if errBind := scope.Bind(func() error {
close(started)
<-release
return nil
}); errBind != nil {
t.Fatal(errBind)
}
firstClose := make(chan error, 1)
go func() { firstClose <- registry.Close() }()
select {
case <-started:
case <-time.After(time.Second):
t.Fatal("first Close() did not close the bound resource")
}
secondClose := make(chan error, 1)
go func() { secondClose <- registry.Close() }()
select {
case errClose := <-secondClose:
t.Fatalf("second Close() returned before resource close completed: %v", errClose)
case <-time.After(20 * time.Millisecond):
}
close(release)
if errClose := <-firstClose; errClose != nil {
t.Fatalf("first Close() error = %v", errClose)
}
if errClose := <-secondClose; errClose != nil {
t.Fatalf("second Close() error = %v", errClose)
}
}
func TestDrainRejectsLateBind(t *testing.T) {
registry := New()
pending, errBegin := registry.BeginDispatch()
if errBegin != nil {
t.Fatal(errBegin)
}
scope, errInstall := registry.Install(pending, ScopeSpec{})
if errInstall != nil {
t.Fatal(errInstall)
}
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
done := make(chan error, 1)
go func() { done <- registry.Drain(ctx) }()
deadline := time.After(time.Second)
for State(registry.state.Load()) == StateAccepting {
select {
case <-deadline:
t.Fatal("registry did not begin draining")
default:
time.Sleep(time.Millisecond)
}
}
if errBind := scope.Bind(func() error { return nil }); !errors.Is(errBind, ErrRegistryNotAccepting) {
t.Fatalf("Bind() error = %v, want ErrRegistryNotAccepting", errBind)
}
scope.End("canceled")
if errDrain := <-done; errDrain != nil {
t.Fatalf("Drain() error = %v", errDrain)
}
}
func TestDrainRejectsLateInstall(t *testing.T) {
registry := New()
pending, errBegin := registry.BeginDispatch()
if errBegin != nil {
t.Fatal(errBegin)
}
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
done := make(chan error, 1)
go func() { done <- registry.Drain(ctx) }()
deadline := time.After(time.Second)
for State(registry.state.Load()) == StateAccepting {
select {
case <-deadline:
t.Fatal("registry did not begin draining")
default:
time.Sleep(time.Millisecond)
}
}
if _, errInstall := registry.Install(pending, ScopeSpec{}); !errors.Is(errInstall, ErrRegistryNotAccepting) {
t.Fatalf("Install() error = %v, want ErrRegistryNotAccepting", errInstall)
}
if errDrain := <-done; errDrain != nil {
t.Fatalf("Drain() error = %v", errDrain)
}
}