From bafc4129b4255f61135c04f6181bac90156362be Mon Sep 17 00:00:00 2001 From: pardnchiu Date: Sat, 18 Jul 2026 13:44:09 +0800 Subject: [PATCH] breaking: Move sources into core package and require new import path --- .github/workflows/test.yml | 6 +- .gitignore | 3 +- README.md | 89 +++++------- README.zh.md | 88 ------------ main_test.go => core/main_test.go | 2 +- new.go => core/new.go | 2 +- option.go => core/option.go | 2 +- pending.go => core/pending.go | 2 +- priority.go => core/priority.go | 2 +- task.go => core/task.go | 2 +- cover.png | Bin 49617 -> 0 bytes doc.md | 226 ------------------------------ doc.zh.md | 226 ------------------------------ doc/README.zh.md | 69 +++++++++ doc/architecture.md | 136 ++++++++++++++++++ doc/architecture.zh.md | 136 ++++++++++++++++++ doc/doc.md | 219 +++++++++++++++++++++++++++++ doc/doc.zh.md | 219 +++++++++++++++++++++++++++++ 18 files changed, 824 insertions(+), 605 deletions(-) delete mode 100644 README.zh.md rename main_test.go => core/main_test.go (99%) rename new.go => core/new.go (99%) rename option.go => core/option.go (97%) rename pending.go => core/pending.go (99%) rename priority.go => core/priority.go (97%) rename task.go => core/task.go (98%) delete mode 100644 cover.png delete mode 100644 doc.md delete mode 100644 doc.zh.md create mode 100644 doc/README.zh.md create mode 100644 doc/architecture.md create mode 100644 doc/architecture.zh.md create mode 100644 doc/doc.md create mode 100644 doc/doc.zh.md diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 82a81fc..f77eade 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -2,11 +2,9 @@ name: Test on: push: - branches: [ main, master ] - tags: - - 'v*' + branches: [ develop ] pull_request: - branches: [ master ] + branches: [ develop ] jobs: test: diff --git a/.gitignore b/.gitignore index 496ee2c..152b132 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ -.DS_Store \ No newline at end of file +.DS_Store +.tmp* diff --git a/README.md b/README.md index aa43802..0f6159a 100644 --- a/README.md +++ b/README.md @@ -1,66 +1,54 @@ > [!NOTE] -> This README was generated by [SKILL](https://github.com/pardnchiu/skill-readme-generate), get the ZH version from [here](./README.zh.md). +> This README was generated by [SKILL](https://github.com/agenvoy/skill-readme-generate), get the ZH version from [here](./doc/README.zh.md). -# go-queue +*** + +

+PRIORITY-AWARE TASK QUEUE FOR GO +

-[![pkg](https://pkg.go.dev/badge/github.com/pardnchiu/go-queue.svg)](https://pkg.go.dev/github.com/pardnchiu/go-queue) -[![card](https://goreportcard.com/badge/github.com/pardnchiu/go-queue)](https://goreportcard.com/report/github.com/pardnchiu/go-queue) -[![codecov](https://img.shields.io/codecov/c/github/pardnchiu/go-queue/master)](https://app.codecov.io/github/pardnchiu/go-queue/tree/master) -[![license](https://img.shields.io/github/license/pardnchiu/go-queue)](LICENSE) -[![version](https://img.shields.io/github/v/tag/pardnchiu/go-queue?label=release)](https://github.com/pardnchiu/go-queue/releases) +

+Go Reference +Release +License +Coverage +

+ +*** -> A min-heap based priority task queue with automatic promotion, retry, and panic recovery, designed for Go concurrency workloads. +> A Go task queue library with priority scheduling, timeout retries, and an atomic state machine ## Table of Contents - [Features](#features) - [Architecture](#architecture) -- [File Structure](#file-structure) - [License](#license) - [Author](#author) -- [Stars](#stars) ## Features -> `go get github.com/pardnchiu/go-queue` · [Documentation](./doc.md) - -### Five-Level Priority Scheduling with Auto-Promotion - -Tasks are scheduled through a min-heap across five priority levels: Immediate, High, Retry, Normal, and Low. Low-priority tasks automatically promote to higher levels after exceeding a configurable wait threshold, eliminating starvation without manual intervention. - -### Atomic State Machine Lifecycle - -Queue state transitions from Created to Running to Closed are driven entirely by `atomic.CompareAndSwap`, avoiding mutexes on the Start and Shutdown paths. This ensures safe concurrent access from multiple goroutines with minimal latency. +> `go get github.com/pardnchiu/go-queue` · [Documentation](./doc/doc.md) -### Built-in Retry with Panic Recovery - -Each task supports a configurable retry limit; failed tasks re-enter the heap at Retry priority. Workers automatically recover from panics and convert them to error reports, preventing a single task crash from taking down the entire worker pool. +- **Five-Level Priority Queue** — Orders Immediate, High, Retry, Normal, and Low tasks, with automatic promotion after wait timeouts. +- **Worker Pool Parallelism** — Defaults to CPU×2 workers with configurable queue capacity and global timeout. +- **Timeout and Retry** — Isolates panics, enforces per-task timeouts, and re-enqueues failed tasks at the highest priority. +- **Functional Options** — Configures each enqueue with WithTaskID, WithTimeout, WithCallback, and WithRetry. +- **Atomic State Machine** — Transitions Created, Running, and Closed via CAS for lock-free, idempotent Shutdown. ## Architecture +> [Full Architecture](./doc/architecture.md) + ```mermaid graph TB - E[Enqueue] -->|Push| H[Min-Heap] - H -->|Pop| W[Worker Pool] - W -->|Execute| T[Task] - T -->|Fail + Retry| H - H -->|Promote| H - T -->|Success| CB[Callback] - T -->|Panic| R[Recovery → Error] -``` - -## File Structure - -``` -go-queue/ -├── new.go # Queue construction, worker startup and shutdown -├── task.go # Task struct and min-heap implementation -├── pending.go # Pending queue, push/pop and auto-promotion -├── priority.go # Priority level definitions and timeout calculation -├── option.go # Enqueue options (TaskID, Timeout, Callback, Retry) -├── main_test.go # Tests -├── go.mod -└── LICENSE + App[Application] --> Enqueue[Enqueue] + Enqueue --> Pending[Pending Heap] + Pending --> Workers[Worker Pool] + Workers --> Execute[Execute / Timeout / Retry] + App --> Start[Start] + App --> Shutdown[Shutdown] + Start --> Workers + Shutdown --> Pending ``` ## License @@ -69,20 +57,13 @@ This project is licensed under the [MIT LICENSE](LICENSE). ## Author - +

邱敬幃 Pardn Chiu

- - - - - - -## Stars - -[![Star](https://api.star-history.com/svg?repos=pardnchiu/go-queue&type=Date)](https://www.star-history.com/#pardnchiu/go-queue&Date) +hi@pardn.io
+https://www.linkedin.com/in/pardnchiu *** -©️ 2025 [邱敬幃 Pardn Chiu](https://linkedin.com/in/pardnchiu) +©️ 2025 [邱敬幃 Pardn Chiu](https://www.linkedin.com/in/pardnchiu) diff --git a/README.zh.md b/README.zh.md deleted file mode 100644 index 1f60cfb..0000000 --- a/README.zh.md +++ /dev/null @@ -1,88 +0,0 @@ -> [!NOTE] -> 此 README 由 [SKILL](https://github.com/pardnchiu/skill-readme-generate) 生成,英文版請參閱 [這裡](./README.md)。 - -# go-queue - -[![pkg](https://pkg.go.dev/badge/github.com/pardnchiu/go-queue.svg)](https://pkg.go.dev/github.com/pardnchiu/go-queue) -[![card](https://goreportcard.com/badge/github.com/pardnchiu/go-queue)](https://goreportcard.com/report/github.com/pardnchiu/go-queue) -[![codecov](https://img.shields.io/codecov/c/github/pardnchiu/go-queue/master)](https://app.codecov.io/github/pardnchiu/go-queue/tree/master) -[![license](https://img.shields.io/github/license/pardnchiu/go-queue)](LICENSE) -[![version](https://img.shields.io/github/v/tag/pardnchiu/go-queue?label=release)](https://github.com/pardnchiu/go-queue/releases) - -> 基於 Min-Heap 的優先任務佇列,支援自動升級、Retry 與 Panic Recovery,專為 Go 併發場景設計。 - -## 目錄 - -- [功能特點](#功能特點) -- [架構](#架構) -- [檔案結構](#檔案結構) -- [授權](#授權) -- [Author](#author) -- [Stars](#stars) - -## 功能特點 - -> `go get github.com/pardnchiu/go-queue` · [完整文件](./doc.zh.md) - -### 五級優先排程與自動升級 - -任務依 Immediate、High、Retry、Normal、Low 五個等級透過 Min-Heap 排程,低優先任務在等待超過閾值後自動升級至更高等級,從根本上消除 Starvation 問題,無需手動干預。 - -### Atomic 狀態機驅動生命週期 - -佇列的 Created → Running → Closed 狀態轉換完全透過 `atomic.CompareAndSwap` 實現,避免在 Start 和 Shutdown 路徑上使用 Mutex,確保多個 Goroutine 同時操作時的安全性與低延遲。 - -### 內建 Retry 與 Panic Recovery - -每個任務可配置獨立的重試次數上限,失敗後以 Retry 優先級重新入列。Worker 層自動攔截 Panic 並轉為錯誤回報,防止單一任務崩潰拖垮整個 Worker Pool。 - -## 架構 - -```mermaid -graph TB - E[Enqueue] -->|Push| H[Min-Heap] - H -->|Pop| W[Worker Pool] - W -->|Execute| T[Task] - T -->|Fail + Retry| H - H -->|Promote| H - T -->|Success| CB[Callback] - T -->|Panic| R[Recovery → Error] -``` - -## 檔案結構 - -``` -go-queue/ -├── new.go # 佇列建構、Worker 啟動與 Shutdown -├── task.go # 任務結構與 Min-Heap 實作 -├── pending.go # 等待佇列、Push/Pop 與自動升級 -├── priority.go # 優先等級定義與 Timeout 計算 -├── option.go # Enqueue 選項(TaskID、Timeout、Callback、Retry) -├── main_test.go # 測試 -├── go.mod -└── LICENSE -``` - -## 授權 - -本專案採用 [MIT LICENSE](LICENSE)。 - -## Author - - - -

邱敬幃 Pardn Chiu

- - - - - - - -## Stars - -[![Star](https://api.star-history.com/svg?repos=pardnchiu/go-queue&type=Date)](https://www.star-history.com/#pardnchiu/go-queue&Date) - -*** - -©️ 2025 [邱敬幃 Pardn Chiu](https://linkedin.com/in/pardnchiu) diff --git a/main_test.go b/core/main_test.go similarity index 99% rename from main_test.go rename to core/main_test.go index 69474d5..a285426 100644 --- a/main_test.go +++ b/core/main_test.go @@ -1,5 +1,5 @@ // * generated by claude sonnet 4.5 -package goQueue +package core import ( "context" diff --git a/new.go b/core/new.go similarity index 99% rename from new.go rename to core/new.go index 6fbf7b2..f660b0d 100644 --- a/new.go +++ b/core/new.go @@ -1,4 +1,4 @@ -package goQueue +package core import ( "context" diff --git a/option.go b/core/option.go similarity index 97% rename from option.go rename to core/option.go index 6e51417..520da8a 100644 --- a/option.go +++ b/core/option.go @@ -1,4 +1,4 @@ -package goQueue +package core import "time" diff --git a/pending.go b/core/pending.go similarity index 99% rename from pending.go rename to core/pending.go index 16226a9..3194d65 100644 --- a/pending.go +++ b/core/pending.go @@ -1,4 +1,4 @@ -package goQueue +package core import ( "container/heap" diff --git a/priority.go b/core/priority.go similarity index 97% rename from priority.go rename to core/priority.go index 0859d18..043ac94 100644 --- a/priority.go +++ b/core/priority.go @@ -1,4 +1,4 @@ -package goQueue +package core import ( "time" diff --git a/task.go b/core/task.go similarity index 98% rename from task.go rename to core/task.go index 737faf6..ea136bf 100644 --- a/task.go +++ b/core/task.go @@ -1,4 +1,4 @@ -package goQueue +package core import ( "context" diff --git a/cover.png b/cover.png deleted file mode 100644 index 7377fd57b3285d5d3a072c7f325915a0e4bc6190..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 49617 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 b8R(KIhvvd`G|Ybv1O85*)IEV}T3-1dM2PzF diff --git a/doc.md b/doc.md deleted file mode 100644 index 1bc622b..0000000 --- a/doc.md +++ /dev/null @@ -1,226 +0,0 @@ -# go-queue - Documentation - -> Back to [README](./README.md) - -## Prerequisites - -- Go 1.23 or higher - -## Installation - -### Using go get - -```bash -go get github.com/pardnchiu/go-queue -``` - -### From Source - -```bash -git clone https://github.com/pardnchiu/go-queue.git -cd go-queue -go test ./... -``` - -## Usage - -### Basic - -Create a queue, start workers, and submit tasks: - -```go -package main - -import ( - "context" - "fmt" - "log" - - goQueue "github.com/pardnchiu/go-queue" -) - -func main() { - q := goQueue.New(&goQueue.Config{ - Workers: 4, - }) - - ctx := context.Background() - if err := q.Start(ctx); err != nil { - log.Fatal(err) - } - - id, err := q.Enqueue(ctx, "", func(ctx context.Context) error { - fmt.Println("task executed") - return nil - }) - if err != nil { - log.Fatal(err) - } - fmt.Println("task id:", id) - - if err := q.Shutdown(ctx); err != nil { - log.Fatal(err) - } -} -``` - -### Presets and Priority - -Define task types with preset priority and timeout: - -```go -q := goQueue.New(&goQueue.Config{ - Workers: 4, - Timeout: 30 * time.Second, - Preset: map[string]goQueue.PresetConfig{ - "critical": {Priority: goQueue.PriorityImmediate}, - "batch": {Priority: goQueue.PriorityLow, Timeout: 60 * time.Second}, - }, -}) - -ctx := context.Background() -q.Start(ctx) - -// Enqueue with Immediate priority -q.Enqueue(ctx, "critical", func(ctx context.Context) error { - // High priority task - return nil -}) - -// Enqueue with Low priority, 60s timeout -q.Enqueue(ctx, "batch", func(ctx context.Context) error { - // Low priority batch task - return nil -}) - -q.Shutdown(ctx) -``` - -### Retry and Callback - -Configure retry and completion callback for tasks: - -```go -q := goQueue.New(&goQueue.Config{Workers: 2}) -ctx := context.Background() -q.Start(ctx) - -id, err := q.Enqueue(ctx, "", func(ctx context.Context) error { - // Operation that may fail - return doSomething() -}, - goQueue.WithRetry(3), // Retry up to 3 times - goQueue.WithTaskID("my-task-001"), // Custom task ID - goQueue.WithTimeout(10*time.Second), // Per-task timeout - goQueue.WithCallback(func(id string) { // Completion callback - fmt.Printf("task %s completed\n", id) - }), -) -if err != nil { - log.Fatal(err) -} - -q.Shutdown(ctx) -``` - -### Graceful Shutdown with Timeout - -Limit shutdown wait time to prevent indefinite blocking: - -```go -q := goQueue.New(&goQueue.Config{Workers: 4}) -ctx := context.Background() -q.Start(ctx) - -// ... submit tasks ... - -shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second) -defer cancel() - -if err := q.Shutdown(shutdownCtx); err != nil { - fmt.Printf("shutdown timeout: %v\n", err) -} -``` - -## API Reference - -### New - -```go -func New(config *Config) *Queue -``` - -Create a new queue instance. Passing `nil` for config uses default values. - -### Config - -```go -type Config struct { - Workers int // Number of workers, default CPU * 2 - Size int // Queue capacity limit, default Workers * 64 - Timeout time.Duration // Global task timeout, default 30s - Preset map[string]PresetConfig // Task type preset configurations -} -``` - -### PresetConfig - -```go -type PresetConfig struct { - Priority Priority // Priority level, default PriorityNormal - Timeout time.Duration // Task timeout, 0 = auto-calculated by Priority -} -``` - -### Priority - -```go -type Priority int - -const ( - PriorityImmediate Priority = iota // Highest priority - PriorityHigh // High priority - PriorityRetry // Reserved for retried tasks - PriorityNormal // Normal (default) - PriorityLow // Low priority -) -``` - -Priority affects timeout calculation: Immediate = base/4, High = base/2, Normal = base, Low = base*2, clamped to the 15~120 second range. - -### Queue.Start - -```go -func (q *Queue) Start(ctx context.Context) error -``` - -Start the worker pool. Returns an error if called again or if the queue is already closed. - -### Queue.Enqueue - -```go -func (q *Queue) Enqueue(ctx context.Context, presetName string, action func(ctx context.Context) error, options ...EnqueueOption) (string, error) -``` - -Submit a task to the queue. `presetName` maps to a key in `Config.Preset`; an empty string uses defaults. Returns the task ID or an error (context cancelled, queue full, or queue closed). - -### Queue.Shutdown - -```go -func (q *Queue) Shutdown(ctx context.Context) error -``` - -Close the queue and wait for all workers to finish. Returns an error with the remaining task count if the context times out. - -### EnqueueOption - -| Function | Signature | Description | -|----------|-----------|-------------| -| `WithTaskID` | `func WithTaskID(id string) EnqueueOption` | Set a custom task ID; defaults to auto-generated UUID v4 | -| `WithTimeout` | `func WithTimeout(d time.Duration) EnqueueOption` | Override per-task timeout | -| `WithCallback` | `func WithCallback(fn func(id string)) EnqueueOption` | Callback invoked after successful task completion | -| `WithRetry` | `func WithRetry(retryMax ...int) EnqueueOption` | Enable retry with default max 3; optionally specify limit | - -*** - -©️ 2025 [邱敬幃 Pardn Chiu](https://linkedin.com/in/pardnchiu) diff --git a/doc.zh.md b/doc.zh.md deleted file mode 100644 index 414c804..0000000 --- a/doc.zh.md +++ /dev/null @@ -1,226 +0,0 @@ -# go-queue - 技術文件 - -> 返回 [README](./README.zh.md) - -## 前置需求 - -- Go 1.23 或更高版本 - -## 安裝 - -### 使用 go get - -```bash -go get github.com/pardnchiu/go-queue -``` - -### 從原始碼建置 - -```bash -git clone https://github.com/pardnchiu/go-queue.git -cd go-queue -go test ./... -``` - -## 使用方式 - -### 基礎用法 - -建立佇列、啟動 Worker 並提交任務: - -```go -package main - -import ( - "context" - "fmt" - "log" - - goQueue "github.com/pardnchiu/go-queue" -) - -func main() { - q := goQueue.New(&goQueue.Config{ - Workers: 4, - }) - - ctx := context.Background() - if err := q.Start(ctx); err != nil { - log.Fatal(err) - } - - id, err := q.Enqueue(ctx, "", func(ctx context.Context) error { - fmt.Println("task executed") - return nil - }) - if err != nil { - log.Fatal(err) - } - fmt.Println("task id:", id) - - if err := q.Shutdown(ctx); err != nil { - log.Fatal(err) - } -} -``` - -### 使用 Preset 與優先級 - -透過 Preset 預定義不同任務類型的優先級與 Timeout: - -```go -q := goQueue.New(&goQueue.Config{ - Workers: 4, - Timeout: 30 * time.Second, - Preset: map[string]goQueue.PresetConfig{ - "critical": {Priority: goQueue.PriorityImmediate}, - "batch": {Priority: goQueue.PriorityLow, Timeout: 60 * time.Second}, - }, -}) - -ctx := context.Background() -q.Start(ctx) - -// 以 Immediate 優先級入列 -q.Enqueue(ctx, "critical", func(ctx context.Context) error { - // 高優先任務 - return nil -}) - -// 以 Low 優先級入列,Timeout 60 秒 -q.Enqueue(ctx, "batch", func(ctx context.Context) error { - // 低優先批次任務 - return nil -}) - -q.Shutdown(ctx) -``` - -### 使用 Retry 與 Callback - -為任務配置重試機制與完成回呼: - -```go -q := goQueue.New(&goQueue.Config{Workers: 2}) -ctx := context.Background() -q.Start(ctx) - -id, err := q.Enqueue(ctx, "", func(ctx context.Context) error { - // 可能失敗的操作 - return doSomething() -}, - goQueue.WithRetry(3), // 最多重試 3 次 - goQueue.WithTaskID("my-task-001"), // 自訂任務 ID - goQueue.WithTimeout(10*time.Second), // 任務級 Timeout - goQueue.WithCallback(func(id string) { // 完成回呼 - fmt.Printf("task %s completed\n", id) - }), -) -if err != nil { - log.Fatal(err) -} - -q.Shutdown(ctx) -``` - -### Graceful Shutdown 與 Timeout - -限制 Shutdown 等待時間,避免無限阻塞: - -```go -q := goQueue.New(&goQueue.Config{Workers: 4}) -ctx := context.Background() -q.Start(ctx) - -// ... 提交任務 ... - -shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second) -defer cancel() - -if err := q.Shutdown(shutdownCtx); err != nil { - fmt.Printf("shutdown timeout: %v\n", err) -} -``` - -## API 參考 - -### New - -```go -func New(config *Config) *Queue -``` - -建立新的佇列實例。`config` 為 `nil` 時使用預設值。 - -### Config - -```go -type Config struct { - Workers int // Worker 數量,預設 CPU * 2 - Size int // 佇列容量上限,預設 Workers * 64 - Timeout time.Duration // 全域任務 Timeout,預設 30 秒 - Preset map[string]PresetConfig // 任務類型預設配置 -} -``` - -### PresetConfig - -```go -type PresetConfig struct { - Priority Priority // 優先等級,預設 PriorityNormal - Timeout time.Duration // 任務 Timeout,0 = 依 Priority 自動計算 -} -``` - -### Priority - -```go -type Priority int - -const ( - PriorityImmediate Priority = iota // 最高優先 - PriorityHigh // 高優先 - PriorityRetry // 重試任務專用 - PriorityNormal // 一般(預設) - PriorityLow // 低優先 -) -``` - -Priority 影響 Timeout 計算:Immediate = base/4、High = base/2、Normal = base、Low = base*2,最終限制在 15~120 秒區間。 - -### Queue.Start - -```go -func (q *Queue) Start(ctx context.Context) error -``` - -啟動 Worker Pool。重複呼叫或佇列已關閉時回傳錯誤。 - -### Queue.Enqueue - -```go -func (q *Queue) Enqueue(ctx context.Context, presetName string, action func(ctx context.Context) error, options ...EnqueueOption) (string, error) -``` - -提交任務至佇列。`presetName` 對應 `Config.Preset` 中的鍵,空字串使用預設值。回傳任務 ID 或錯誤(Context 已取消、佇列已滿或已關閉)。 - -### Queue.Shutdown - -```go -func (q *Queue) Shutdown(ctx context.Context) error -``` - -關閉佇列並等待所有 Worker 完成。Context 超時時回傳剩餘任務數量的錯誤。 - -### EnqueueOption - -| 函式 | 簽章 | 說明 | -|------|------|------| -| `WithTaskID` | `func WithTaskID(id string) EnqueueOption` | 自訂任務 ID,預設自動產生 UUID v4 | -| `WithTimeout` | `func WithTimeout(d time.Duration) EnqueueOption` | 覆寫任務級 Timeout | -| `WithCallback` | `func WithCallback(fn func(id string)) EnqueueOption` | 任務成功完成後的回呼函式 | -| `WithRetry` | `func WithRetry(retryMax ...int) EnqueueOption` | 啟用重試,預設最多 3 次;可指定上限 | - -*** - -©️ 2025 [邱敬幃 Pardn Chiu](https://linkedin.com/in/pardnchiu) diff --git a/doc/README.zh.md b/doc/README.zh.md new file mode 100644 index 0000000..54b79dc --- /dev/null +++ b/doc/README.zh.md @@ -0,0 +1,69 @@ +> [!NOTE] +> 此 README 由 [SKILL](https://github.com/agenvoy/skill-readme-generate) 生成,英文版請參閱 [這裡](../README.md)。 + +*** + +

+PRIORITY-AWARE TASK QUEUE FOR GO +

+ +

+Go Reference +Release +License +Coverage +

+ +*** + +> Go 任務佇列函式庫,具備優先級排程、逾時重試與原子狀態機 + +## 目錄 + +- [功能特點](#功能特點) +- [架構](#架構) +- [授權](#授權) +- [Author](#author) + +## 功能特點 + +> `go get github.com/pardnchiu/go-queue` · [完整文件](./doc.zh.md) + +- **五級優先佇列** — Immediate/High/Retry/Normal/Low 排序,低優先級逾時後自動晉升。 +- **Worker 池並行** — 預設 CPU×2 個 worker,可調佇列容量與全域逾時。 +- **逾時與重試** — 任務逾時、panic 隔離、可設定重試次數並以最高優先級重新入隊。 +- **Functional Options** — WithTaskID、WithTimeout、WithCallback、WithRetry 彈性設定單次入隊。 +- **原子狀態機** — Created/Running/Closed 以 CAS 轉換,冪等 Shutdown 且無 mutex 競爭。 + +## 架構 + +> [完整架構](./architecture.zh.md) + +```mermaid +graph TB + App[應用程式] --> Enqueue[Enqueue] + Enqueue --> Pending[待處理 Heap] + Pending --> Workers[Worker 池] + Workers --> Execute[執行 / 逾時 / 重試] + App --> Start[Start] + App --> Shutdown[Shutdown] + Start --> Workers + Shutdown --> Pending +``` + +## 授權 + +本專案採用 [MIT LICENSE](../LICENSE)。 + +## Author + + + +

邱敬幃 Pardn Chiu

+ +hi@pardn.io
+https://www.linkedin.com/in/pardnchiu + +*** + +©️ 2025 [邱敬幃 Pardn Chiu](https://www.linkedin.com/in/pardnchiu) diff --git a/doc/architecture.md b/doc/architecture.md new file mode 100644 index 0000000..7781673 --- /dev/null +++ b/doc/architecture.md @@ -0,0 +1,136 @@ +# go-queue - Architecture + +> Back to [README](../README.md) + +## Overview + +```mermaid +graph TB + App[Application] --> Enqueue[Enqueue] + App --> Start[Start] + App --> Shutdown[Shutdown] + Enqueue --> Pending[Pending Heap] + Start --> Workers[Worker Pool] + Pending --> Workers + Workers --> Execute[Execute / Timeout / Retry] + Shutdown --> Pending + Shutdown --> Workers +``` + +## Module: Queue + +Public API and lifecycle entry point. Owns config, pending queue, and worker coordination. + +```mermaid +graph TB + subgraph Queue + New[New] --> Config[Config / Preset] + New --> Pending[pending] + Start[Start] --> Workers[worker goroutines] + Enqueue[Enqueue] --> Options[EnqueueOption] + Enqueue --> Pending + Workers --> Execute[execute] + Execute --> Retry[setRetry] + Retry --> Pending + Shutdown[Shutdown] --> Close[pending.Close] + Shutdown --> Wait[WaitGroup] + end + App[Caller] --> New + App --> Start + App --> Enqueue + App --> Shutdown +``` + +## Module: pending + +Mutex + condition-variable protected priority queue with promotion and close broadcast. + +```mermaid +graph TB + subgraph pending + Push[Push] --> Heap[taskHeap] + Pop[Pop] --> Promote[promoteLocked] + Promote --> Heap + Pop --> Heap + Close[Close] --> Broadcast[cond.Broadcast] + Len[Len] --> Heap + end + Queue[Queue] --> Push + Queue --> Pop + Queue --> Close +``` + +## Module: taskHeap + +`container/heap` ordered by priority then `startAt`, with capacity shrink on pop. + +```mermaid +classDiagram + class task { + +string ID + +string preset + +Priority priority + +func action + +Duration timeout + +func callback + +Time startAt + +bool retryOn + +int retryMax + +int retryTimes + } + class taskHeap { + +taskHeaps tasks + +int minCap + +Len() + +Less() + +Swap() + +Push() + +Pop() + } + taskHeap --> task : holds +``` + +## Data Flow + +```mermaid +sequenceDiagram + participant App as Application + participant Q as Queue + participant P as pending + participant W as Worker + participant T as task action + + App->>Q: New(config) + App->>Q: Start(ctx) + Q->>W: spawn workers + App->>Q: Enqueue(preset, action, opts) + Q->>P: Push(task) + P-->>W: Pop(task, promotions) + W->>T: action(ctx with timeout) + alt success + T-->>W: nil + W->>App: callback(id) async + else failure with retry + W->>P: Push(PriorityRetry) + else failure or timeout + W-->>W: slog error + end + App->>Q: Shutdown(ctx) + Q->>P: Close() + Q->>W: wait WaitGroup +``` + +## State Machine + +```mermaid +stateDiagram-v2 + [*] --> Created + Created --> Running: Start CAS + Running --> Closed: Shutdown CAS + Created --> Closed: Shutdown CAS + Closed --> Closed: Shutdown idempotent +``` + +*** + +©️ 2025 [邱敬幃 Pardn Chiu](https://www.linkedin.com/in/pardnchiu) diff --git a/doc/architecture.zh.md b/doc/architecture.zh.md new file mode 100644 index 0000000..5ee26ef --- /dev/null +++ b/doc/architecture.zh.md @@ -0,0 +1,136 @@ +# go-queue - 架構 + +> 返回 [README](./README.zh.md) + +## 概覽 + +```mermaid +graph TB + App[應用程式] --> Enqueue[Enqueue] + App --> Start[Start] + App --> Shutdown[Shutdown] + Enqueue --> Pending[待處理 Heap] + Start --> Workers[Worker 池] + Pending --> Workers + Workers --> Execute[執行 / 逾時 / 重試] + Shutdown --> Pending + Shutdown --> Workers +``` + +## Module: Queue + +對外 API 與生命週期入口,持有設定、pending 佇列與 worker 協調。 + +```mermaid +graph TB + subgraph Queue + New[New] --> Config[Config / Preset] + New --> Pending[pending] + Start[Start] --> Workers[worker goroutines] + Enqueue[Enqueue] --> Options[EnqueueOption] + Enqueue --> Pending + Workers --> Execute[execute] + Execute --> Retry[setRetry] + Retry --> Pending + Shutdown[Shutdown] --> Close[pending.Close] + Shutdown --> Wait[WaitGroup] + end + App[呼叫端] --> New + App --> Start + App --> Enqueue + App --> Shutdown +``` + +## Module: pending + +以 mutex + cond 保護的優先佇列,支援晉升與關閉廣播。 + +```mermaid +graph TB + subgraph pending + Push[Push] --> Heap[taskHeap] + Pop[Pop] --> Promote[promoteLocked] + Promote --> Heap + Pop --> Heap + Close[Close] --> Broadcast[cond.Broadcast] + Len[Len] --> Heap + end + Queue[Queue] --> Push + Queue --> Pop + Queue --> Close +``` + +## Module: taskHeap + +`container/heap` 實作,依 priority 再依 startAt 排序,並在縮減時回收容量。 + +```mermaid +classDiagram + class task { + +string ID + +string preset + +Priority priority + +func action + +Duration timeout + +func callback + +Time startAt + +bool retryOn + +int retryMax + +int retryTimes + } + class taskHeap { + +taskHeaps tasks + +int minCap + +Len() + +Less() + +Swap() + +Push() + +Pop() + } + taskHeap --> task : holds +``` + +## 資料流 + +```mermaid +sequenceDiagram + participant App as 應用程式 + participant Q as Queue + participant P as pending + participant W as Worker + participant T as task action + + App->>Q: New(config) + App->>Q: Start(ctx) + Q->>W: spawn workers + App->>Q: Enqueue(preset, action, opts) + Q->>P: Push(task) + P-->>W: Pop(task, promotions) + W->>T: action(ctx with timeout) + alt 成功 + T-->>W: nil + W->>App: callback(id) 非同步 + else 失敗且可重試 + W->>P: Push(PriorityRetry) + else 失敗或逾時 + W-->>W: slog error + end + App->>Q: Shutdown(ctx) + Q->>P: Close() + Q->>W: wait WaitGroup +``` + +## 狀態機 + +```mermaid +stateDiagram-v2 + [*] --> Created + Created --> Running: Start CAS + Running --> Closed: Shutdown CAS + Created --> Closed: Shutdown CAS + Closed --> Closed: Shutdown 冪等 +``` + +*** + +©️ 2025 [邱敬幃 Pardn Chiu](https://www.linkedin.com/in/pardnchiu) diff --git a/doc/doc.md b/doc/doc.md new file mode 100644 index 0000000..efde6f2 --- /dev/null +++ b/doc/doc.md @@ -0,0 +1,219 @@ +# go-queue - Documentation + +> Back to [README](../README.md) + +## Prerequisites + +- Go 1.23 or higher + +## Installation + +### Using go get + +```bash +go get github.com/pardnchiu/go-queue +``` + +### From Source + +```bash +git clone https://github.com/pardnchiu/go-queue.git +cd go-queue +go test ./... +``` + +## Usage + +### Basic + +```go +package main + +import ( + "context" + "fmt" + "log" + "time" + + goQueue "github.com/pardnchiu/go-queue/core" +) + +func main() { + q := goQueue.New(&goQueue.Config{ + Workers: 4, + }) + + ctx := context.Background() + if err := q.Start(ctx); err != nil { + log.Fatal(err) + } + defer func() { + if err := q.Shutdown(context.Background()); err != nil { + log.Printf("shutdown: %v", err) + } + }() + + id, err := q.Enqueue(ctx, "", func(ctx context.Context) error { + fmt.Println("task running") + return nil + }) + if err != nil { + log.Fatal(err) + } + fmt.Println("enqueued:", id) + time.Sleep(100 * time.Millisecond) +} +``` + +### Priority Presets + +```go +q := goQueue.New(&goQueue.Config{ + Workers: 2, + Timeout: 30 * time.Second, + Preset: map[string]goQueue.PresetConfig{ + "urgent": {Priority: goQueue.PriorityImmediate}, + "batch": {Priority: goQueue.PriorityLow, Timeout: 60 * time.Second}, + }, +}) + +q.Start(ctx) + +// high priority first +q.Enqueue(ctx, "urgent", func(ctx context.Context) error { + // ... + return nil +}) + +q.Enqueue(ctx, "batch", func(ctx context.Context) error { + // ... + return nil +}) +``` + +### Enqueue Options + +```go +id, err := q.Enqueue(ctx, "urgent", func(ctx context.Context) error { + // business logic + return nil +}, + goQueue.WithTaskID("order-123"), + goQueue.WithTimeout(10*time.Second), + goQueue.WithRetry(2), + goQueue.WithCallback(func(id string) { + fmt.Println("done:", id) + }), +) +if err != nil { + log.Fatal(err) +} +``` + +### Graceful Shutdown + +```go +shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second) +defer cancel() + +if err := q.Shutdown(shutdownCtx); err != nil { + // timed out: tasks still remaining + log.Printf("shutdown: %v", err) +} +``` + +## API Reference + +### New + +```go +func New(config *Config) *Queue +``` + +Creates a queue. Pass `nil` for defaults. + +| Field | Type | Default | Description | +|------|------|------|------| +| `Workers` | `int` | `runtime.NumCPU() * 2` | Worker count | +| `Size` | `int` | `Workers * 64` | Pending queue capacity | +| `Timeout` | `time.Duration` | `30 * time.Second` | Base timeout | +| `Preset` | `map[string]PresetConfig` | empty | Named priority/timeout presets | + +### PresetConfig + +```go +type PresetConfig struct { + Priority Priority + Timeout time.Duration +} +``` + +| Field | Description | +|------|------| +| `Priority` | Priority level; zero value is `PriorityImmediate` (iota 0) | +| `Timeout` | When `0`, derived from base `Timeout` by priority | + +### Priority + +| Constant | Value | Description | +|------|----|------| +| `PriorityImmediate` | 0 | Highest, run first | +| `PriorityHigh` | 1 | High priority | +| `PriorityRetry` | 2 | Retry tasks | +| `PriorityNormal` | 3 | Default | +| `PriorityLow` | 4 | Lowest; may be promoted after wait | + +### Start + +```go +func (q *Queue) Start(ctx context.Context) error +``` + +Starts the worker pool. Transitions only from `Created` to `Running`; returns error if already started or closed. + +### Enqueue + +```go +func (q *Queue) Enqueue(ctx context.Context, presetName string, action func(ctx context.Context) error, options ...EnqueueOption) (string, error) +``` + +Enqueues a task and returns its ID. Errors when the queue is closed, full, or `ctx` is canceled. + +### EnqueueOption + +| Option | Signature | Description | +|------|------|------| +| `WithTaskID` | `func WithTaskID(id string) EnqueueOption` | Custom task ID; auto UUID if omitted | +| `WithTimeout` | `func WithTimeout(d time.Duration) EnqueueOption` | Override per-task timeout | +| `WithCallback` | `func WithCallback(fn func(id string)) EnqueueOption` | Async callback after success | +| `WithRetry` | `func WithRetry(retryMax ...int) EnqueueOption` | Enable retries; default max 3 when arg omitted | + +### Shutdown + +```go +func (q *Queue) Shutdown(ctx context.Context) error +``` + +Closes the queue, drains pending work, and waits for workers. On `ctx` timeout, returns remaining task count. Idempotent. + +### Timeout Derivation + +Base is `Config.Timeout` (or preset override), then adjusted by priority and clamped to 15s–120s: + +| Priority | Formula | +|----------|------| +| Immediate | `timeout / 4` | +| High / Retry | `timeout / 2` | +| Normal | `timeout` | +| Low | `timeout * 2` | + +### Priority Promotion + +| From | Wait | To | +|------|----------|------| +| Low | `clamp(Timeout, 30s, 120s)` | Normal | +| Normal | `clamp(Timeout*2, 30s, 120s)` | High | + +*** + +©️ 2025 [邱敬幃 Pardn Chiu](https://www.linkedin.com/in/pardnchiu) diff --git a/doc/doc.zh.md b/doc/doc.zh.md new file mode 100644 index 0000000..b1c60c9 --- /dev/null +++ b/doc/doc.zh.md @@ -0,0 +1,219 @@ +# go-queue - 技術文件 + +> 返回 [README](./README.zh.md) + +## 前置需求 + +- Go 1.23 或更高版本 + +## 安裝 + +### 使用 go get + +```bash +go get github.com/pardnchiu/go-queue +``` + +### 從原始碼 + +```bash +git clone https://github.com/pardnchiu/go-queue.git +cd go-queue +go test ./... +``` + +## 使用方式 + +### 基礎用法 + +```go +package main + +import ( + "context" + "fmt" + "log" + "time" + + goQueue "github.com/pardnchiu/go-queue/core" +) + +func main() { + q := goQueue.New(&goQueue.Config{ + Workers: 4, + }) + + ctx := context.Background() + if err := q.Start(ctx); err != nil { + log.Fatal(err) + } + defer func() { + if err := q.Shutdown(context.Background()); err != nil { + log.Printf("shutdown: %v", err) + } + }() + + id, err := q.Enqueue(ctx, "", func(ctx context.Context) error { + fmt.Println("task running") + return nil + }) + if err != nil { + log.Fatal(err) + } + fmt.Println("enqueued:", id) + time.Sleep(100 * time.Millisecond) +} +``` + +### 優先級預設 + +```go +q := goQueue.New(&goQueue.Config{ + Workers: 2, + Timeout: 30 * time.Second, + Preset: map[string]goQueue.PresetConfig{ + "urgent": {Priority: goQueue.PriorityImmediate}, + "batch": {Priority: goQueue.PriorityLow, Timeout: 60 * time.Second}, + }, +}) + +q.Start(ctx) + +// high priority first +q.Enqueue(ctx, "urgent", func(ctx context.Context) error { + // ... + return nil +}) + +q.Enqueue(ctx, "batch", func(ctx context.Context) error { + // ... + return nil +}) +``` + +### 入隊選項 + +```go +id, err := q.Enqueue(ctx, "urgent", func(ctx context.Context) error { + // 業務邏輯 + return nil +}, + goQueue.WithTaskID("order-123"), + goQueue.WithTimeout(10*time.Second), + goQueue.WithRetry(2), + goQueue.WithCallback(func(id string) { + fmt.Println("done:", id) + }), +) +if err != nil { + log.Fatal(err) +} +``` + +### 優雅關閉 + +```go +shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second) +defer cancel() + +if err := q.Shutdown(shutdownCtx); err != nil { + // 逾時:仍有任務未完成 + log.Printf("shutdown: %v", err) +} +``` + +## API 參考 + +### New + +```go +func New(config *Config) *Queue +``` + +建立佇列。`config` 可為 `nil`,此時使用預設值。 + +| 欄位 | 型別 | 預設 | 說明 | +|------|------|------|------| +| `Workers` | `int` | `runtime.NumCPU() * 2` | Worker 數量 | +| `Size` | `int` | `Workers * 64` | 待處理佇列容量 | +| `Timeout` | `time.Duration` | `30 * time.Second` | 基準逾時 | +| `Preset` | `map[string]PresetConfig` | empty | 具名優先級/逾時設定 | + +### PresetConfig + +```go +type PresetConfig struct { + Priority Priority + Timeout time.Duration +} +``` + +| 欄位 | 說明 | +|------|------| +| `Priority` | 優先級;零值為 `PriorityImmediate`(iota 0) | +| `Timeout` | `0` 時依 Priority 由基準 `Timeout` 推算 | + +### Priority + +| 常數 | 值 | 說明 | +|------|----|------| +| `PriorityImmediate` | 0 | 最高,立刻執行 | +| `PriorityHigh` | 1 | 高優先 | +| `PriorityRetry` | 2 | 重試任務 | +| `PriorityNormal` | 3 | 一般 | +| `PriorityLow` | 4 | 最低;逾時後可晉升 | + +### Start + +```go +func (q *Queue) Start(ctx context.Context) error +``` + +啟動 worker 池。僅能從 `Created` 轉為 `Running`;重複呼叫或關閉後呼叫會回傳錯誤。 + +### Enqueue + +```go +func (q *Queue) Enqueue(ctx context.Context, presetName string, action func(ctx context.Context) error, options ...EnqueueOption) (string, error) +``` + +將任務入隊並回傳 task ID。佇列已關閉、已滿,或 `ctx` 已取消時回傳錯誤。 + +### EnqueueOption + +| 選項 | 簽章 | 說明 | +|------|------|------| +| `WithTaskID` | `func WithTaskID(id string) EnqueueOption` | 自訂 task ID;省略則自動產生 UUID | +| `WithTimeout` | `func WithTimeout(d time.Duration) EnqueueOption` | 覆寫本次任務逾時 | +| `WithCallback` | `func WithCallback(fn func(id string)) EnqueueOption` | 成功完成後非同步回呼 | +| `WithRetry` | `func WithRetry(retryMax ...int) EnqueueOption` | 啟用重試;省略參數時預設最多 3 次 | + +### Shutdown + +```go +func (q *Queue) Shutdown(ctx context.Context) error +``` + +關閉佇列、排空待處理任務並等待 worker 結束。`ctx` 逾時時回傳剩餘任務數錯誤。可重複呼叫(冪等)。 + +### 逾時推算規則 + +基準為 `Config.Timeout`(或 preset 覆寫),再依優先級調整,並限制在 15s–120s: + +| Priority | 計算 | +|----------|------| +| Immediate | `timeout / 4` | +| High / Retry | `timeout / 2` | +| Normal | `timeout` | +| Low | `timeout * 2` | + +### 優先級晉升 + +| 來源 | 等待時間 | 目標 | +|------|----------|------| +| Low | `clamp(Timeout, 30s, 120s)` | Normal | +| Normal | `clamp(Timeout*2, 30s, 120s)` | High | + +*** + +©️ 2025 [邱敬幃 Pardn Chiu](https://www.linkedin.com/in/pardnchiu)