maxcap
A Go library for managing a maximum concurrent number of on-going resource usage on a per-client basis.
When a client exceeds their maximum concurrency cap, the oldest resource for that client is automatically closed.
Installation
go get codeberg.org/jrh3k5/maxcap
Usage
The registration.Registrar interface defines the contract for managing per-client concurrency. The interface is generic over the client identifier type, so you can use any type — such as a string, integer, or custom struct — to identify clients.
Any implementation of this interface manages the maximum concurrent open resources per client. When the limit is exceeded, the oldest resource for that client is closed via the provided close handler. The returned de-registration function must be called when the resource is closed to maintain accurate internal state.
package main
import (
"context"
"log"
"codeberg.org/jrh3k5/maxcap/pkg/registration"
"google.golang.org/grpc"
)
type ClientID struct {
Tenant string
UserID string
}
func main() {
var registrar registration.Registrar[ClientID]
// Assign a concrete implementation, such as local.NewRegistrar(...)
conn, err := grpc.Dial("example.com:8080", grpc.WithInsecure())
if err != nil {
log.Fatal(err)
}
defer conn.Close()
stream, err := conn.CreateStream(context.Background(), nil, "/chat.ChatService/Chat")
if err != nil {
log.Fatal(err)
}
ctx := context.Background()
clientID := ClientID{
Tenant: "acme",
UserID: "user-abc",
}
deregister, err := registrar.RegisterResourceOpen(ctx, clientID, func(_ context.Context) error {
return stream.CloseSend()
})
if err != nil {
log.Fatal(err)
}
defer deregister(ctx)
}
Local Registrar
The local.Registrar is an in-memory implementation of registration.Registrar. It tracks open resources per client and enforces the concurrency limit entirely within the local process. It is not intended for distributed resource management.
It supports calling the close handler for abandoned resources: a background goroutine periodically scans tracked resources and closes any that have exceeded the configured expiry duration without being explicitly deregistered.
import (
"context"
"time"
"codeberg.org/jrh3k5/maxcap/pkg/registration/local"
"codeberg.org/jrh3k5/maxcap/pkg/registration/metrics"
)
registrar := local.NewRegistrar(
context.Background(),
func(clientID ClientID) string {
return clientID.Tenant + "/" + clientID.UserID
},
10,
5*time.Minute,
30*time.Minute,
local.NewNopEventHandler[ClientID](),
metrics.NewNopGauge[ClientID](),
)
defer registrar.Close()
The constructor takes a context, a mapper function that converts the client identifier into a string key, the maximum number of concurrent resources allowed per client, a resource cleanup frequency, a resource expiry duration, an event handler, and a gauge for reporting active resource counts. Call Close() to stop background cleanup when shutting down.
No-op Registrar
For cases where you wish to be able to satisfy a requirement of a supplied registration.Registrar implementation but do not want any active resource management, the nop.Registrar implementation provides a means to do that. This can be useful if you want to, for example, be able to turn resource management on and off.
Redis Registrar
The redis.Registrar is a Redis-backed implementation of registration.Registrar intended for distributed resource management across multiple application instances. It uses a Redis list per client to track open resources and publishes close messages via Redis pub/sub when the concurrency limit is exceeded.
It does not support calling the close handler for abandoned resources. Resources that are registered but never explicitly deregistered will remain tracked in Redis until the resource list key expires (configured at construction).
import (
"context"
"time"
"codeberg.org/jrh3k5/maxcap/pkg/registration/metrics"
"codeberg.org/jrh3k5/maxcap/pkg/registration/redis"
"codeberg.org/jrh3k5/maxcap/pkg/task"
"github.com/go-redsync/redsync/v4"
goredis "github.com/go-redsync/redsync/v4/redis/goredis/v9"
"github.com/redis/go-redis/v9"
)
redisClient := redis.NewClient(&redis.Options{Addr: "localhost:6379"})
registrar, err := redis.NewRegistrar(
context.Background(),
redisClient,
task.NewGoroutineRunner(),
10,
func(clientID ClientID) string {
return clientID.Tenant + "/" + clientID.UserID
},
5*time.Minute,
redis.NewNopEventHandler[ClientID](),
metrics.NewNopGauge[ClientID](),
)
if err != nil {
panic(err)
}
Event Callbacks
Both the local and Redis registrars accept an event handler that provides lifecycle callbacks for resource operations. See the event.Handler interface.
A no-op implementation is available via local.NewNopEventHandler[ClientID]() or redis.NewNopEventHandler[ClientID]().
Gauging
Both the local and Redis registrars accept a metrics.Gauge that reports the current number of actively-used resources per client. This is an optional observability hook; the reported count reflects the state after concurrency cap enforcement.
A no-op implementation is available via metrics.NewNopGauge[ClientID]().
The Gauge interface is parameterized by client identifier type:
type Gauge[ClientIdentifier any] interface {
ClientResourcesUtilized(ctx context.Context, clientIdentifier ClientIdentifier, resourcesUtilized uint64)
}
Implement this interface to integrate with your metrics system of choice, such as Prometheus:
type prometheusGauge struct{}
func (g *prometheusGauge) ClientResourcesUtilized(_ context.Context, clientID client.Key, count uint64) {
myPrometheusGauge.WithLabelValues(clientID.String()).Set(float64(count))
}
Logging
This library uses the standard library's log/slog package for structured logging. Configure the default slog logger to control log output and verbosity.
AI Usage Disclosure
OpenCode's Big Pickle model was used to generate some unit tests (e.g., for bucketing), resolve linting errors raised by golangci-lint, and the LUA script used within the Redis implementation. It was also used as a code review tool.