refactor: make ownership repository context aware
This commit is contained in:
parent
179e4d5a0c
commit
c8e64758da
@ -1,6 +1,7 @@
|
||||
package pool
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
ownershipDomain "proxy-pool/internal/domain/ownership"
|
||||
@ -30,44 +31,62 @@ func NewOwnershipManager(repository ownershipDomain.Repository) (*OwnershipManag
|
||||
return &OwnershipManager{repository: repository}, nil
|
||||
}
|
||||
|
||||
func (m *OwnershipManager) Assign(now time.Time, proxyID, workerID string, ttl time.Duration) (Assignment, error) {
|
||||
if m == nil || m.repository == nil {
|
||||
func (m *OwnershipManager) Assign(ctx context.Context, now time.Time, proxyID, workerID string, ttl time.Duration) (Assignment, error) {
|
||||
if m == nil || m.repository == nil || ctx == nil {
|
||||
return Assignment{}, ErrInvalidOwnership
|
||||
}
|
||||
return m.repository.Assign(now, proxyID, workerID, ttl)
|
||||
if err := ctx.Err(); err != nil {
|
||||
return Assignment{}, err
|
||||
}
|
||||
return m.repository.Assign(ctx, now, proxyID, workerID, ttl)
|
||||
}
|
||||
|
||||
func (m *OwnershipManager) Renew(now time.Time, proxyID, workerID string, epoch uint64, ttl time.Duration) (Assignment, error) {
|
||||
if m == nil || m.repository == nil {
|
||||
func (m *OwnershipManager) Renew(ctx context.Context, now time.Time, proxyID, workerID string, epoch uint64, ttl time.Duration) (Assignment, error) {
|
||||
if m == nil || m.repository == nil || ctx == nil {
|
||||
return Assignment{}, ErrInvalidOwnership
|
||||
}
|
||||
return m.repository.Renew(now, proxyID, workerID, epoch, ttl)
|
||||
if err := ctx.Err(); err != nil {
|
||||
return Assignment{}, err
|
||||
}
|
||||
return m.repository.Renew(ctx, now, proxyID, workerID, epoch, ttl)
|
||||
}
|
||||
|
||||
func (m *OwnershipManager) BeginDrain(proxyID, workerID string, epoch uint64) (Assignment, error) {
|
||||
if m == nil || m.repository == nil {
|
||||
func (m *OwnershipManager) BeginDrain(ctx context.Context, proxyID, workerID string, epoch uint64) (Assignment, error) {
|
||||
if m == nil || m.repository == nil || ctx == nil {
|
||||
return Assignment{}, ErrInvalidOwnership
|
||||
}
|
||||
return m.repository.BeginDrain(proxyID, workerID, epoch)
|
||||
if err := ctx.Err(); err != nil {
|
||||
return Assignment{}, err
|
||||
}
|
||||
return m.repository.BeginDrain(ctx, proxyID, workerID, epoch)
|
||||
}
|
||||
|
||||
func (m *OwnershipManager) AcknowledgeDrain(proxyID, workerID string, epoch uint64, active, reserved int64) error {
|
||||
if m == nil || m.repository == nil {
|
||||
func (m *OwnershipManager) AcknowledgeDrain(ctx context.Context, proxyID, workerID string, epoch uint64, active, reserved int64) error {
|
||||
if m == nil || m.repository == nil || ctx == nil {
|
||||
return ErrInvalidOwnership
|
||||
}
|
||||
return m.repository.AcknowledgeDrain(proxyID, workerID, epoch, active, reserved)
|
||||
if err := ctx.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
return m.repository.AcknowledgeDrain(ctx, proxyID, workerID, epoch, active, reserved)
|
||||
}
|
||||
|
||||
func (m *OwnershipManager) Get(proxyID string) (Assignment, bool) {
|
||||
if m == nil || m.repository == nil {
|
||||
return Assignment{}, false
|
||||
func (m *OwnershipManager) Get(ctx context.Context, proxyID string) (Assignment, bool, error) {
|
||||
if m == nil || m.repository == nil || ctx == nil {
|
||||
return Assignment{}, false, ErrInvalidOwnership
|
||||
}
|
||||
return m.repository.Get(proxyID)
|
||||
if err := ctx.Err(); err != nil {
|
||||
return Assignment{}, false, err
|
||||
}
|
||||
return m.repository.Get(ctx, proxyID)
|
||||
}
|
||||
|
||||
func (m *OwnershipManager) Expire(now time.Time) []Assignment {
|
||||
if m == nil || m.repository == nil {
|
||||
return nil
|
||||
func (m *OwnershipManager) Expire(ctx context.Context, now time.Time, limit int) ([]Assignment, error) {
|
||||
if m == nil || m.repository == nil || ctx == nil || limit <= 0 {
|
||||
return nil, ErrInvalidOwnership
|
||||
}
|
||||
return m.repository.Expire(now)
|
||||
if err := ctx.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return m.repository.Expire(ctx, now, limit)
|
||||
}
|
||||
|
||||
@ -11,6 +11,7 @@ import (
|
||||
|
||||
"proxy-pool/internal/domain/activitypool"
|
||||
extractionDomain "proxy-pool/internal/domain/extraction"
|
||||
ownershipDomain "proxy-pool/internal/domain/ownership"
|
||||
proxyDomain "proxy-pool/internal/domain/proxy"
|
||||
)
|
||||
|
||||
@ -24,7 +25,7 @@ func TestOwnershipManagerPreventsDualAssignment(t *testing.T) {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
_, err := manager.Assign(now, "proxy-1", fmt.Sprintf("worker-%d", index), time.Minute)
|
||||
_, err := manager.Assign(context.Background(), now, "proxy-1", fmt.Sprintf("worker-%d", index), time.Minute)
|
||||
if err == nil {
|
||||
succeeded.Add(1)
|
||||
return
|
||||
@ -44,21 +45,22 @@ func TestOwnershipManagerPreventsDualAssignment(t *testing.T) {
|
||||
func TestOwnershipManagerRenewsOnlyCurrentAssignment(t *testing.T) {
|
||||
manager := newTestOwnershipManager(t, "proxy-1")
|
||||
now := time.Date(2026, 7, 28, 12, 0, 0, 0, time.UTC)
|
||||
assigned, err := manager.Assign(now, "proxy-1", "worker-1", time.Minute)
|
||||
ctx := context.Background()
|
||||
assigned, err := manager.Assign(ctx, now, "proxy-1", "worker-1", time.Minute)
|
||||
if err != nil {
|
||||
t.Fatalf("Assign(): %v", err)
|
||||
}
|
||||
renewed, err := manager.Renew(now.Add(30*time.Second), "proxy-1", "worker-1", assigned.Epoch, time.Minute)
|
||||
renewed, err := manager.Renew(ctx, now.Add(30*time.Second), "proxy-1", "worker-1", assigned.Epoch, time.Minute)
|
||||
if err != nil {
|
||||
t.Fatalf("Renew(): %v", err)
|
||||
}
|
||||
if renewed.Version != assigned.Version+1 || !renewed.ExpiresAt.Equal(now.Add(90*time.Second)) {
|
||||
t.Fatalf("renewed assignment = %+v", renewed)
|
||||
}
|
||||
if _, err := manager.Renew(now, "proxy-1", "worker-2", assigned.Epoch, time.Minute); !errors.Is(err, ErrStaleAssignment) {
|
||||
if _, err := manager.Renew(ctx, now, "proxy-1", "worker-2", assigned.Epoch, time.Minute); !errors.Is(err, ErrStaleAssignment) {
|
||||
t.Fatalf("Renew(stale) error = %v, want ErrStaleAssignment", err)
|
||||
}
|
||||
if expired := manager.Expire(now.Add(time.Minute)); len(expired) != 0 {
|
||||
if expired, err := manager.Expire(ctx, now.Add(time.Minute), 32); err != nil || len(expired) != 0 {
|
||||
t.Fatalf("renewed assignment expired at old deadline: %+v", expired)
|
||||
}
|
||||
}
|
||||
@ -77,7 +79,7 @@ func TestSharedRepositoryMakesOwnershipAndExtractionMutuallyExclusive(t *testing
|
||||
extracted := make(chan bool, 1)
|
||||
go func() {
|
||||
<-start
|
||||
_, assignErr := manager.Assign(now, "proxy-1", "worker-1", time.Minute)
|
||||
_, assignErr := manager.Assign(context.Background(), now, "proxy-1", "worker-1", time.Minute)
|
||||
if assignErr != nil && !errors.Is(assignErr, ErrOwnershipUnavailable) {
|
||||
t.Errorf("iteration %d Assign(): %v", iteration, assignErr)
|
||||
}
|
||||
@ -111,11 +113,12 @@ func TestSharedRepositoryMakesOwnershipAndExtractionMutuallyExclusive(t *testing
|
||||
func TestOwnershipManagerRequiresDrainAckAtZeroRuntime(t *testing.T) {
|
||||
manager := newTestOwnershipManager(t, "proxy-1")
|
||||
now := time.Date(2026, 7, 28, 12, 0, 0, 0, time.UTC)
|
||||
assignment, err := manager.Assign(now, "proxy-1", "worker-1", time.Minute)
|
||||
ctx := context.Background()
|
||||
assignment, err := manager.Assign(ctx, now, "proxy-1", "worker-1", time.Minute)
|
||||
if err != nil {
|
||||
t.Fatalf("Assign(): %v", err)
|
||||
}
|
||||
draining, err := manager.BeginDrain("proxy-1", "worker-1", assignment.Epoch)
|
||||
draining, err := manager.BeginDrain(ctx, "proxy-1", "worker-1", assignment.Epoch)
|
||||
if err != nil {
|
||||
t.Fatalf("BeginDrain(): %v", err)
|
||||
}
|
||||
@ -123,13 +126,13 @@ func TestOwnershipManagerRequiresDrainAckAtZeroRuntime(t *testing.T) {
|
||||
t.Fatalf("draining assignment = %+v", draining)
|
||||
}
|
||||
|
||||
if err := manager.AcknowledgeDrain("proxy-1", "worker-1", assignment.Epoch, 1, 0); !errors.Is(err, ErrDrainNotReady) {
|
||||
if err := manager.AcknowledgeDrain(ctx, "proxy-1", "worker-1", assignment.Epoch, 1, 0); !errors.Is(err, ErrDrainNotReady) {
|
||||
t.Fatalf("AcknowledgeDrain(active) error = %v, want ErrDrainNotReady", err)
|
||||
}
|
||||
if err := manager.AcknowledgeDrain("proxy-1", "worker-1", assignment.Epoch, 0, 0); err != nil {
|
||||
if err := manager.AcknowledgeDrain(ctx, "proxy-1", "worker-1", assignment.Epoch, 0, 0); err != nil {
|
||||
t.Fatalf("AcknowledgeDrain(zero): %v", err)
|
||||
}
|
||||
if _, ok := manager.Get("proxy-1"); ok {
|
||||
if _, ok, err := manager.Get(ctx, "proxy-1"); err != nil || ok {
|
||||
t.Fatal("assignment still exists after drain acknowledgement")
|
||||
}
|
||||
}
|
||||
@ -137,18 +140,19 @@ func TestOwnershipManagerRequiresDrainAckAtZeroRuntime(t *testing.T) {
|
||||
func TestOwnershipManagerExpiresCrashedWorkerAssignment(t *testing.T) {
|
||||
manager := newTestOwnershipManager(t, "proxy-1")
|
||||
now := time.Date(2026, 7, 28, 12, 0, 0, 0, time.UTC)
|
||||
first, err := manager.Assign(now, "proxy-1", "worker-1", time.Minute)
|
||||
ctx := context.Background()
|
||||
first, err := manager.Assign(ctx, now, "proxy-1", "worker-1", time.Minute)
|
||||
if err != nil {
|
||||
t.Fatalf("Assign(first): %v", err)
|
||||
}
|
||||
if expired := manager.Expire(now.Add(59 * time.Second)); len(expired) != 0 {
|
||||
if expired, err := manager.Expire(ctx, now.Add(59*time.Second), 32); err != nil || len(expired) != 0 {
|
||||
t.Fatalf("expired early: %+v", expired)
|
||||
}
|
||||
if expired := manager.Expire(now.Add(time.Minute)); len(expired) != 1 || expired[0].ProxyID != "proxy-1" {
|
||||
if expired, err := manager.Expire(ctx, now.Add(time.Minute), 32); err != nil || len(expired) != 1 || expired[0].ProxyID != "proxy-1" {
|
||||
t.Fatalf("Expire() = %+v, want proxy-1", expired)
|
||||
}
|
||||
|
||||
second, err := manager.Assign(now.Add(time.Minute), "proxy-1", "worker-2", time.Minute)
|
||||
second, err := manager.Assign(ctx, now.Add(time.Minute), "proxy-1", "worker-2", time.Minute)
|
||||
if err != nil {
|
||||
t.Fatalf("Assign(second): %v", err)
|
||||
}
|
||||
@ -157,6 +161,125 @@ func TestOwnershipManagerExpiresCrashedWorkerAssignment(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestOwnershipManagerPropagatesCanceledContextWithoutCallingRepository(t *testing.T) {
|
||||
repository := &ownershipRepositoryStub{}
|
||||
manager, err := NewOwnershipManager(repository)
|
||||
if err != nil {
|
||||
t.Fatalf("NewOwnershipManager(): %v", err)
|
||||
}
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cancel()
|
||||
|
||||
_, err = manager.Assign(ctx, time.Now(), "proxy-1", "worker-1", time.Minute)
|
||||
if !errors.Is(err, context.Canceled) {
|
||||
t.Fatalf("Assign() error = %v, want context.Canceled", err)
|
||||
}
|
||||
if repository.assignCalled {
|
||||
t.Fatal("repository Assign called with canceled context")
|
||||
}
|
||||
}
|
||||
|
||||
func TestOwnershipManagerPassesContextAndRepositoryError(t *testing.T) {
|
||||
storageErr := errors.New("redis unavailable")
|
||||
ctx := context.WithValue(context.Background(), ownershipContextKey{}, "request-1")
|
||||
repository := &ownershipRepositoryStub{
|
||||
assign: func(got context.Context, _ time.Time, _, _ string, _ time.Duration) (Assignment, error) {
|
||||
if got != ctx {
|
||||
t.Fatal("Assign() did not pass the original context")
|
||||
}
|
||||
return Assignment{}, storageErr
|
||||
},
|
||||
}
|
||||
manager, err := NewOwnershipManager(repository)
|
||||
if err != nil {
|
||||
t.Fatalf("NewOwnershipManager(): %v", err)
|
||||
}
|
||||
|
||||
_, err = manager.Assign(ctx, time.Now(), "proxy-1", "worker-1", time.Minute)
|
||||
if !errors.Is(err, storageErr) {
|
||||
t.Fatalf("Assign() error = %v, want repository error", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOwnershipManagerPassesExpireLimit(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
now := time.Date(2026, 7, 29, 12, 0, 0, 0, time.UTC)
|
||||
want := []Assignment{{ProxyID: "proxy-1"}}
|
||||
repository := &ownershipRepositoryStub{
|
||||
expire: func(got context.Context, gotNow time.Time, gotLimit int) ([]Assignment, error) {
|
||||
if got != ctx || !gotNow.Equal(now) || gotLimit != 32 {
|
||||
t.Fatalf("Expire() arguments = (%v, %v, %d)", got, gotNow, gotLimit)
|
||||
}
|
||||
return want, nil
|
||||
},
|
||||
}
|
||||
manager, err := NewOwnershipManager(repository)
|
||||
if err != nil {
|
||||
t.Fatalf("NewOwnershipManager(): %v", err)
|
||||
}
|
||||
|
||||
got, err := manager.Expire(ctx, now, 32)
|
||||
if err != nil || len(got) != 1 || got[0].ProxyID != want[0].ProxyID {
|
||||
t.Fatalf("Expire() = %+v, %v", got, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOwnershipManagerRejectsInvalidContextAndExpireLimit(t *testing.T) {
|
||||
manager, err := NewOwnershipManager(&ownershipRepositoryStub{})
|
||||
if err != nil {
|
||||
t.Fatalf("NewOwnershipManager(): %v", err)
|
||||
}
|
||||
now := time.Date(2026, 7, 29, 12, 0, 0, 0, time.UTC)
|
||||
|
||||
if _, err := manager.Assign(nil, now, "proxy-1", "worker-1", time.Minute); !errors.Is(err, ErrInvalidOwnership) {
|
||||
t.Fatalf("Assign(nil context) error = %v, want ErrInvalidOwnership", err)
|
||||
}
|
||||
if _, err := manager.Expire(context.Background(), now, 0); !errors.Is(err, ErrInvalidOwnership) {
|
||||
t.Fatalf("Expire(zero limit) error = %v, want ErrInvalidOwnership", err)
|
||||
}
|
||||
}
|
||||
|
||||
type ownershipContextKey struct{}
|
||||
|
||||
type ownershipRepositoryStub struct {
|
||||
assign func(context.Context, time.Time, string, string, time.Duration) (Assignment, error)
|
||||
expire func(context.Context, time.Time, int) ([]Assignment, error)
|
||||
assignCalled bool
|
||||
}
|
||||
|
||||
func (r *ownershipRepositoryStub) Assign(ctx context.Context, now time.Time, proxyID, workerID string, ttl time.Duration) (Assignment, error) {
|
||||
r.assignCalled = true
|
||||
if r.assign != nil {
|
||||
return r.assign(ctx, now, proxyID, workerID, ttl)
|
||||
}
|
||||
return Assignment{}, nil
|
||||
}
|
||||
|
||||
func (r *ownershipRepositoryStub) Renew(context.Context, time.Time, string, string, uint64, time.Duration) (Assignment, error) {
|
||||
return Assignment{}, nil
|
||||
}
|
||||
|
||||
func (r *ownershipRepositoryStub) BeginDrain(context.Context, string, string, uint64) (Assignment, error) {
|
||||
return Assignment{}, nil
|
||||
}
|
||||
|
||||
func (r *ownershipRepositoryStub) AcknowledgeDrain(context.Context, string, string, uint64, int64, int64) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *ownershipRepositoryStub) Get(context.Context, string) (Assignment, bool, error) {
|
||||
return Assignment{}, false, nil
|
||||
}
|
||||
|
||||
func (r *ownershipRepositoryStub) Expire(ctx context.Context, now time.Time, limit int) ([]Assignment, error) {
|
||||
if r.expire != nil {
|
||||
return r.expire(ctx, now, limit)
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
var _ ownershipDomain.Repository = (*ownershipRepositoryStub)(nil)
|
||||
|
||||
func newTestOwnershipManager(t *testing.T, proxyIDs ...string) *OwnershipManager {
|
||||
t.Helper()
|
||||
now := time.Date(2026, 7, 28, 12, 0, 0, 0, time.UTC)
|
||||
|
||||
@ -284,12 +284,18 @@ func (p *MemoryPool) Extract(ctx context.Context, command extractionDomain.Comma
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (p *MemoryPool) Assign(now time.Time, proxyID, workerID string, ttl time.Duration) (ownershipDomain.Assignment, error) {
|
||||
func (p *MemoryPool) Assign(ctx context.Context, now time.Time, proxyID, workerID string, ttl time.Duration) (ownershipDomain.Assignment, error) {
|
||||
if err := ownershipContextError(ctx); err != nil {
|
||||
return ownershipDomain.Assignment{}, err
|
||||
}
|
||||
if p == nil || now.IsZero() || proxyID == "" || workerID == "" || ttl <= 0 {
|
||||
return ownershipDomain.Assignment{}, ownershipDomain.ErrInvalidOwnership
|
||||
}
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
if err := ctx.Err(); err != nil {
|
||||
return ownershipDomain.Assignment{}, err
|
||||
}
|
||||
p.purgeExpiredLocked(now)
|
||||
if current, exists := p.ownership[proxyID]; exists {
|
||||
if current.ExpiresAt.After(now) {
|
||||
@ -316,12 +322,18 @@ func (p *MemoryPool) Assign(now time.Time, proxyID, workerID string, ttl time.Du
|
||||
return assignment, nil
|
||||
}
|
||||
|
||||
func (p *MemoryPool) Renew(now time.Time, proxyID, workerID string, epoch uint64, ttl time.Duration) (ownershipDomain.Assignment, error) {
|
||||
func (p *MemoryPool) Renew(ctx context.Context, now time.Time, proxyID, workerID string, epoch uint64, ttl time.Duration) (ownershipDomain.Assignment, error) {
|
||||
if err := ownershipContextError(ctx); err != nil {
|
||||
return ownershipDomain.Assignment{}, err
|
||||
}
|
||||
if p == nil || now.IsZero() || ttl <= 0 {
|
||||
return ownershipDomain.Assignment{}, ownershipDomain.ErrInvalidOwnership
|
||||
}
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
if err := ctx.Err(); err != nil {
|
||||
return ownershipDomain.Assignment{}, err
|
||||
}
|
||||
p.purgeExpiredLocked(now)
|
||||
assignment, ok := p.ownership[proxyID]
|
||||
if !ok || assignment.WorkerID != workerID || assignment.Epoch != epoch || !assignment.ExpiresAt.After(now) {
|
||||
@ -337,12 +349,18 @@ func (p *MemoryPool) Renew(now time.Time, proxyID, workerID string, epoch uint64
|
||||
return assignment, nil
|
||||
}
|
||||
|
||||
func (p *MemoryPool) BeginDrain(proxyID, workerID string, epoch uint64) (ownershipDomain.Assignment, error) {
|
||||
func (p *MemoryPool) BeginDrain(ctx context.Context, proxyID, workerID string, epoch uint64) (ownershipDomain.Assignment, error) {
|
||||
if err := ownershipContextError(ctx); err != nil {
|
||||
return ownershipDomain.Assignment{}, err
|
||||
}
|
||||
if p == nil {
|
||||
return ownershipDomain.Assignment{}, ownershipDomain.ErrStaleAssignment
|
||||
}
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
if err := ctx.Err(); err != nil {
|
||||
return ownershipDomain.Assignment{}, err
|
||||
}
|
||||
assignment, ok := p.ownership[proxyID]
|
||||
if !ok || assignment.WorkerID != workerID || assignment.Epoch != epoch {
|
||||
return ownershipDomain.Assignment{}, ownershipDomain.ErrStaleAssignment
|
||||
@ -355,12 +373,18 @@ func (p *MemoryPool) BeginDrain(proxyID, workerID string, epoch uint64) (ownersh
|
||||
return assignment, nil
|
||||
}
|
||||
|
||||
func (p *MemoryPool) AcknowledgeDrain(proxyID, workerID string, epoch uint64, active, reserved int64) error {
|
||||
func (p *MemoryPool) AcknowledgeDrain(ctx context.Context, proxyID, workerID string, epoch uint64, active, reserved int64) error {
|
||||
if err := ownershipContextError(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
if p == nil || active < 0 || reserved < 0 {
|
||||
return ownershipDomain.ErrInvalidOwnership
|
||||
}
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
if err := ctx.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
assignment, ok := p.ownership[proxyID]
|
||||
if !ok || assignment.WorkerID != workerID || assignment.Epoch != epoch {
|
||||
return ownershipDomain.ErrStaleAssignment
|
||||
@ -379,28 +403,53 @@ func (p *MemoryPool) AcknowledgeDrain(proxyID, workerID string, epoch uint64, ac
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *MemoryPool) Get(proxyID string) (ownershipDomain.Assignment, bool) {
|
||||
func (p *MemoryPool) Get(ctx context.Context, proxyID string) (ownershipDomain.Assignment, bool, error) {
|
||||
if err := ownershipContextError(ctx); err != nil {
|
||||
return ownershipDomain.Assignment{}, false, err
|
||||
}
|
||||
if p == nil {
|
||||
return ownershipDomain.Assignment{}, false
|
||||
return ownershipDomain.Assignment{}, false, nil
|
||||
}
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
if err := ctx.Err(); err != nil {
|
||||
return ownershipDomain.Assignment{}, false, err
|
||||
}
|
||||
assignment, ok := p.ownership[proxyID]
|
||||
return assignment, ok
|
||||
return assignment, ok, nil
|
||||
}
|
||||
|
||||
func (p *MemoryPool) Expire(now time.Time) []ownershipDomain.Assignment {
|
||||
func (p *MemoryPool) Expire(ctx context.Context, now time.Time, limit int) ([]ownershipDomain.Assignment, error) {
|
||||
if err := ownershipContextError(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if limit <= 0 {
|
||||
return nil, ownershipDomain.ErrInvalidOwnership
|
||||
}
|
||||
if p == nil {
|
||||
return nil
|
||||
return nil, nil
|
||||
}
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
expired := make([]ownershipDomain.Assignment, 0)
|
||||
if err := ctx.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
eligibleIDs := make([]string, 0)
|
||||
for proxyID, assignment := range p.ownership {
|
||||
entry, exists := p.entryByIDLocked(proxyID)
|
||||
_, exists := p.entryByIDLocked(proxyID)
|
||||
if exists && assignment.ExpiresAt.After(now) {
|
||||
continue
|
||||
}
|
||||
eligibleIDs = append(eligibleIDs, proxyID)
|
||||
}
|
||||
sort.Strings(eligibleIDs)
|
||||
if len(eligibleIDs) > limit {
|
||||
eligibleIDs = eligibleIDs[:limit]
|
||||
}
|
||||
expired := make([]ownershipDomain.Assignment, 0, len(eligibleIDs))
|
||||
for _, proxyID := range eligibleIDs {
|
||||
assignment := p.ownership[proxyID]
|
||||
entry, exists := p.entryByIDLocked(proxyID)
|
||||
if exists && entry.OwnerWorkerID == assignment.WorkerID {
|
||||
entry.OwnerWorkerID = ""
|
||||
p.setEntryByIDLocked(proxyID, entry)
|
||||
@ -408,9 +457,14 @@ func (p *MemoryPool) Expire(now time.Time) []ownershipDomain.Assignment {
|
||||
expired = append(expired, assignment)
|
||||
delete(p.ownership, proxyID)
|
||||
}
|
||||
p.purgeExpiredLocked(now)
|
||||
sort.Slice(expired, func(i, j int) bool { return expired[i].ProxyID < expired[j].ProxyID })
|
||||
return expired
|
||||
return expired, nil
|
||||
}
|
||||
|
||||
func ownershipContextError(ctx context.Context) error {
|
||||
if ctx == nil {
|
||||
return ownershipDomain.ErrInvalidOwnership
|
||||
}
|
||||
return ctx.Err()
|
||||
}
|
||||
|
||||
func (p *MemoryPool) purgeExpiredLocked(now time.Time) int {
|
||||
|
||||
@ -283,11 +283,11 @@ func TestMemoryPoolAssignRecoversExpiredLeaseWithoutSeparateSweep(t *testing.T)
|
||||
now := time.Date(2026, 7, 29, 10, 0, 0, 0, time.UTC)
|
||||
pool := poolWithOneProxy(t, now)
|
||||
proxyID := pool.Snapshot(now)[0].Proxy.ID
|
||||
first, err := pool.Assign(now, proxyID, "worker-a", 5*time.Second)
|
||||
first, err := pool.Assign(context.Background(), now, proxyID, "worker-a", 5*time.Second)
|
||||
if err != nil {
|
||||
t.Fatalf("Assign(first): %v", err)
|
||||
}
|
||||
second, err := pool.Assign(now.Add(5*time.Second), proxyID, "worker-b", 5*time.Second)
|
||||
second, err := pool.Assign(context.Background(), now.Add(5*time.Second), proxyID, "worker-b", 5*time.Second)
|
||||
if err != nil {
|
||||
t.Fatalf("Assign(after lease expiry): %v", err)
|
||||
}
|
||||
@ -305,7 +305,7 @@ func TestMemoryPoolMakesOwnershipAndExtractionMutuallyExclusive(t *testing.T) {
|
||||
extracted := make(chan bool, 1)
|
||||
go func() {
|
||||
<-start
|
||||
_, err := pool.Assign(now, pool.Snapshot(now)[0].Proxy.ID, "worker-a", time.Minute)
|
||||
_, err := pool.Assign(context.Background(), now, pool.Snapshot(now)[0].Proxy.ID, "worker-a", time.Minute)
|
||||
if err != nil && !errors.Is(err, ownershipDomain.ErrOwnershipUnavailable) {
|
||||
t.Errorf("Assign(): %v", err)
|
||||
}
|
||||
@ -336,6 +336,101 @@ func TestMemoryPoolMakesOwnershipAndExtractionMutuallyExclusive(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestMemoryPoolOwnershipMethodsPropagateCanceledContext(t *testing.T) {
|
||||
now := time.Date(2026, 7, 29, 10, 0, 0, 0, time.UTC)
|
||||
pool := poolWithOneProxy(t, now)
|
||||
proxyID := pool.Snapshot(now)[0].Proxy.ID
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cancel()
|
||||
|
||||
if _, err := pool.Assign(ctx, now, proxyID, "worker-a", time.Minute); !errors.Is(err, context.Canceled) {
|
||||
t.Fatalf("Assign() error = %v, want context.Canceled", err)
|
||||
}
|
||||
if _, err := pool.Renew(ctx, now, proxyID, "worker-a", 1, time.Minute); !errors.Is(err, context.Canceled) {
|
||||
t.Fatalf("Renew() error = %v, want context.Canceled", err)
|
||||
}
|
||||
if _, err := pool.BeginDrain(ctx, proxyID, "worker-a", 1); !errors.Is(err, context.Canceled) {
|
||||
t.Fatalf("BeginDrain() error = %v, want context.Canceled", err)
|
||||
}
|
||||
if err := pool.AcknowledgeDrain(ctx, proxyID, "worker-a", 1, 0, 0); !errors.Is(err, context.Canceled) {
|
||||
t.Fatalf("AcknowledgeDrain() error = %v, want context.Canceled", err)
|
||||
}
|
||||
if _, _, err := pool.Get(ctx, proxyID); !errors.Is(err, context.Canceled) {
|
||||
t.Fatalf("Get() error = %v, want context.Canceled", err)
|
||||
}
|
||||
if _, err := pool.Expire(ctx, now, 1); !errors.Is(err, context.Canceled) {
|
||||
t.Fatalf("Expire() error = %v, want context.Canceled", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMemoryPoolOwnershipMethodRechecksContextAfterLock(t *testing.T) {
|
||||
now := time.Date(2026, 7, 29, 10, 0, 0, 0, time.UTC)
|
||||
pool := poolWithOneProxy(t, now)
|
||||
proxyID := pool.Snapshot(now)[0].Proxy.ID
|
||||
base, cancel := context.WithCancel(context.Background())
|
||||
ctx := &firstCheckContext{Context: base, checked: make(chan struct{})}
|
||||
result := make(chan error, 1)
|
||||
|
||||
pool.mu.Lock()
|
||||
go func() {
|
||||
_, err := pool.Assign(ctx, now, proxyID, "worker-a", time.Minute)
|
||||
result <- err
|
||||
}()
|
||||
<-ctx.checked
|
||||
cancel()
|
||||
pool.mu.Unlock()
|
||||
|
||||
if err := <-result; !errors.Is(err, context.Canceled) {
|
||||
t.Fatalf("Assign() error = %v, want context.Canceled", err)
|
||||
}
|
||||
if _, ok, err := pool.Get(context.Background(), proxyID); err != nil || ok {
|
||||
t.Fatal("canceled Assign() mutated ownership")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMemoryPoolExpireUsesStableProxyIDOrderAndLimit(t *testing.T) {
|
||||
now := time.Date(2026, 7, 29, 10, 0, 0, 0, time.UTC)
|
||||
pool := NewMemoryPool()
|
||||
proxies := []proxyDomain.Proxy{
|
||||
{ID: "proxy-c", Scheme: proxyDomain.SchemeHTTP, Host: "192.0.2.3", Port: 8003, State: proxyDomain.StateAvailable},
|
||||
{ID: "proxy-a", Scheme: proxyDomain.SchemeHTTP, Host: "192.0.2.1", Port: 8001, State: proxyDomain.StateAvailable},
|
||||
{ID: "proxy-b", Scheme: proxyDomain.SchemeHTTP, Host: "192.0.2.2", Port: 8002, State: proxyDomain.StateAvailable},
|
||||
}
|
||||
if _, err := pool.UpsertFetched(context.Background(), "provider-a", FetchedBatch{
|
||||
ObservedAt: now, ConfiguredTTL: 10 * time.Minute, Proxies: proxies,
|
||||
}); err != nil {
|
||||
t.Fatalf("UpsertFetched(): %v", err)
|
||||
}
|
||||
for _, proxyID := range []string{"proxy-c", "proxy-a", "proxy-b"} {
|
||||
if _, err := pool.Assign(context.Background(), now, proxyID, "worker-a", time.Minute); err != nil {
|
||||
t.Fatalf("Assign(%s): %v", proxyID, err)
|
||||
}
|
||||
}
|
||||
|
||||
expired, err := pool.Expire(context.Background(), now.Add(time.Minute), 2)
|
||||
if err != nil {
|
||||
t.Fatalf("Expire(): %v", err)
|
||||
}
|
||||
if len(expired) != 2 || expired[0].ProxyID != "proxy-a" || expired[1].ProxyID != "proxy-b" {
|
||||
t.Fatalf("Expire() = %+v, want proxy-a then proxy-b", expired)
|
||||
}
|
||||
if _, ok, err := pool.Get(context.Background(), "proxy-c"); err != nil || !ok {
|
||||
t.Fatalf("Get(proxy-c) = ok %v, error %v; want remaining assignment", ok, err)
|
||||
}
|
||||
}
|
||||
|
||||
type firstCheckContext struct {
|
||||
context.Context
|
||||
checked chan struct{}
|
||||
once sync.Once
|
||||
}
|
||||
|
||||
func (c *firstCheckContext) Err() error {
|
||||
err := c.Context.Err()
|
||||
c.once.Do(func() { close(c.checked) })
|
||||
return err
|
||||
}
|
||||
|
||||
func poolWithOneProxy(t *testing.T, now time.Time) *MemoryPool {
|
||||
t.Helper()
|
||||
pool := NewMemoryPool()
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
package ownership
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"time"
|
||||
)
|
||||
@ -26,10 +27,10 @@ type Assignment struct {
|
||||
// Repository is the shared authority for ownership changes. Implementations
|
||||
// that also support extraction must serialize both operations transactionally.
|
||||
type Repository interface {
|
||||
Assign(now time.Time, proxyID, workerID string, ttl time.Duration) (Assignment, error)
|
||||
Renew(now time.Time, proxyID, workerID string, epoch uint64, ttl time.Duration) (Assignment, error)
|
||||
BeginDrain(proxyID, workerID string, epoch uint64) (Assignment, error)
|
||||
AcknowledgeDrain(proxyID, workerID string, epoch uint64, active, reserved int64) error
|
||||
Get(proxyID string) (Assignment, bool)
|
||||
Expire(now time.Time) []Assignment
|
||||
Assign(context.Context, time.Time, string, string, time.Duration) (Assignment, error)
|
||||
Renew(context.Context, time.Time, string, string, uint64, time.Duration) (Assignment, error)
|
||||
BeginDrain(context.Context, string, string, uint64) (Assignment, error)
|
||||
AcknowledgeDrain(context.Context, string, string, uint64, int64, int64) error
|
||||
Get(context.Context, string) (Assignment, bool, error)
|
||||
Expire(context.Context, time.Time, int) ([]Assignment, error)
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user