refactor: make ownership repository context aware
This commit is contained in:
parent
179e4d5a0c
commit
c8e64758da
@ -1,6 +1,7 @@
|
|||||||
package pool
|
package pool
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
ownershipDomain "proxy-pool/internal/domain/ownership"
|
ownershipDomain "proxy-pool/internal/domain/ownership"
|
||||||
@ -30,44 +31,62 @@ func NewOwnershipManager(repository ownershipDomain.Repository) (*OwnershipManag
|
|||||||
return &OwnershipManager{repository: repository}, nil
|
return &OwnershipManager{repository: repository}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *OwnershipManager) Assign(now time.Time, proxyID, workerID string, ttl time.Duration) (Assignment, error) {
|
func (m *OwnershipManager) Assign(ctx context.Context, now time.Time, proxyID, workerID string, ttl time.Duration) (Assignment, error) {
|
||||||
if m == nil || m.repository == nil {
|
if m == nil || m.repository == nil || ctx == nil {
|
||||||
return Assignment{}, ErrInvalidOwnership
|
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) {
|
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 {
|
if m == nil || m.repository == nil || ctx == nil {
|
||||||
return Assignment{}, ErrInvalidOwnership
|
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) {
|
func (m *OwnershipManager) BeginDrain(ctx context.Context, proxyID, workerID string, epoch uint64) (Assignment, error) {
|
||||||
if m == nil || m.repository == nil {
|
if m == nil || m.repository == nil || ctx == nil {
|
||||||
return Assignment{}, ErrInvalidOwnership
|
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 {
|
func (m *OwnershipManager) AcknowledgeDrain(ctx context.Context, proxyID, workerID string, epoch uint64, active, reserved int64) error {
|
||||||
if m == nil || m.repository == nil {
|
if m == nil || m.repository == nil || ctx == nil {
|
||||||
return ErrInvalidOwnership
|
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) {
|
func (m *OwnershipManager) Get(ctx context.Context, proxyID string) (Assignment, bool, error) {
|
||||||
if m == nil || m.repository == nil {
|
if m == nil || m.repository == nil || ctx == nil {
|
||||||
return Assignment{}, false
|
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 {
|
func (m *OwnershipManager) Expire(ctx context.Context, now time.Time, limit int) ([]Assignment, error) {
|
||||||
if m == nil || m.repository == nil {
|
if m == nil || m.repository == nil || ctx == nil || limit <= 0 {
|
||||||
return nil
|
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"
|
"proxy-pool/internal/domain/activitypool"
|
||||||
extractionDomain "proxy-pool/internal/domain/extraction"
|
extractionDomain "proxy-pool/internal/domain/extraction"
|
||||||
|
ownershipDomain "proxy-pool/internal/domain/ownership"
|
||||||
proxyDomain "proxy-pool/internal/domain/proxy"
|
proxyDomain "proxy-pool/internal/domain/proxy"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -24,7 +25,7 @@ func TestOwnershipManagerPreventsDualAssignment(t *testing.T) {
|
|||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
go func() {
|
go func() {
|
||||||
defer wg.Done()
|
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 {
|
if err == nil {
|
||||||
succeeded.Add(1)
|
succeeded.Add(1)
|
||||||
return
|
return
|
||||||
@ -44,21 +45,22 @@ func TestOwnershipManagerPreventsDualAssignment(t *testing.T) {
|
|||||||
func TestOwnershipManagerRenewsOnlyCurrentAssignment(t *testing.T) {
|
func TestOwnershipManagerRenewsOnlyCurrentAssignment(t *testing.T) {
|
||||||
manager := newTestOwnershipManager(t, "proxy-1")
|
manager := newTestOwnershipManager(t, "proxy-1")
|
||||||
now := time.Date(2026, 7, 28, 12, 0, 0, 0, time.UTC)
|
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 {
|
if err != nil {
|
||||||
t.Fatalf("Assign(): %v", err)
|
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 {
|
if err != nil {
|
||||||
t.Fatalf("Renew(): %v", err)
|
t.Fatalf("Renew(): %v", err)
|
||||||
}
|
}
|
||||||
if renewed.Version != assigned.Version+1 || !renewed.ExpiresAt.Equal(now.Add(90*time.Second)) {
|
if renewed.Version != assigned.Version+1 || !renewed.ExpiresAt.Equal(now.Add(90*time.Second)) {
|
||||||
t.Fatalf("renewed assignment = %+v", renewed)
|
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)
|
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)
|
t.Fatalf("renewed assignment expired at old deadline: %+v", expired)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -77,7 +79,7 @@ func TestSharedRepositoryMakesOwnershipAndExtractionMutuallyExclusive(t *testing
|
|||||||
extracted := make(chan bool, 1)
|
extracted := make(chan bool, 1)
|
||||||
go func() {
|
go func() {
|
||||||
<-start
|
<-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) {
|
if assignErr != nil && !errors.Is(assignErr, ErrOwnershipUnavailable) {
|
||||||
t.Errorf("iteration %d Assign(): %v", iteration, assignErr)
|
t.Errorf("iteration %d Assign(): %v", iteration, assignErr)
|
||||||
}
|
}
|
||||||
@ -111,11 +113,12 @@ func TestSharedRepositoryMakesOwnershipAndExtractionMutuallyExclusive(t *testing
|
|||||||
func TestOwnershipManagerRequiresDrainAckAtZeroRuntime(t *testing.T) {
|
func TestOwnershipManagerRequiresDrainAckAtZeroRuntime(t *testing.T) {
|
||||||
manager := newTestOwnershipManager(t, "proxy-1")
|
manager := newTestOwnershipManager(t, "proxy-1")
|
||||||
now := time.Date(2026, 7, 28, 12, 0, 0, 0, time.UTC)
|
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 {
|
if err != nil {
|
||||||
t.Fatalf("Assign(): %v", err)
|
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 {
|
if err != nil {
|
||||||
t.Fatalf("BeginDrain(): %v", err)
|
t.Fatalf("BeginDrain(): %v", err)
|
||||||
}
|
}
|
||||||
@ -123,13 +126,13 @@ func TestOwnershipManagerRequiresDrainAckAtZeroRuntime(t *testing.T) {
|
|||||||
t.Fatalf("draining assignment = %+v", draining)
|
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)
|
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)
|
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")
|
t.Fatal("assignment still exists after drain acknowledgement")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -137,18 +140,19 @@ func TestOwnershipManagerRequiresDrainAckAtZeroRuntime(t *testing.T) {
|
|||||||
func TestOwnershipManagerExpiresCrashedWorkerAssignment(t *testing.T) {
|
func TestOwnershipManagerExpiresCrashedWorkerAssignment(t *testing.T) {
|
||||||
manager := newTestOwnershipManager(t, "proxy-1")
|
manager := newTestOwnershipManager(t, "proxy-1")
|
||||||
now := time.Date(2026, 7, 28, 12, 0, 0, 0, time.UTC)
|
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 {
|
if err != nil {
|
||||||
t.Fatalf("Assign(first): %v", err)
|
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)
|
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)
|
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 {
|
if err != nil {
|
||||||
t.Fatalf("Assign(second): %v", err)
|
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 {
|
func newTestOwnershipManager(t *testing.T, proxyIDs ...string) *OwnershipManager {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
now := time.Date(2026, 7, 28, 12, 0, 0, 0, time.UTC)
|
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
|
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 {
|
if p == nil || now.IsZero() || proxyID == "" || workerID == "" || ttl <= 0 {
|
||||||
return ownershipDomain.Assignment{}, ownershipDomain.ErrInvalidOwnership
|
return ownershipDomain.Assignment{}, ownershipDomain.ErrInvalidOwnership
|
||||||
}
|
}
|
||||||
p.mu.Lock()
|
p.mu.Lock()
|
||||||
defer p.mu.Unlock()
|
defer p.mu.Unlock()
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return ownershipDomain.Assignment{}, err
|
||||||
|
}
|
||||||
p.purgeExpiredLocked(now)
|
p.purgeExpiredLocked(now)
|
||||||
if current, exists := p.ownership[proxyID]; exists {
|
if current, exists := p.ownership[proxyID]; exists {
|
||||||
if current.ExpiresAt.After(now) {
|
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
|
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 {
|
if p == nil || now.IsZero() || ttl <= 0 {
|
||||||
return ownershipDomain.Assignment{}, ownershipDomain.ErrInvalidOwnership
|
return ownershipDomain.Assignment{}, ownershipDomain.ErrInvalidOwnership
|
||||||
}
|
}
|
||||||
p.mu.Lock()
|
p.mu.Lock()
|
||||||
defer p.mu.Unlock()
|
defer p.mu.Unlock()
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return ownershipDomain.Assignment{}, err
|
||||||
|
}
|
||||||
p.purgeExpiredLocked(now)
|
p.purgeExpiredLocked(now)
|
||||||
assignment, ok := p.ownership[proxyID]
|
assignment, ok := p.ownership[proxyID]
|
||||||
if !ok || assignment.WorkerID != workerID || assignment.Epoch != epoch || !assignment.ExpiresAt.After(now) {
|
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
|
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 {
|
if p == nil {
|
||||||
return ownershipDomain.Assignment{}, ownershipDomain.ErrStaleAssignment
|
return ownershipDomain.Assignment{}, ownershipDomain.ErrStaleAssignment
|
||||||
}
|
}
|
||||||
p.mu.Lock()
|
p.mu.Lock()
|
||||||
defer p.mu.Unlock()
|
defer p.mu.Unlock()
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return ownershipDomain.Assignment{}, err
|
||||||
|
}
|
||||||
assignment, ok := p.ownership[proxyID]
|
assignment, ok := p.ownership[proxyID]
|
||||||
if !ok || assignment.WorkerID != workerID || assignment.Epoch != epoch {
|
if !ok || assignment.WorkerID != workerID || assignment.Epoch != epoch {
|
||||||
return ownershipDomain.Assignment{}, ownershipDomain.ErrStaleAssignment
|
return ownershipDomain.Assignment{}, ownershipDomain.ErrStaleAssignment
|
||||||
@ -355,12 +373,18 @@ func (p *MemoryPool) BeginDrain(proxyID, workerID string, epoch uint64) (ownersh
|
|||||||
return assignment, nil
|
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 {
|
if p == nil || active < 0 || reserved < 0 {
|
||||||
return ownershipDomain.ErrInvalidOwnership
|
return ownershipDomain.ErrInvalidOwnership
|
||||||
}
|
}
|
||||||
p.mu.Lock()
|
p.mu.Lock()
|
||||||
defer p.mu.Unlock()
|
defer p.mu.Unlock()
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
assignment, ok := p.ownership[proxyID]
|
assignment, ok := p.ownership[proxyID]
|
||||||
if !ok || assignment.WorkerID != workerID || assignment.Epoch != epoch {
|
if !ok || assignment.WorkerID != workerID || assignment.Epoch != epoch {
|
||||||
return ownershipDomain.ErrStaleAssignment
|
return ownershipDomain.ErrStaleAssignment
|
||||||
@ -379,28 +403,53 @@ func (p *MemoryPool) AcknowledgeDrain(proxyID, workerID string, epoch uint64, ac
|
|||||||
return nil
|
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 {
|
if p == nil {
|
||||||
return ownershipDomain.Assignment{}, false
|
return ownershipDomain.Assignment{}, false, nil
|
||||||
}
|
}
|
||||||
p.mu.Lock()
|
p.mu.Lock()
|
||||||
defer p.mu.Unlock()
|
defer p.mu.Unlock()
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return ownershipDomain.Assignment{}, false, err
|
||||||
|
}
|
||||||
assignment, ok := p.ownership[proxyID]
|
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 {
|
if p == nil {
|
||||||
return nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
p.mu.Lock()
|
p.mu.Lock()
|
||||||
defer p.mu.Unlock()
|
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 {
|
for proxyID, assignment := range p.ownership {
|
||||||
entry, exists := p.entryByIDLocked(proxyID)
|
_, exists := p.entryByIDLocked(proxyID)
|
||||||
if exists && assignment.ExpiresAt.After(now) {
|
if exists && assignment.ExpiresAt.After(now) {
|
||||||
continue
|
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 {
|
if exists && entry.OwnerWorkerID == assignment.WorkerID {
|
||||||
entry.OwnerWorkerID = ""
|
entry.OwnerWorkerID = ""
|
||||||
p.setEntryByIDLocked(proxyID, entry)
|
p.setEntryByIDLocked(proxyID, entry)
|
||||||
@ -408,9 +457,14 @@ func (p *MemoryPool) Expire(now time.Time) []ownershipDomain.Assignment {
|
|||||||
expired = append(expired, assignment)
|
expired = append(expired, assignment)
|
||||||
delete(p.ownership, proxyID)
|
delete(p.ownership, proxyID)
|
||||||
}
|
}
|
||||||
p.purgeExpiredLocked(now)
|
return expired, nil
|
||||||
sort.Slice(expired, func(i, j int) bool { return expired[i].ProxyID < expired[j].ProxyID })
|
}
|
||||||
return expired
|
|
||||||
|
func ownershipContextError(ctx context.Context) error {
|
||||||
|
if ctx == nil {
|
||||||
|
return ownershipDomain.ErrInvalidOwnership
|
||||||
|
}
|
||||||
|
return ctx.Err()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *MemoryPool) purgeExpiredLocked(now time.Time) int {
|
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)
|
now := time.Date(2026, 7, 29, 10, 0, 0, 0, time.UTC)
|
||||||
pool := poolWithOneProxy(t, now)
|
pool := poolWithOneProxy(t, now)
|
||||||
proxyID := pool.Snapshot(now)[0].Proxy.ID
|
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 {
|
if err != nil {
|
||||||
t.Fatalf("Assign(first): %v", err)
|
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 {
|
if err != nil {
|
||||||
t.Fatalf("Assign(after lease expiry): %v", err)
|
t.Fatalf("Assign(after lease expiry): %v", err)
|
||||||
}
|
}
|
||||||
@ -305,7 +305,7 @@ func TestMemoryPoolMakesOwnershipAndExtractionMutuallyExclusive(t *testing.T) {
|
|||||||
extracted := make(chan bool, 1)
|
extracted := make(chan bool, 1)
|
||||||
go func() {
|
go func() {
|
||||||
<-start
|
<-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) {
|
if err != nil && !errors.Is(err, ownershipDomain.ErrOwnershipUnavailable) {
|
||||||
t.Errorf("Assign(): %v", err)
|
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 {
|
func poolWithOneProxy(t *testing.T, now time.Time) *MemoryPool {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
pool := NewMemoryPool()
|
pool := NewMemoryPool()
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
package ownership
|
package ownership
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
@ -26,10 +27,10 @@ type Assignment struct {
|
|||||||
// Repository is the shared authority for ownership changes. Implementations
|
// Repository is the shared authority for ownership changes. Implementations
|
||||||
// that also support extraction must serialize both operations transactionally.
|
// that also support extraction must serialize both operations transactionally.
|
||||||
type Repository interface {
|
type Repository interface {
|
||||||
Assign(now time.Time, proxyID, workerID string, ttl time.Duration) (Assignment, error)
|
Assign(context.Context, time.Time, string, string, time.Duration) (Assignment, error)
|
||||||
Renew(now time.Time, proxyID, workerID string, epoch uint64, ttl time.Duration) (Assignment, error)
|
Renew(context.Context, time.Time, string, string, uint64, time.Duration) (Assignment, error)
|
||||||
BeginDrain(proxyID, workerID string, epoch uint64) (Assignment, error)
|
BeginDrain(context.Context, string, string, uint64) (Assignment, error)
|
||||||
AcknowledgeDrain(proxyID, workerID string, epoch uint64, active, reserved int64) error
|
AcknowledgeDrain(context.Context, string, string, uint64, int64, int64) error
|
||||||
Get(proxyID string) (Assignment, bool)
|
Get(context.Context, string) (Assignment, bool, error)
|
||||||
Expire(now time.Time) []Assignment
|
Expire(context.Context, time.Time, int) ([]Assignment, error)
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user