feat(multiplayer): add shared allocation quota

This commit is contained in:
Josh Creek
2026-09-01 18:38:06 +01:00
parent 55706ba9ea
commit 72e8d27633
11 changed files with 211 additions and 4 deletions
+15
View File
@@ -30,11 +30,16 @@ type AllocationBudget interface {
Allow(region string, now time.Time) error
}
type SharedAllocationQuota interface {
Consume(context.Context, string, time.Time) error
}
type Service struct {
Provider Provider
Durable Durable
Roster RosterPublisher
Budget AllocationBudget
Quota SharedAllocationQuota
Now func() time.Time
}
@@ -88,6 +93,11 @@ func (s Service) Allocate(ctx context.Context, request domain.AllocationRequest,
return agones.AllocatedServer{}, err
}
}
if s.Quota != nil {
if err := s.Quota.Consume(ctx, request.Region, now); err != nil {
return agones.AllocatedServer{}, err
}
}
result, err := s.Provider.Allocate(ctx, request, labels, now)
if err != nil {
return agones.AllocatedServer{}, err
@@ -102,6 +112,11 @@ func (s Service) RecordProviderAllocation(ctx context.Context, result agones.All
if s.Durable == nil || result.Allocation.State != domain.ServerAllocated || result.Endpoint == "" {
return domain.Allocation{}, domain.ErrAllocationInput
}
if s.Quota != nil {
if err := s.Quota.Consume(ctx, result.Allocation.Region, now); err != nil {
return domain.Allocation{}, err
}
}
return s.Durable.RecordProviderAllocation(ctx, result.Allocation, now)
}
+35
View File
@@ -32,6 +32,16 @@ type rosterSpy struct {
err error
}
type quotaSpy struct {
calls int
err error
}
func (q *quotaSpy) Consume(context.Context, string, time.Time) error {
q.calls++
return q.err
}
func (r *rosterSpy) PublishRoster(_ context.Context, _ domain.Assignment, _ []domain.SignedJoinAuthorisation, _ func([]byte, []byte) bool) error {
r.calls++
return r.err
@@ -63,6 +73,31 @@ func TestServiceDoesNotReturnProviderResultAfterDurableFailure(t *testing.T) {
}
}
func TestServiceConsumesSharedQuotaBeforeFreshProviderCall(t *testing.T) {
provider := &providerSpy{result: agones.AllocatedServer{Allocation: domain.Allocation{AllocationID: "a", State: domain.ServerAllocated}, Endpoint: "127.0.0.1:7777"}}
quota := &quotaSpy{err: errors.New("quota exhausted")}
service := Service{Provider: provider, Durable: &durableSpy{}, Quota: quota, Now: func() time.Time { return time.Unix(1000, 0) }}
if _, err := service.Allocate(context.Background(), domain.AllocationRequest{AllocationID: "a", MatchID: "m", Region: "EU", Build: "b", Protocol: 1, Transport: "enet"}, nil); err == nil {
t.Fatal("quota rejection was ignored")
}
if quota.calls != 1 || provider.calls != 0 {
t.Fatalf("quota/provider calls = %d/%d, want 1/0", quota.calls, provider.calls)
}
}
func TestServiceConsumesSharedQuotaOnceWhenReconcilingProviderResult(t *testing.T) {
quota := &quotaSpy{}
durable := &durableSpy{}
service := Service{Durable: durable, Quota: quota, Now: func() time.Time { return time.Unix(1000, 0) }}
result := agones.AllocatedServer{Allocation: domain.Allocation{AllocationID: "a", MatchID: "m", Region: "EU", State: domain.ServerAllocated}, Endpoint: "127.0.0.1:7777"}
if _, err := service.RecordProviderAllocation(context.Background(), result, time.Unix(1000, 0)); err != nil {
t.Fatalf("reconciliation failed: %v", err)
}
if quota.calls != 1 || durable.calls != 1 {
t.Fatalf("quota/durable calls = %d/%d, want 1/1", quota.calls, durable.calls)
}
}
func TestServiceAllocatesOnlyUnanimouslyAcceptedMatchingProposal(t *testing.T) {
proposal := domain.Proposal{
ProposalID: "proposal-1", Playlist: domain.Casual, State: domain.Accepted,