agilepool
High-performance adaptive goroutine pool for Go with auto-scaling via Little's Law, sliding-window rate statistics, chunked overflow buffer, multiple idle container backends (LinkedList/MinHeap/Slice/RingQueue), customizable spinlock, retry with exponential backoff, and context-aware submission. Use when working with the github.com/Yiming1997/agilePool/v2 codebase — adding features, fixing bugs, writing tests, or answering questions about pool internals and API usage.
日本語の概要は準備中です。原文の説明を表示しています。
実行スコア 未検証
インストール方法を見る含まれるファイル(1)
- SKILL.md9.9 KB
SKILL.md(原文)
インストールする前に、エージェントに与えられる指示の中身を確認できます。
agilePool — Adaptive Goroutine Pool
Overview
Package agilepool (github.com/Yiming1997/agilePool/v2, Go 1.23.4) provides an adaptive goroutine pool that dynamically scales worker count using Little's Law. It features:
- Adaptive scaling: Median rate sampling over a sliding window →
target = submitMedian × runningWorkers / consumeMedian, with backlog-weighted boosting and exit-rate compensation. - Chunked overflow buffer: When the handoff channel is full, tasks spool into a linked-list of fixed-size chunks (4096 slots each). Soft cap at 100k total tasks; exceeding it blocks on the channel instead of growing unboundedly.
- Multiple idle containers:
LinkedListType(default FIFO),MinHeapType,SliceType,RingQueueType— pluggable via config. - Configurable locking:
muIdlelock can besync.Mutex(default) or a customspinLock(three-phase: CAS → exponential backoff → Gosched). - Task variants:
Taskinterface,TaskFunc(bare func),TaskWithRetry(exponential backoff), context-aware viaSubmitCtx, timeout viaSubmitBefore. - Panic-safe: Workers recover from panics and continue processing.
- Graceful shutdown:
Close()+Wait()pattern.
Quick Start
import agilepool "github.com/Yiming1997/agilePool/v2"
pool := agilepool.NewPool(agilepool.NewConfig(
agilepool.WithWorkerNumCapacity(1000),
agilepool.WithTaskQueueSize(10000),
))
defer pool.Close()
pool.Submit(agilepool.TaskFunc(func() error {
// your work here
return nil
}))
pool.Wait()
API Reference
Pool Creation
pool := agilepool.NewPool(config *Config)
NewPool(nil) uses defaults (unlimited workers, LinkedListType, MutexLock).
Task Interface and Types
// Core interface
type Task interface {
process()
}
// Simple function adapter
// func() error → NewTask → Task
pool.Submit(agilepool.NewTask(func() error { return nil }))
type TaskFunc func() error // func() error → TaskFunc → Task
pool.Submit(agilepool.TaskFunc(func() error { return nil }))
// Retry with backoff
pool.Submit(&agilepool.TaskWithRetry{
MinBackOff: 1 * time.Second,
MaxBackOff: 200 * time.Second,
RetryNum: 3, // retries on error, so total attempts = RetryNum + 1
BackOffStrategy: nil, // nil → default exponential: min * 2^retryNum, capped at max
Task: func() error { return errors.New("fail") },
})
Submission Methods
| Method | Behavior |
|---|---|
pool.Submit(task) | Enqueues task; no-op on nil. Returns void. |
pool.TrySubmit(task) bool | Returns false if rejected (pool closed, nil task, or NONBLOCK mode queue full). |
pool.SubmitCtx(ctx, task) | Wraps task with context; skips execution if ctx.Err() != nil at dequeue time. |
pool.SubmitBefore(task, timeout) | Skips task if not started within timeout. |
Shutdown
pool.Close() // Idempotent. Stops cleaner/scaler. New submits become no-ops.
pool.Wait() // Blocks until all in-flight tasks complete.
// Always: Close() first, then Wait().
Critical: Call Close() before Wait(). Calling Wait() alone blocks forever because the background goroutines never exit.
Query Methods
pool.GetRunningWorkersNum() int64 // currently active goroutines
pool.GetWorkerCreateCount() int64 // lifetime sync.Pool allocations
pool.GetIdleWorkerCount() int64 // parked in idle container
pool.GetTaskQueueLen() int // len(taskQueue) — does NOT include chunked buffer
pool.GetCapacity() int64 // max worker limit
agilepool.GetDefaultPool() *Pool // most recently created pool (nil after Close)
Logger
pool.SetLogger(l Logger) // interface { Printf(string, ...interface{}); Println(...interface{}) }
// Pass *log.Logger, zap.SugaredLogger, etc.
Configuration Options
All via functional options pattern:
agilepool.NewConfig(
agilepool.WithCleanPeriod(500 * time.Millisecond), // expired worker cleanup interval
agilepool.WithTaskQueueSize(10000), // handoff channel capacity
agilepool.WithWorkerNumCapacity(20000), // max concurrent workers
agilepool.WithBlockMode(agilepool.BLOCK), // BLOCK (default) or NONBLOCK
agilepool.WithIdleContainerType(agilepool.LinkedListType), // LinkedList/MinHeap/Slice/RingQueue
agilepool.WithLockType(agilepool.SpinLock), // MutexLock (default) or SpinLock
agilepool.WithStatsSamplePeriod(100 * time.Millisecond), // rate sampling interval
agilepool.WithStatsWindowSize(10), // sliding window count for median
agilepool.WithScalerPeriod(10 * time.Millisecond), // scaler tick interval
agilepool.WithBacklogDecayFactor(0.3), // queue backlog weight in scaler (0-1)
)
WorkMode
- BLOCK (default):
Submitblocks until the task is accepted (channel or chunked buffer). - NONBLOCK:
Submitdrops the task when the handoff channel is full. UseTrySubmitto detect rejection.
IdleContainerType
| Type | Add | Pop | Best for |
|---|---|---|---|
LinkedListType | O(1) | O(1) | General purpose (default) |
MinHeapType | O(log n) | O(log n) | Ordered by lastActiveAt |
SliceType | O(1) | O(n) | Simple, small pools |
RingQueueType | O(1) | O(1) | High-frequency Pop; works well with SpinLock |
Common Patterns
Graceful Shutdown
pool := agilepool.NewPool(config)
defer pool.Close() // 1. Stop accepting new work
// ... submit tasks ...
pool.Wait() // 2. Wait for completion
Producer-Consumer
pool := agilepool.NewPool(agilepool.NewConfig(
agilepool.WithWorkerNumCapacity(runtime.NumCPU() * 2),
))
defer pool.Close()
var wg sync.WaitGroup
for _, item := range items {
wg.Add(1)
it := item
pool.Submit(agilepool.TaskFunc(func() error {
defer wg.Done()
process(it)
return nil
}))
}
wg.Wait()
pool.Wait()
Context-Aware with Cancellation
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
pool.SubmitCtx(ctx, agilepool.TaskFunc(func() error {
select {
case <-ctx.Done():
return ctx.Err()
default:
return doWork()
}
}))
Anti-Patterns & Pitfalls
-
Don't call
Wait()beforeClose()— the pool has background goroutines (cleaner, scaler, sampler).Wait()withoutClose()blocks forever. -
Don't
Close()from within a task without defer —Close()stops the cleaner goroutine, causing idle workers to accumulate. Closing inside a task is safe but usedefer pool.Close()only at the outer scope. -
Nil
*TaskWithRetrypanics —isNilTaskchecks*TaskWithRetry == nilbut not itsTaskfield. EnsureTaskis non-nil. -
SubmitCtxwithnilcontext — automatically usescontext.Background(). An already-canceled context causes the task to be silently dropped. -
GetTaskQueueLen()is incomplete — it only reportslen(taskQueue), not the chunked overflow buffer. For true backlog, check your own metrics. -
Task closure capture — Go loop variable capture applies. Always copy:
for _, v := range items { v := v pool.Submit(agilepool.TaskFunc(func() error { use(v); return nil })) }
Testing
Tests use package agilepool_test (external test package) — import github.com/Yiming1997/agilePool/v2 and use the public API. Use testing.Short() guards for heavy benchmarks:
if testing.Short() {
taskCount = 20000 // reduced
}
Internal Architecture (when modifying code)
pool.go: Core Pool struct, NewPool, Submit path (channel → chunked buffer → backpressure), scaler withscaleIfNeeded(), Close/Wait.config.go:Configstruct andWith*functional options.worker.go:worker.run()— three-phase task acquisition loop: (1) channel receive, (2) chunked buffer batch (up to 8 per lock), (3) second channel check before parking.task.go:Taskinterface,TaskFunc,TaskWithRetry(exponential backoff default:min × 2^retryNum, capped atmax).idle_container.go:IdleWorkerContainerinterface +IdleContainerTypeenum.idle_linkedlist.go/idle_heap.go/idle_slice.go/idle_ring_queue.go: Concrete implementations.histogram.go: Fixed-bucket sliding-window histogram for rate sampling. Used by scaler to compute median submit/consume/exit rates.spinlock.go: Three-phase spin lock implementingsync.Locker. Used formuIdlewhenWithLockType(SpinLock).stack_trace.go:Stack(skip)for panic recovery — reads source files, so not for production hot paths.
Code Modification Notes
pendingTasksis atomic — decremented inpool.done(), incremented on submit. It covers submitters blocked on the channel, giving the scaler full visibility.- Workers are pooled via
sync.Pool. Do NOTworkerPool.Put(w)when the worker is also inidleWorks— that causes dual ownership and data races. - The scaler runs every
scalerPeriod(default 10ms). It reads median rates from histograms, computes target via Little's Law with backlog-weighted boosting. taskMuprotects the chunked buffer. Workers batch-pop up to 8 tasks per lock acquisition to amortize contention.- The spinlock does NOT call into the kernel — optimal when hold times are <1µs (e.g., RingQueue index swaps). Use
MutexLockfor longer or unpredictable hold times.
レビュー
まだレビューはありません。使ってみた感想をお寄せください。
コメント
ログインなしで書き込めます。使ってみた感想、質問、動かなかった環境などを気軽にどうぞ。
まだコメントはありません。最初のコメントを書いてみませんか。