Weighted Semaphore for Variable-Cost Work

Bound the total weight of concurrent work, acquire and release arbitrary weights, and wake waiters when capacity frees up.

GoResilience

Code

snippet
package main

import (
	"context"
	"fmt"
	"sync"
	"time"
)

type WeightedSemaphore struct {
	mu       sync.Mutex
	cond     *sync.Cond
	capacity int64
	used     int64
}

func NewWeightedSemaphore(capacity int64) (*WeightedSemaphore, error) {
	if capacity < 1 {
		return nil, fmt.Errorf("capacity must be positive")
	}
	sem := &WeightedSemaphore{capacity: capacity}
	sem.cond = sync.NewCond(&sem.mu)
	return sem, nil
}

func (s *WeightedSemaphore) Acquire(ctx context.Context, weight int64) error {
	if weight < 1 || weight > s.capacity {
		return fmt.Errorf("weight %d outside 1..%d", weight, s.capacity)
	}

	done := make(chan struct{})
	go func() {
		select {
		case <-ctx.Done():
			s.mu.Lock()
			s.cond.Broadcast()
			s.mu.Unlock()
		case <-done:
		}
	}()
	defer close(done)

	s.mu.Lock()
	defer s.mu.Unlock()
	for s.used+weight > s.capacity {
		if err := ctx.Err(); err != nil {
			return err
		}
		s.cond.Wait()
	}
	if err := ctx.Err(); err != nil {
		return err
	}
	s.used += weight
	return nil
}

func (s *WeightedSemaphore) Release(weight int64) {
	s.mu.Lock()
	defer s.mu.Unlock()
	if weight < 1 || weight > s.used {
		panic(fmt.Sprintf("release %d with %d in use", weight, s.used))
	}
	s.used -= weight
	s.cond.Broadcast()
}

func main() {
	sem, err := NewWeightedSemaphore(3)
	if err != nil {
		fmt.Println(err)
		return
	}

	jobs := []struct {
		name   string
		weight int64
	}{
		{"small", 1},
		{"medium", 2},
		{"large", 3},
		{"small-2", 1},
	}

	var wg sync.WaitGroup
	for _, job := range jobs {
		job := job
		wg.Add(1)
		go func() {
			defer wg.Done()
			ctx, cancel := context.WithTimeout(context.Background(), time.Second)
			defer cancel()
			if err := sem.Acquire(ctx, job.weight); err != nil {
				fmt.Printf("%s: %v\n", job.name, err)
				return
			}
			time.Sleep(20 * time.Millisecond)
			sem.Release(job.weight)
			fmt.Printf("%s finished (weight %d)\n", job.name, job.weight)
		}()
	}
	wg.Wait()
	fmt.Println("all jobs done")
}