proxy-pool/internal/domain/proxy/capacity_test.go

192 lines
5.4 KiB
Go

package proxy
import (
"errors"
"sync"
"sync/atomic"
"testing"
)
func TestReservationCancelReleasesReservedCapacity(t *testing.T) {
capacity := NewCapacity(1)
reservation, ok := capacity.Reserve()
if !ok {
t.Fatal("Reserve() = false, want reservation")
}
if err := reservation.Cancel(); err != nil {
t.Fatalf("Cancel() error = %v", err)
}
assertCapacityCounters(t, capacity, 0, 0)
if err := reservation.Cancel(); !errors.Is(err, ErrReservationFinished) {
t.Fatalf("second Cancel() error = %v, want ErrReservationFinished", err)
}
if err := reservation.Commit(); !errors.Is(err, ErrReservationFinished) {
t.Fatalf("Commit() after Cancel error = %v, want ErrReservationFinished", err)
}
if err := reservation.Release(); !errors.Is(err, ErrReservationFinished) {
t.Fatalf("Release() after Cancel error = %v, want ErrReservationFinished", err)
}
reused, ok := capacity.Reserve()
if !ok {
t.Fatal("Reserve() after Cancel = false, want released slot")
}
if err := reused.Cancel(); err != nil {
t.Fatalf("reused Cancel() error = %v", err)
}
assertCapacityCounters(t, capacity, 0, 0)
}
func TestReservationCommitAndReleaseAreSingleUse(t *testing.T) {
capacity := NewCapacity(1)
reservation, ok := capacity.Reserve()
if !ok {
t.Fatal("Reserve() = false, want reservation")
}
if err := reservation.Commit(); err != nil {
t.Fatalf("Commit() error = %v", err)
}
assertCapacityCounters(t, capacity, 1, 0)
if err := reservation.Commit(); !errors.Is(err, ErrReservationCommitted) {
t.Fatalf("second Commit() error = %v, want ErrReservationCommitted", err)
}
if err := reservation.Cancel(); !errors.Is(err, ErrReservationFinished) {
t.Fatalf("Cancel() after Commit error = %v, want ErrReservationFinished", err)
}
assertCapacityCounters(t, capacity, 1, 0)
if err := reservation.Release(); err != nil {
t.Fatalf("Release() error = %v", err)
}
if err := reservation.Release(); !errors.Is(err, ErrReservationFinished) {
t.Fatalf("second Release() error = %v, want ErrReservationFinished", err)
}
if err := reservation.Commit(); !errors.Is(err, ErrReservationFinished) {
t.Fatalf("Commit() after Release error = %v, want ErrReservationFinished", err)
}
assertCapacityCounters(t, capacity, 0, 0)
}
func TestReleaseBeforeCommitDoesNotConsumeReservation(t *testing.T) {
capacity := NewCapacity(1)
reservation, ok := capacity.Reserve()
if !ok {
t.Fatal("Reserve() = false, want reservation")
}
if err := reservation.Release(); !errors.Is(err, ErrReservationFinished) {
t.Fatalf("Release() before Commit error = %v, want ErrReservationFinished", err)
}
assertCapacityCounters(t, capacity, 0, 1)
if err := reservation.Cancel(); err != nil {
t.Fatalf("Cancel() after rejected Release error = %v", err)
}
assertCapacityCounters(t, capacity, 0, 0)
}
func TestConcurrentReservationTerminationPreservesCounters(t *testing.T) {
for iteration := range 1_000 {
capacity := NewCapacity(1)
reservation, ok := capacity.Reserve()
if !ok {
t.Fatalf("iteration %d Reserve() = false", iteration)
}
start := make(chan struct{})
results := make(chan terminationResult, 2)
var wait sync.WaitGroup
wait.Add(2)
go func() {
defer wait.Done()
<-start
results <- terminationResult{operation: "commit", err: reservation.Commit()}
}()
go func() {
defer wait.Done()
<-start
results <- terminationResult{operation: "cancel", err: reservation.Cancel()}
}()
close(start)
wait.Wait()
close(results)
var succeeded string
for result := range results {
if result.err == nil {
if succeeded != "" {
t.Fatalf("iteration %d operations %s and %s both succeeded", iteration, succeeded, result.operation)
}
succeeded = result.operation
continue
}
if !errors.Is(result.err, ErrReservationFinished) {
t.Fatalf("iteration %d %s error = %v, want ErrReservationFinished", iteration, result.operation, result.err)
}
}
switch succeeded {
case "commit":
assertCapacityCounters(t, capacity, 1, 0)
if err := reservation.Release(); err != nil {
t.Fatalf("iteration %d Release() error = %v", iteration, err)
}
case "cancel":
assertCapacityCounters(t, capacity, 0, 0)
default:
t.Fatalf("iteration %d has no successful termination", iteration)
}
assertCapacityCounters(t, capacity, 0, 0)
}
}
func TestConcurrentReleaseSucceedsOnce(t *testing.T) {
capacity := NewCapacity(1)
reservation, ok := capacity.Reserve()
if !ok {
t.Fatal("Reserve() = false, want reservation")
}
if err := reservation.Commit(); err != nil {
t.Fatalf("Commit() error = %v", err)
}
var succeeded atomic.Int64
var unexpected atomic.Int64
var wait sync.WaitGroup
for range 100 {
wait.Add(1)
go func() {
defer wait.Done()
err := reservation.Release()
switch {
case err == nil:
succeeded.Add(1)
case !errors.Is(err, ErrReservationFinished):
unexpected.Add(1)
}
}()
}
wait.Wait()
if succeeded.Load() != 1 || unexpected.Load() != 0 {
t.Fatalf("Release() results = success:%d unexpected:%d, want 1 and 0", succeeded.Load(), unexpected.Load())
}
assertCapacityCounters(t, capacity, 0, 0)
}
type terminationResult struct {
operation string
err error
}
func assertCapacityCounters(t *testing.T, capacity *Capacity, active, reserved int64) {
t.Helper()
if got := capacity.Active(); got != active {
t.Fatalf("Active() = %d, want %d", got, active)
}
if got := capacity.Reserved(); got != reserved {
t.Fatalf("Reserved() = %d, want %d", got, reserved)
}
}