Production-grade concurrency toolkit for Go
Jack provides the missing pieces for building robust, observable concurrent systems. No magic, no reflection hacks—just solid patterns you'd otherwise write yourself.
Go's concurrency primitives are excellent, but production systems need more:
- Panic recovery that doesn't crash your entire process
- Backpressure when queues fill up
- Visibility into what your goroutines are actually doing
- Graceful shutdown that finishes in-flight work
- Health checks that degrade and accelerate automatically
Jack fills these gaps without getting in your way.
Fixed-size worker pool with backpressure. Tasks queue when workers are busy. Submissions fail fast when the queue is full.
pool := jack.NewPool(5, jack.PoolingWithQueueSize(100))
pool.Submit(jack.Func(func() error {
// work
return nil
}))Type-safe async computation with composition. Wait for results, chain transformations, recover from errors.
f := jack.Async(func() (string, error) {
return fetchUser()
})
f.Then(ctx, func(user string) (any, error) {
return fetchProfile(user)
}).Await()Health check scheduler that degrades and accelerates. Tracks consecutive failures, applies jitter, notifies observers.
doctor := jack.NewDoctor(jack.DoctorWithMaxConcurrent(10))
doctor.Add(jack.NewPatient(jack.PatientConfig{
ID: "database",
Interval: 10 * time.Second,
MaxFailures: 3,
Check: checkDB,
OnStateChange: func(e jack.PatientEvent) {
if e.State == jack.PatientFailed {
triggerAlert(e.ID)
}
},
}))Rate-limit rapid calls. Execute only after a quiet period or when thresholds are hit.
db := jack.NewDebouncer(
jack.WithDebounceDelay(500*time.Millisecond),
jack.WithDebounceMaxCalls(10),
)
db.Do(expensiveOperation)Background task with exponential backoff and jitter. Perfect for reconciliation loops.
looper := jack.NewLooper(reconcile,
jack.WithLooperInterval(5*time.Second),
jack.WithLooperBackoff(true),
jack.WithLooperMaxInterval(time.Minute),
)
looper.Start()Graceful termination with signal handling. Register cleanup in LIFO order.
sd := jack.NewShutdown(jack.ShutdownWithTimeout(30*time.Second))
sd.Register(db.Close)
sd.Register(cache.Flush)
sd.Wait() // blocks until SIGTERMTTL expiration with min-heap and sharding.
reaper := jack.NewReaper(5*time.Minute,
jack.ReaperWithHandler(func(ctx context.Context, id string) {
cleanup(id)
}),
)
reaper.Touch("session-123")Scheduled callbacks with keep-alive resets.
lm := jack.NewLifetime()
lm.ScheduleTimed(ctx, "heartbeat", func(ctx context.Context, id string) {
markDead(id)
}, 30*time.Second)
lm.ResetTimed("heartbeat") // extend on activitySingle-worker queue, cron-style scheduling, and coordinated goroutine groups with error collection.
Context-aware mutex with panic recovery.
var mu jack.Safely
err := mu.SafeCtx(ctx, func() error {
// critical section that respects context cancellation
return nil
})Every component emits events you can hook into:
obs := jack.NewObservable[jack.Event](10)
obs.Add(myObserver)
pool := jack.NewPool(5, jack.PoolingWithObservable(obs))Doctor, Scheduler, and Looper have their own event types for metrics and alerting.
Panics become *jack.CaughtPanic with stack traces. No silent failures.
err := jack.Safe(func() error {
panic("boom")
})
if cp, ok := err.(*jack.CaughtPanic); ok {
log.Printf("panic: %v\n%s", cp.Value, cp.Stack)
}| Problem | Use |
|---|---|
| Process many independent tasks concurrently | Pool |
| Need result from async operation | Future |
| Run periodic health checks with degradation | Doctor |
| Rate-limit bursty calls | Debouncer |
| Background loop with backoff | Looper |
| Graceful shutdown with cleanup ordering | Shutdown |
| Expire items after TTL | Reaper |
| Schedule callbacks with keep-alive | Lifetime |
| Coordinate multiple goroutines, collect errors | Group |
| Sequential async processing | Runner |
| Cron-style recurring tasks | Scheduler |
| Safe locking with timeouts | Safely |
go test -v -race ./...Race detector is your friend. Jack is race-free by design.
MIT