├── .github
├── FUNDING.yml
├── dependabot.yml
└── workflows
│ ├── codeql.yaml
│ ├── go.yml
│ └── goreleaser.yml
├── .gitignore
├── .golangci.yml
├── .goreleaser.yaml
├── LICENSE
├── README.md
├── README.zh-cn.md
├── README.zh-tw.md
├── _example
├── example01
│ ├── go.mod
│ ├── go.sum
│ └── main.go
└── example02
│ ├── go.mod
│ ├── go.sum
│ └── main.go
├── bearer.yml
├── benchmark_test.go
├── core
└── worker.go
├── errors.go
├── go.mod
├── go.sum
├── images
├── _base.d2
├── flow-01.d2
├── flow-01.svg
├── flow-02.d2
├── flow-02.svg
├── flow-03.d2
├── flow-03.svg
├── nats.svg
└── rabbitmq.svg
├── job
├── benchmark_test.go
├── job.go
├── job_test.go
├── option.go
└── option_test.go
├── logger.go
├── logger_test.go
├── metric.go
├── metric_test.go
├── mocks
├── mock_queued_message.go
├── mock_task_message.go
├── mock_worker.go
└── mocks.go
├── options.go
├── options_test.go
├── pool.go
├── pool_test.go
├── queue.go
├── queue_example_test.go
├── queue_test.go
├── recovery.go
├── ring.go
├── ring_test.go
├── thread.go
└── thread_test.go
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | # These are supported funding model platforms
2 |
3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
4 | patreon: # Replace with a single Patreon username
5 | open_collective: # Replace with a single Open Collective username
6 | ko_fi: # Replace with a single Ko-fi username
7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
9 | liberapay: # Replace with a single Liberapay username
10 | issuehunt: # Replace with a single IssueHunt username
11 | otechie: # Replace with a single Otechie username
12 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
13 | custom: ['https://www.paypal.me/appleboy46']
14 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: github-actions
4 | directory: /
5 | schedule:
6 | interval: weekly
7 | - package-ecosystem: gomod
8 | directory: /
9 | schedule:
10 | interval: weekly
11 |
--------------------------------------------------------------------------------
/.github/workflows/codeql.yaml:
--------------------------------------------------------------------------------
1 | # For most projects, this workflow file will not need changing; you simply need
2 | # to commit it to your repository.
3 | #
4 | # You may wish to alter this file to override the set of languages analyzed,
5 | # or to provide custom queries or build logic.
6 | name: "CodeQL"
7 |
8 | on:
9 | push:
10 | branches: [master]
11 | pull_request:
12 | # The branches below must be a subset of the branches above
13 | branches: [master]
14 | schedule:
15 | - cron: "30 1 * * 0"
16 |
17 | jobs:
18 | analyze:
19 | name: Analyze
20 | runs-on: ubuntu-latest
21 |
22 | permissions:
23 | # required for all workflows
24 | security-events: write
25 |
26 | # only required for workflows in private repositories
27 | actions: read
28 | contents: read
29 |
30 | strategy:
31 | fail-fast: false
32 | matrix:
33 | # Override automatic language detection by changing the below list
34 | # Supported options are ['csharp', 'cpp', 'go', 'java', 'javascript', 'python']
35 | # TODO: Enable for javascript later
36 | language: ["go"]
37 |
38 | steps:
39 | - name: Checkout repository
40 | uses: actions/checkout@v4
41 |
42 | # Initializes the CodeQL tools for scanning.
43 | - name: Initialize CodeQL
44 | uses: github/codeql-action/init@v3
45 | with:
46 | languages: ${{ matrix.language }}
47 | # If you wish to specify custom queries, you can do so here or in a config file.
48 | # By default, queries listed here will override any specified in a config file.
49 | # Prefix the list here with "+" to use these queries and those in the config file.
50 | # queries: ./path/to/local/query, your-org/your-repo/queries@main
51 |
52 | - name: Perform CodeQL Analysis
53 | uses: github/codeql-action/analyze@v3
54 |
--------------------------------------------------------------------------------
/.github/workflows/go.yml:
--------------------------------------------------------------------------------
1 | name: Run Tests
2 |
3 | on:
4 | push:
5 | branches:
6 | - master
7 | pull_request:
8 | branches:
9 | - master
10 |
11 | permissions:
12 | actions: read
13 | contents: read
14 | statuses: write
15 |
16 | concurrency:
17 | group: ${{ github.workflow }}-${{ github.ref }}
18 | cancel-in-progress: true
19 |
20 | jobs:
21 | lint:
22 | runs-on: ubuntu-latest
23 | steps:
24 | - name: Checkout Code
25 | uses: actions/checkout@v4
26 | with:
27 | ref: ${{ github.ref }}
28 | - name: Setup go
29 | uses: actions/setup-go@v5
30 | with:
31 | go-version-file: "go.mod"
32 | check-latest: true
33 | - name: Setup golangci-lint
34 | uses: golangci/golangci-lint-action@v8
35 | with:
36 | version: v2.1
37 | args: --verbose
38 |
39 | - name: Bearer
40 | uses: bearer/bearer-action@v2
41 | with:
42 | diff: true
43 |
44 | test:
45 | strategy:
46 | matrix:
47 | os: [ubuntu-latest]
48 | go: [1.23, 1.24]
49 | include:
50 | - os: ubuntu-latest
51 | go-build: ~/.cache/go-build
52 | name: ${{ matrix.os }} @ Go ${{ matrix.go }}
53 | runs-on: ${{ matrix.os }}
54 | env:
55 | GO111MODULE: on
56 | GOPROXY: https://proxy.golang.org
57 | steps:
58 | - name: Set up Go ${{ matrix.go }}
59 | uses: actions/setup-go@v5
60 | with:
61 | go-version: ${{ matrix.go }}
62 |
63 | - name: Checkout Code
64 | uses: actions/checkout@v4
65 | with:
66 | ref: ${{ github.ref }}
67 |
68 | - uses: actions/cache@v4
69 | with:
70 | path: |
71 | ${{ matrix.go-build }}
72 | ~/go/pkg/mod
73 | key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
74 | restore-keys: |
75 | ${{ runner.os }}-go-
76 | - name: Run Tests
77 | run: |
78 | go test -race -v -covermode=atomic -coverprofile=coverage.out
79 |
80 | - name: Run Benchmark
81 | run: |
82 | go test -v -run=^$ -count 5 -benchmem -bench . ./...
83 |
84 | - name: Upload coverage to Codecov
85 | uses: codecov/codecov-action@v5
86 | with:
87 | flags: ${{ matrix.os }},go-${{ matrix.go }}
88 |
--------------------------------------------------------------------------------
/.github/workflows/goreleaser.yml:
--------------------------------------------------------------------------------
1 | name: Goreleaser
2 |
3 | on:
4 | push:
5 | tags:
6 | - "*"
7 |
8 | permissions:
9 | contents: write
10 |
11 | jobs:
12 | goreleaser:
13 | runs-on: ubuntu-latest
14 | steps:
15 | - name: Checkout
16 | uses: actions/checkout@v4
17 | with:
18 | fetch-depth: 0
19 | - name: Set up Go
20 | uses: actions/setup-go@v5
21 | with:
22 | go-version: "^1"
23 | - name: Run GoReleaser
24 | uses: goreleaser/goreleaser-action@v6
25 | with:
26 | # either 'goreleaser' (default) or 'goreleaser-pro'
27 | distribution: goreleaser
28 | version: latest
29 | args: release --clean
30 | env:
31 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
32 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Binaries for programs and plugins
2 | *.exe
3 | *.exe~
4 | *.dll
5 | *.so
6 | *.dylib
7 |
8 | # Test binary, built with `go test -c`
9 | *.test
10 |
11 | # Output of the go coverage tool, specifically when used with LiteIDE
12 | *.out
13 |
14 | # Dependency directories (remove the comment below to include it)
15 | # vendor/
16 |
--------------------------------------------------------------------------------
/.golangci.yml:
--------------------------------------------------------------------------------
1 | version: "2"
2 | linters:
3 | default: none
4 | enable:
5 | - bodyclose
6 | - dogsled
7 | - dupl
8 | - errcheck
9 | - exhaustive
10 | - gochecknoinits
11 | - goconst
12 | - gocritic
13 | - gocyclo
14 | - goprintffuncname
15 | - gosec
16 | - govet
17 | - ineffassign
18 | - lll
19 | - misspell
20 | - nakedret
21 | - noctx
22 | - nolintlint
23 | - staticcheck
24 | - unconvert
25 | - unparam
26 | - unused
27 | - whitespace
28 | exclusions:
29 | generated: lax
30 | presets:
31 | - comments
32 | - common-false-positives
33 | - legacy
34 | - std-error-handling
35 | paths:
36 | - third_party$
37 | - builtin$
38 | - examples$
39 | formatters:
40 | enable:
41 | - gofmt
42 | - gofumpt
43 | - goimports
44 | exclusions:
45 | generated: lax
46 | paths:
47 | - third_party$
48 | - builtin$
49 | - examples$
50 |
--------------------------------------------------------------------------------
/.goreleaser.yaml:
--------------------------------------------------------------------------------
1 | builds:
2 | - # If true, skip the build.
3 | # Useful for library projects.
4 | # Default is false
5 | skip: true
6 |
7 | changelog:
8 | # Set it to true if you wish to skip the changelog generation.
9 | # This may result in an empty release notes on GitHub/GitLab/Gitea.
10 | disable: false
11 |
12 | # Changelog generation implementation to use.
13 | #
14 | # Valid options are:
15 | # - `git`: uses `git log`;
16 | # - `github`: uses the compare GitHub API, appending the author login to the changelog.
17 | # - `gitlab`: uses the compare GitLab API, appending the author name and email to the changelog.
18 | # - `github-native`: uses the GitHub release notes generation API, disables the groups feature.
19 | #
20 | # Defaults to `git`.
21 | use: github
22 |
23 | # Sorts the changelog by the commit's messages.
24 | # Could either be asc, desc or empty
25 | # Default is empty
26 | sort: asc
27 |
28 | # Group commits messages by given regex and title.
29 | # Order value defines the order of the groups.
30 | # Proving no regex means all commits will be grouped under the default group.
31 | # Groups are disabled when using github-native, as it already groups things by itself.
32 | #
33 | # Default is no groups.
34 | groups:
35 | - title: Features
36 | regexp: "^.*feat[(\\w-)]*:+.*$"
37 | order: 0
38 | - title: "Bug fixes"
39 | regexp: "^.*fix[(\\w-)]*:+.*$"
40 | order: 1
41 | - title: "Enhancements"
42 | regexp: "^.*chore[(\\w-)]*:+.*$"
43 | order: 2
44 | - title: "Refactor"
45 | regexp: "^.*refactor[(\\w-)]*:+.*$"
46 | order: 3
47 | - title: "Build process updates"
48 | regexp: ^.*?(build|ci)(\(.+\))??!?:.+$
49 | order: 4
50 | - title: "Documentation updates"
51 | regexp: ^.*?docs?(\(.+\))??!?:.+$
52 | order: 4
53 | - title: Others
54 | order: 999
55 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 Bo-Yi Wu
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Queue
2 |
3 | [](https://github.com/golang-queue/queue/actions/workflows/codeql.yaml)
4 | [](https://github.com/golang-queue/queue/actions/workflows/go.yml)
5 | [](https://codecov.io/gh/golang-queue/queue)
6 |
7 | [繁體中文](./README.zh-tw.md) | [简体中文](./README.zh-cn.md)
8 |
9 | Queue is a Golang library designed to help you create and manage a pool of Goroutines (lightweight threads). It allows you to efficiently run multiple tasks in parallel, utilizing the full CPU capacity of your machine.
10 |
11 | ## Features
12 |
13 | - [x] Supports [Circular buffer](https://en.wikipedia.org/wiki/Circular_buffer) queues.
14 | - [x] Integrates with [NSQ](https://nsq.io/) for real-time distributed messaging.
15 | - [x] Integrates with [NATS](https://nats.io/) for adaptive edge and distributed systems.
16 | - [x] Integrates with [Redis Pub/Sub](https://redis.io/docs/manual/pubsub/).
17 | - [x] Integrates with [Redis Streams](https://redis.io/docs/manual/data-types/streams/).
18 | - [x] Integrates with [RabbitMQ](https://www.rabbitmq.com/).
19 |
20 | ## Queue Scenario
21 |
22 | A simple queue service using a ring buffer as the default backend.
23 |
24 | 
25 |
26 | Easily switch the queue service to use NSQ, NATS, or Redis.
27 |
28 | 
29 |
30 | Supports multiple producers and consumers.
31 |
32 | 
33 |
34 | ## Requirements
35 |
36 | Go version **1.22** or above
37 |
38 | ## Installation
39 |
40 | To install the stable version:
41 |
42 | ```sh
43 | go get github.com/golang-queue/queue
44 | ```
45 |
46 | To install the latest version:
47 |
48 | ```sh
49 | go get github.com/golang-queue/queue@master
50 | ```
51 |
52 | ## Usage
53 |
54 | ### Basic Usage of Pool (using the Task function)
55 |
56 | By calling the `QueueTask()` method, you can schedule tasks to be executed by workers (Goroutines) in the pool.
57 |
58 | ```go
59 | package main
60 |
61 | import (
62 | "context"
63 | "fmt"
64 | "time"
65 |
66 | "github.com/golang-queue/queue"
67 | )
68 |
69 | func main() {
70 | taskN := 100
71 | rets := make(chan string, taskN)
72 |
73 | // initialize the queue pool
74 | q := queue.NewPool(5)
75 | // shut down the service and notify all workers
76 | // wait until all jobs are complete
77 | defer q.Release()
78 |
79 | // assign tasks to the queue
80 | for i := 0; i < taskN; i++ {
81 | go func(i int) {
82 | if err := q.QueueTask(func(ctx context.Context) error {
83 | rets <- fmt.Sprintf("Hi Gopher, handle the job: %02d", +i)
84 | return nil
85 | }); err != nil {
86 | panic(err)
87 | }
88 | }(i)
89 | }
90 |
91 | // wait until all tasks are done
92 | for i := 0; i < taskN; i++ {
93 | fmt.Println("message:", <-rets)
94 | time.Sleep(20 * time.Millisecond)
95 | }
96 | }
97 | ```
98 |
99 | ### Basic Usage of Pool (using a message queue)
100 |
101 | Define a new message struct and implement the `Bytes()` function to encode the message. Use the `WithFn` function to handle messages from the queue.
102 |
103 | ```go
104 | package main
105 |
106 | import (
107 | "context"
108 | "encoding/json"
109 | "fmt"
110 | "log"
111 | "time"
112 |
113 | "github.com/golang-queue/queue"
114 | "github.com/golang-queue/queue/core"
115 | )
116 |
117 | type job struct {
118 | Name string
119 | Message string
120 | }
121 |
122 | func (j *job) Bytes() []byte {
123 | b, err := json.Marshal(j)
124 | if err != nil {
125 | panic(err)
126 | }
127 | return b
128 | }
129 |
130 | func main() {
131 | taskN := 100
132 | rets := make(chan string, taskN)
133 |
134 | // initialize the queue pool
135 | q := queue.NewPool(5, queue.WithFn(func(ctx context.Context, m core.TaskMessage) error {
136 | var v job
137 | if err := json.Unmarshal(m.Payload(), &v); err != nil {
138 | return err
139 | }
140 |
141 | rets <- "Hi, " + v.Name + ", " + v.Message
142 | return nil
143 | }))
144 | // shut down the service and notify all workers
145 | // wait until all jobs are complete
146 | defer q.Release()
147 |
148 | // assign tasks to the queue
149 | for i := 0; i < taskN; i++ {
150 | go func(i int) {
151 | if err := q.Queue(&job{
152 | Name: "Gopher",
153 | Message: fmt.Sprintf("handle the job: %d", i+1),
154 | }); err != nil {
155 | log.Println(err)
156 | }
157 | }(i)
158 | }
159 |
160 | // wait until all tasks are done
161 | for i := 0; i < taskN; i++ {
162 | fmt.Println("message:", <-rets)
163 | time.Sleep(50 * time.Millisecond)
164 | }
165 | }
166 | ```
167 |
168 | ## Using NSQ as a Queue
169 |
170 | Refer to the [NSQ documentation](https://github.com/golang-queue/nsq) for more details.
171 |
172 | ```go
173 | package main
174 |
175 | import (
176 | "context"
177 | "encoding/json"
178 | "fmt"
179 | "log"
180 | "time"
181 |
182 | "github.com/golang-queue/nsq"
183 | "github.com/golang-queue/queue"
184 | "github.com/golang-queue/queue/core"
185 | )
186 |
187 | type job struct {
188 | Message string
189 | }
190 |
191 | func (j *job) Bytes() []byte {
192 | b, err := json.Marshal(j)
193 | if err != nil {
194 | panic(err)
195 | }
196 | return b
197 | }
198 |
199 | func main() {
200 | taskN := 100
201 | rets := make(chan string, taskN)
202 |
203 | // define the worker
204 | w := nsq.NewWorker(
205 | nsq.WithAddr("127.0.0.1:4150"),
206 | nsq.WithTopic("example"),
207 | nsq.WithChannel("foobar"),
208 | // concurrent job number
209 | nsq.WithMaxInFlight(10),
210 | nsq.WithRunFunc(func(ctx context.Context, m core.TaskMessage) error {
211 | var v job
212 | if err := json.Unmarshal(m.Payload(), &v); err != nil {
213 | return err
214 | }
215 |
216 | rets <- v.Message
217 | return nil
218 | }),
219 | )
220 |
221 | // define the queue
222 | q := queue.NewPool(
223 | 5,
224 | queue.WithWorker(w),
225 | )
226 |
227 | // assign tasks to the queue
228 | for i := 0; i < taskN; i++ {
229 | go func(i int) {
230 | q.Queue(&job{
231 | Message: fmt.Sprintf("handle the job: %d", i+1),
232 | })
233 | }(i)
234 | }
235 |
236 | // wait until all tasks are done
237 | for i := 0; i < taskN; i++ {
238 | fmt.Println("message:", <-rets)
239 | time.Sleep(50 * time.Millisecond)
240 | }
241 |
242 | // shut down the service and notify all workers
243 | q.Release()
244 | }
245 | ```
246 |
247 | ## Using NATS as a Queue
248 |
249 | Refer to the [NATS documentation](https://github.com/golang-queue/nats) for more details.
250 |
251 | ```go
252 | package main
253 |
254 | import (
255 | "context"
256 | "encoding/json"
257 | "fmt"
258 | "log"
259 | "time"
260 |
261 | "github.com/golang-queue/nats"
262 | "github.com/golang-queue/queue"
263 | "github.com/golang-queue/queue/core"
264 | )
265 |
266 | type job struct {
267 | Message string
268 | }
269 |
270 | func (j *job) Bytes() []byte {
271 | b, err := json.Marshal(j)
272 | if err != nil {
273 | panic(err)
274 | }
275 | return b
276 | }
277 |
278 | func main() {
279 | taskN := 100
280 | rets := make(chan string, taskN)
281 |
282 | // define the worker
283 | w := nats.NewWorker(
284 | nats.WithAddr("127.0.0.1:4222"),
285 | nats.WithSubj("example"),
286 | nats.WithQueue("foobar"),
287 | nats.WithRunFunc(func(ctx context.Context, m core.TaskMessage) error {
288 | var v job
289 | if err := json.Unmarshal(m.Payload(), &v); err != nil {
290 | return err
291 | }
292 |
293 | rets <- v.Message
294 | return nil
295 | }),
296 | )
297 |
298 | // define the queue
299 | q, err := queue.NewQueue(
300 | queue.WithWorkerCount(10),
301 | queue.WithWorker(w),
302 | )
303 | if err != nil {
304 | log.Fatal(err)
305 | }
306 |
307 | // start the workers
308 | q.Start()
309 |
310 | // assign tasks to the queue
311 | for i := 0; i < taskN; i++ {
312 | go func(i int) {
313 | q.Queue(&job{
314 | Message: fmt.Sprintf("handle the job: %d", i+1),
315 | })
316 | }(i)
317 | }
318 |
319 | // wait until all tasks are done
320 | for i := 0; i < taskN; i++ {
321 | fmt.Println("message:", <-rets)
322 | time.Sleep(50 * time.Millisecond)
323 | }
324 |
325 | // shut down the service and notify all workers
326 | q.Release()
327 | }
328 | ```
329 |
330 | ## Using Redis (Pub/Sub) as a Queue
331 |
332 | Refer to the [Redis documentation](https://github.com/golang-queue/redisdb) for more details.
333 |
334 | ```go
335 | package main
336 |
337 | import (
338 | "context"
339 | "encoding/json"
340 | "fmt"
341 | "log"
342 | "time"
343 |
344 | "github.com/golang-queue/queue"
345 | "github.com/golang-queue/queue/core"
346 | "github.com/golang-queue/redisdb"
347 | )
348 |
349 | type job struct {
350 | Message string
351 | }
352 |
353 | func (j *job) Bytes() []byte {
354 | b, err := json.Marshal(j)
355 | if err != nil {
356 | panic(err)
357 | }
358 | return b
359 | }
360 |
361 | func main() {
362 | taskN := 100
363 | rets := make(chan string, taskN)
364 |
365 | // define the worker
366 | w := redisdb.NewWorker(
367 | redisdb.WithAddr("127.0.0.1:6379"),
368 | redisdb.WithChannel("foobar"),
369 | redisdb.WithRunFunc(func(ctx context.Context, m core.TaskMessage) error {
370 | var v job
371 | if err := json.Unmarshal(m.Payload(), &v); err != nil {
372 | return err
373 | }
374 |
375 | rets <- v.Message
376 | return nil
377 | }),
378 | )
379 |
380 | // define the queue
381 | q, err := queue.NewQueue(
382 | queue.WithWorkerCount(10),
383 | queue.WithWorker(w),
384 | )
385 | if err != nil {
386 | log.Fatal(err)
387 | }
388 |
389 | // start the workers
390 | q.Start()
391 |
392 | // assign tasks to the queue
393 | for i := 0; i < taskN; i++ {
394 | go func(i int) {
395 | q.Queue(&job{
396 | Message: fmt.Sprintf("handle the job: %d", i+1),
397 | })
398 | }(i)
399 | }
400 |
401 | // wait until all tasks are done
402 | for i := 0; i < taskN; i++ {
403 | fmt.Println("message:", <-rets)
404 | time.Sleep(50 * time.Millisecond)
405 | }
406 |
407 | // shut down the service and notify all workers
408 | q.Release()
409 | }
410 | ```
411 |
--------------------------------------------------------------------------------
/README.zh-cn.md:
--------------------------------------------------------------------------------
1 | # 队列
2 |
3 | [](https://github.com/golang-queue/queue/actions/workflows/codeql.yaml)
4 | [](https://github.com/golang-queue/queue/actions/workflows/go.yml)
5 | [](https://codecov.io/gh/golang-queue/queue)
6 |
7 | [English](./README.md) | [繁體中文](./README.zh-tw.md)
8 |
9 | Queue 是一个 Golang 库,帮助您创建和管理 Goroutines(轻量级线程)池。它允许您高效地并行运行多个任务,利用机器的 CPU 容量。
10 |
11 | ## 特性
12 |
13 | - [x] 支持 [循环缓冲区](https://en.wikipedia.org/wiki/Circular_buffer) 队列。
14 | - [x] 集成 [NSQ](https://nsq.io/) 进行实时分布式消息传递。
15 | - [x] 集成 [NATS](https://nats.io/) 以适应边缘和分布式系统。
16 | - [x] 集成 [Redis Pub/Sub](https://redis.io/docs/manual/pubsub/)。
17 | - [x] 集成 [Redis Streams](https://redis.io/docs/manual/data-types/streams/)。
18 | - [x] 集成 [RabbitMQ](https://www.rabbitmq.com/)。
19 |
20 | ## 队列场景
21 |
22 | 使用环形缓冲区作为默认后端的简单队列服务。
23 |
24 | 
25 |
26 | 轻松切换队列服务以使用 NSQ、NATS 或 Redis。
27 |
28 | 
29 |
30 | 支持多个生产者和消费者。
31 |
32 | 
33 |
34 | ## 要求
35 |
36 | Go 版本 **1.22** 或以上
37 |
38 | ## 安装
39 |
40 | 安装稳定版本:
41 |
42 | ```sh
43 | go get github.com/golang-queue/queue
44 | ```
45 |
46 | 安装最新版本:
47 |
48 | ```sh
49 | go get github.com/golang-queue/queue@master
50 | ```
51 |
52 | ## 使用
53 |
54 | ### 池的基本用法(使用 Task 函数)
55 |
56 | 通过调用 `QueueTask()` 方法,您可以安排任务由池中的工作者(Goroutines)执行。
57 |
58 | ```go
59 | package main
60 |
61 | import (
62 | "context"
63 | "fmt"
64 | "time"
65 |
66 | "github.com/golang-queue/queue"
67 | )
68 |
69 | func main() {
70 | taskN := 100
71 | rets := make(chan string, taskN)
72 |
73 | // 初始化队列池
74 | q := queue.NewPool(5)
75 | // 关闭服务并通知所有工作者
76 | // 等待所有作业完成
77 | defer q.Release()
78 |
79 | // 将任务分配到队列
80 | for i := 0; i < taskN; i++ {
81 | go func(i int) {
82 | if err := q.QueueTask(func(ctx context.Context) error {
83 | rets <- fmt.Sprintf("Hi Gopher, 处理作业: %02d", +i)
84 | return nil
85 | }); err != nil {
86 | panic(err)
87 | }
88 | }(i)
89 | }
90 |
91 | // 等待所有任务完成
92 | for i := 0; i < taskN; i++ {
93 | fmt.Println("消息:", <-rets)
94 | time.Sleep(20 * time.Millisecond)
95 | }
96 | }
97 | ```
98 |
99 | ### 池的基本用法(使用消息队列)
100 |
101 | 定义一个新的消息结构并实现 `Bytes()` 函数来编码消息。使用 `WithFn` 函数处理来自队列的消息。
102 |
103 | ```go
104 | package main
105 |
106 | import (
107 | "context"
108 | "encoding/json"
109 | "fmt"
110 | "log"
111 | "time"
112 |
113 | "github.com/golang-queue/queue"
114 | "github.com/golang-queue/queue/core"
115 | )
116 |
117 | type job struct {
118 | Name string
119 | Message string
120 | }
121 |
122 | func (j *job) Bytes() []byte {
123 | b, err := json.Marshal(j)
124 | if err != nil {
125 | panic(err)
126 | }
127 | return b
128 | }
129 |
130 | func main() {
131 | taskN := 100
132 | rets := make(chan string, taskN)
133 |
134 | // 初始化队列池
135 | q := queue.NewPool(5, queue.WithFn(func(ctx context.Context, m core.TaskMessage) error {
136 | var v job
137 | if err := json.Unmarshal(m.Payload(), &v); err != nil {
138 | return err
139 | }
140 |
141 | rets <- "Hi, " + v.Name + ", " + v.Message
142 | return nil
143 | }))
144 | // 关闭服务并通知所有工作者
145 | // 等待所有作业完成
146 | defer q.Release()
147 |
148 | // 将任务分配到队列
149 | for i := 0; i < taskN; i++ {
150 | go func(i int) {
151 | if err := q.Queue(&job{
152 | Name: "Gopher",
153 | Message: fmt.Sprintf("处理作业: %d", i+1),
154 | }); err != nil {
155 | log.Println(err)
156 | }
157 | }(i)
158 | }
159 |
160 | // 等待所有任务完成
161 | for i := 0; i < taskN; i++ {
162 | fmt.Println("消息:", <-rets)
163 | time.Sleep(50 * time.Millisecond)
164 | }
165 | }
166 | ```
167 |
168 | ## 使用 NSQ 作为队列
169 |
170 | 请参阅 [NSQ 文档](https://github.com/golang-queue/nsq) 了解更多详情。
171 |
172 | ```go
173 | package main
174 |
175 | import (
176 | "context"
177 | "encoding/json"
178 | "fmt"
179 | "log"
180 | "time"
181 |
182 | "github.com/golang-queue/nsq"
183 | "github.com/golang-queue/queue"
184 | "github.com/golang-queue/queue/core"
185 | )
186 |
187 | type job struct {
188 | Message string
189 | }
190 |
191 | func (j *job) Bytes() []byte {
192 | b, err := json.Marshal(j)
193 | if err != nil {
194 | panic(err)
195 | }
196 | return b
197 | }
198 |
199 | func main() {
200 | taskN := 100
201 | rets := make(chan string, taskN)
202 |
203 | // 定义工作者
204 | w := nsq.NewWorker(
205 | nsq.WithAddr("127.0.0.1:4150"),
206 | nsq.WithTopic("example"),
207 | nsq.WithChannel("foobar"),
208 | // 并发作业数量
209 | nsq.WithMaxInFlight(10),
210 | nsq.WithRunFunc(func(ctx context.Context, m core.TaskMessage) error {
211 | var v job
212 | if err := json.Unmarshal(m.Payload(), &v); err != nil {
213 | return err
214 | }
215 |
216 | rets <- v.Message
217 | return nil
218 | }),
219 | )
220 |
221 | // 定义队列
222 | q := queue.NewPool(
223 | 5,
224 | queue.WithWorker(w),
225 | )
226 |
227 | // 将任务分配到队列
228 | for i := 0; i < taskN; i++ {
229 | go func(i int) {
230 | q.Queue(&job{
231 | Message: fmt.Sprintf("处理作业: %d", i+1),
232 | })
233 | }(i)
234 | }
235 |
236 | // 等待所有任务完成
237 | for i := 0; i < taskN; i++ {
238 | fmt.Println("消息:", <-rets)
239 | time.Sleep(50 * time.Millisecond)
240 | }
241 |
242 | // 关闭服务并通知所有工作者
243 | q.Release()
244 | }
245 | ```
246 |
247 | ## 使用 NATS 作为队列
248 |
249 | 请参阅 [NATS 文档](https://github.com/golang-queue/nats) 了解更多详情。
250 |
251 | ```go
252 | package main
253 |
254 | import (
255 | "context"
256 | "encoding/json"
257 | "fmt"
258 | "log"
259 | "time"
260 |
261 | "github.com/golang-queue/nats"
262 | "github.com/golang-queue/queue"
263 | "github.com/golang-queue/queue/core"
264 | )
265 |
266 | type job struct {
267 | Message string
268 | }
269 |
270 | func (j *job) Bytes() []byte {
271 | b, err := json.Marshal(j)
272 | if err != nil {
273 | panic(err)
274 | }
275 | return b
276 | }
277 |
278 | func main() {
279 | taskN := 100
280 | rets := make(chan string, taskN)
281 |
282 | // 定义工作者
283 | w := nats.NewWorker(
284 | nats.WithAddr("127.0.0.1:4222"),
285 | nats.WithSubj("example"),
286 | nats.WithQueue("foobar"),
287 | nats.WithRunFunc(func(ctx context.Context, m core.TaskMessage) error {
288 | var v job
289 | if err := json.Unmarshal(m.Payload(), &v); err != nil {
290 | return err
291 | }
292 |
293 | rets <- v.Message
294 | return nil
295 | }),
296 | )
297 |
298 | // 定义队列
299 | q, err := queue.NewQueue(
300 | queue.WithWorkerCount(10),
301 | queue.WithWorker(w),
302 | )
303 | if err != nil {
304 | log.Fatal(err)
305 | }
306 |
307 | // 启动工作者
308 | q.Start()
309 |
310 | // 将任务分配到队列
311 | for i := 0; i < taskN; i++ {
312 | go func(i int) {
313 | q.Queue(&job{
314 | Message: fmt.Sprintf("处理作业: %d", i+1),
315 | })
316 | }(i)
317 | }
318 |
319 | // 等待所有任务完成
320 | for i := 0; i < taskN; i++ {
321 | fmt.Println("消息:", <-rets)
322 | time.Sleep(50 * time.Millisecond)
323 | }
324 |
325 | // 关闭服务并通知所有工作者
326 | q.Release()
327 | }
328 | ```
329 |
330 | ## 使用 Redis (Pub/Sub) 作为队列
331 |
332 | 请参阅 [Redis 文档](https://github.com/golang-queue/redisdb) 了解更多详情。
333 |
334 | ```go
335 | package main
336 |
337 | import (
338 | "context"
339 | "encoding/json"
340 | "fmt"
341 | "log"
342 | "time"
343 |
344 | "github.com/golang-queue/queue"
345 | "github.com/golang-queue/queue/core"
346 | "github.com/golang-queue/redisdb"
347 | )
348 |
349 | type job struct {
350 | Message string
351 | }
352 |
353 | func (j *job) Bytes() []byte {
354 | b, err := json.Marshal(j)
355 | if err != nil {
356 | panic(err)
357 | }
358 | return b
359 | }
360 |
361 | func main() {
362 | taskN := 100
363 | rets := make(chan string, taskN)
364 |
365 | // 定义工作者
366 | w := redisdb.NewWorker(
367 | redisdb.WithAddr("127.0.0.1:6379"),
368 | redisdb.WithChannel("foobar"),
369 | redisdb.WithRunFunc(func(ctx context.Context, m core.TaskMessage) error {
370 | var v job
371 | if err := json.Unmarshal(m.Payload(), &v); err != nil {
372 | return err
373 | }
374 |
375 | rets <- v.Message
376 | return nil
377 | }),
378 | )
379 |
380 | // 定义队列
381 | q, err := queue.NewQueue(
382 | queue.WithWorkerCount(10),
383 | queue.WithWorker(w),
384 | )
385 | if err != nil {
386 | log.Fatal(err)
387 | }
388 |
389 | // 启动工作者
390 | q.Start()
391 |
392 | // 将任务分配到队列
393 | for i := 0; i < taskN; i++ {
394 | go func(i int) {
395 | q.Queue(&job{
396 | Message: fmt.Sprintf("处理作业: %d", i+1),
397 | })
398 | }(i)
399 | }
400 |
401 | // 等待所有任务完成
402 | for i := 0; i < taskN; i++ {
403 | fmt.Println("消息:", <-rets)
404 | time.Sleep(50 * time.Millisecond)
405 | }
406 |
407 | // 关闭服务并通知所有工作者
408 | q.Release()
409 | }
410 | ```
411 |
--------------------------------------------------------------------------------
/README.zh-tw.md:
--------------------------------------------------------------------------------
1 | # Queue
2 |
3 | [](https://github.com/golang-queue/queue/actions/workflows/codeql.yaml)
4 | [](https://github.com/golang-queue/queue/actions/workflows/go.yml)
5 | [](https://codecov.io/gh/golang-queue/queue)
6 |
7 | [English](./README.md) | [简体中文](./README.zh-cn.md)
8 |
9 | Queue 是一個 Golang 函式庫,幫助你建立和管理 Goroutines(輕量級執行緒)的池。它允許你有效地並行執行多個任務,充分利用機器的 CPU 容量。
10 |
11 | ## 功能
12 |
13 | - [x] 支援 [環形緩衝區](https://en.wikipedia.org/wiki/Circular_buffer) 隊列。
14 | - [x] 與 [NSQ](https://nsq.io/) 整合,用於即時分佈式消息傳遞。
15 | - [x] 與 [NATS](https://nats.io/) 整合,用於自適應邊緣和分佈式系統。
16 | - [x] 與 [Redis Pub/Sub](https://redis.io/docs/manual/pubsub/) 整合。
17 | - [x] 與 [Redis Streams](https://redis.io/docs/manual/data-types/streams/) 整合。
18 | - [x] 與 [RabbitMQ](https://www.rabbitmq.com/) 整合。
19 |
20 | ## 隊列場景
21 |
22 | 使用環形緩衝區作為預設後端的簡單隊列服務。
23 |
24 | 
25 |
26 | 輕鬆切換隊列服務以使用 NSQ、NATS 或 Redis。
27 |
28 | 
29 |
30 | 支援多個生產者和消費者。
31 |
32 | 
33 |
34 | ## 要求
35 |
36 | Go 版本 **1.22** 或以上
37 |
38 | ## 安裝
39 |
40 | 安裝穩定版本:
41 |
42 | ```sh
43 | go get github.com/golang-queue/queue
44 | ```
45 |
46 | 安裝最新版本:
47 |
48 | ```sh
49 | go get github.com/golang-queue/queue@master
50 | ```
51 |
52 | ## 使用方法
53 |
54 | ### 池的基本使用(使用 Task 函數)
55 |
56 | 通過調用 `QueueTask()` 方法,你可以安排任務由池中的工作者(Goroutines)執行。
57 |
58 | ```go
59 | package main
60 |
61 | import (
62 | "context"
63 | "fmt"
64 | "time"
65 |
66 | "github.com/golang-queue/queue"
67 | )
68 |
69 | func main() {
70 | taskN := 100
71 | rets := make(chan string, taskN)
72 |
73 | // 初始化隊列池
74 | q := queue.NewPool(5)
75 | // 關閉服務並通知所有工作者
76 | // 等待所有工作完成
77 | defer q.Release()
78 |
79 | // 將任務分配給隊列
80 | for i := 0; i < taskN; i++ {
81 | go func(i int) {
82 | if err := q.QueueTask(func(ctx context.Context) error {
83 | rets <- fmt.Sprintf("Hi Gopher, 處理工作: %02d", +i)
84 | return nil
85 | }); err != nil {
86 | panic(err)
87 | }
88 | }(i)
89 | }
90 |
91 | // 等待所有任務完成
92 | for i := 0; i < taskN; i++ {
93 | fmt.Println("message:", <-rets)
94 | time.Sleep(20 * time.Millisecond)
95 | }
96 | }
97 | ```
98 |
99 | ### 池的基本使用(使用消息隊列)
100 |
101 | 定義一個新的消息結構並實現 `Bytes()` 函數來編碼消息。使用 `WithFn` 函數來處理來自隊列的消息。
102 |
103 | ```go
104 | package main
105 |
106 | import (
107 | "context"
108 | "encoding/json"
109 | "fmt"
110 | "log"
111 | "time"
112 |
113 | "github.com/golang-queue/queue"
114 | "github.com/golang-queue/queue/core"
115 | )
116 |
117 | type job struct {
118 | Name string
119 | Message string
120 | }
121 |
122 | func (j *job) Bytes() []byte {
123 | b, err := json.Marshal(j)
124 | if err != nil {
125 | panic(err)
126 | }
127 | return b
128 | }
129 |
130 | func main() {
131 | taskN := 100
132 | rets := make(chan string, taskN)
133 |
134 | // 初始化隊列池
135 | q := queue.NewPool(5, queue.WithFn(func(ctx context.Context, m core.TaskMessage) error {
136 | var v job
137 | if err := json.Unmarshal(m.Payload(), &v); err != nil {
138 | return err
139 | }
140 |
141 | rets <- "Hi, " + v.Name + ", " + v.Message
142 | return nil
143 | }))
144 | // 關閉服務並通知所有工作者
145 | // 等待所有工作完成
146 | defer q.Release()
147 |
148 | // 將任務分配給隊列
149 | for i := 0; i < taskN; i++ {
150 | go func(i int) {
151 | if err := q.Queue(&job{
152 | Name: "Gopher",
153 | Message: fmt.Sprintf("處理工作: %d", i+1),
154 | }); err != nil {
155 | log.Println(err)
156 | }
157 | }(i)
158 | }
159 |
160 | // 等待所有任務完成
161 | for i := 0; i < taskN; i++ {
162 | fmt.Println("message:", <-rets)
163 | time.Sleep(50 * time.Millisecond)
164 | }
165 | }
166 | ```
167 |
168 | ## 使用 NSQ 作為隊列
169 |
170 | 請參閱 [NSQ 文檔](https://github.com/golang-queue/nsq) 以獲取更多詳細信息。
171 |
172 | ```go
173 | package main
174 |
175 | import (
176 | "context"
177 | "encoding/json"
178 | "fmt"
179 | "log"
180 | "time"
181 |
182 | "github.com/golang-queue/nsq"
183 | "github.com/golang-queue/queue"
184 | "github.com/golang-queue/queue/core"
185 | )
186 |
187 | type job struct {
188 | Message string
189 | }
190 |
191 | func (j *job) Bytes() []byte {
192 | b, err := json.Marshal(j)
193 | if err != nil {
194 | panic(err)
195 | }
196 | return b
197 | }
198 |
199 | func main() {
200 | taskN := 100
201 | rets := make(chan string, taskN)
202 |
203 | // 定義工作者
204 | w := nsq.NewWorker(
205 | nsq.WithAddr("127.0.0.1:4150"),
206 | nsq.WithTopic("example"),
207 | nsq.WithChannel("foobar"),
208 | // 並發工作數量
209 | nsq.WithMaxInFlight(10),
210 | nsq.WithRunFunc(func(ctx context.Context, m core.TaskMessage) error {
211 | var v job
212 | if err := json.Unmarshal(m.Payload(), &v); err != nil {
213 | return err
214 | }
215 |
216 | rets <- v.Message
217 | return nil
218 | }),
219 | )
220 |
221 | // 定義隊列
222 | q := queue.NewPool(
223 | 5,
224 | queue.WithWorker(w),
225 | )
226 |
227 | // 將任務分配給隊列
228 | for i := 0; i < taskN; i++ {
229 | go func(i int) {
230 | q.Queue(&job{
231 | Message: fmt.Sprintf("處理工作: %d", i+1),
232 | })
233 | }(i)
234 | }
235 |
236 | // 等待所有任務完成
237 | for i := 0; i < taskN; i++ {
238 | fmt.Println("message:", <-rets)
239 | time.Sleep(50 * time.Millisecond)
240 | }
241 |
242 | // 關閉服務並通知所有工作者
243 | q.Release()
244 | }
245 | ```
246 |
247 | ## 使用 NATS 作為隊列
248 |
249 | 請參閱 [NATS 文檔](https://github.com/golang-queue/nats) 以獲取更多詳細信息。
250 |
251 | ```go
252 | package main
253 |
254 | import (
255 | "context"
256 | "encoding/json"
257 | "fmt"
258 | "log"
259 | "time"
260 |
261 | "github.com/golang-queue/nats"
262 | "github.com/golang-queue/queue"
263 | "github.com/golang-queue/queue/core"
264 | )
265 |
266 | type job struct {
267 | Message string
268 | }
269 |
270 | func (j *job) Bytes() []byte {
271 | b, err := json.Marshal(j)
272 | if err != nil {
273 | panic(err)
274 | }
275 | return b
276 | }
277 |
278 | func main() {
279 | taskN := 100
280 | rets := make(chan string, taskN)
281 |
282 | // 定義工作者
283 | w := nats.NewWorker(
284 | nats.WithAddr("127.0.0.1:4222"),
285 | nats.WithSubj("example"),
286 | nats.WithQueue("foobar"),
287 | nats.WithRunFunc(func(ctx context.Context, m core.TaskMessage) error {
288 | var v job
289 | if err := json.Unmarshal(m.Payload(), &v); err != nil {
290 | return err
291 | }
292 |
293 | rets <- v.Message
294 | return nil
295 | }),
296 | )
297 |
298 | // 定義隊列
299 | q, err := queue.NewQueue(
300 | queue.WithWorkerCount(10),
301 | queue.WithWorker(w),
302 | )
303 | if err != nil {
304 | log.Fatal(err)
305 | }
306 |
307 | // 啟動工作者
308 | q.Start()
309 |
310 | // 將任務分配給隊列
311 | for i := 0; i < taskN; i++ {
312 | go func(i int) {
313 | q.Queue(&job{
314 | Message: fmt.Sprintf("處理工作: %d", i+1),
315 | })
316 | }(i)
317 | }
318 |
319 | // 等待所有任務完成
320 | for i := 0; i < taskN; i++ {
321 | fmt.Println("message:", <-rets)
322 | time.Sleep(50 * time.Millisecond)
323 | }
324 |
325 | // 關閉服務並通知所有工作者
326 | q.Release()
327 | }
328 | ```
329 |
330 | ## 使用 Redis (Pub/Sub) 作為隊列
331 |
332 | 請參閱 [Redis 文檔](https://github.com/golang-queue/redisdb) 以獲取更多詳細信息。
333 |
334 | ```go
335 | package main
336 |
337 | import (
338 | "context"
339 | "encoding/json"
340 | "fmt"
341 | "log"
342 | "time"
343 |
344 | "github.com/golang-queue/queue"
345 | "github.com/golang-queue/queue/core"
346 | "github.com/golang-queue/redisdb"
347 | )
348 |
349 | type job struct {
350 | Message string
351 | }
352 |
353 | func (j *job) Bytes() []byte {
354 | b, err := json.Marshal(j)
355 | if err != nil {
356 | panic(err)
357 | }
358 | return b
359 | }
360 |
361 | func main() {
362 | taskN := 100
363 | rets := make(chan string, taskN)
364 |
365 | // 定義工作者
366 | w := redisdb.NewWorker(
367 | redisdb.WithAddr("127.0.0.1:6379"),
368 | redisdb.WithChannel("foobar"),
369 | redisdb.WithRunFunc(func(ctx context.Context, m core.TaskMessage) error {
370 | var v job
371 | if err := json.Unmarshal(m.Payload(), &v); err != nil {
372 | return err
373 | }
374 |
375 | rets <- v.Message
376 | return nil
377 | }),
378 | )
379 |
380 | // 定義隊列
381 | q, err := queue.NewQueue(
382 | queue.WithWorkerCount(10),
383 | queue.WithWorker(w),
384 | )
385 | if err != nil {
386 | log.Fatal(err)
387 | }
388 |
389 | // 啟動工作者
390 | q.Start()
391 |
392 | // 將任務分配給隊列
393 | for i := 0; i < taskN; i++ {
394 | go func(i int) {
395 | q.Queue(&job{
396 | Message: fmt.Sprintf("處理工作: %d", i+1),
397 | })
398 | }(i)
399 | }
400 |
401 | // 等待所有任務完成
402 | for i := 0; i < taskN; i++ {
403 | fmt.Println("message:", <-rets)
404 | time.Sleep(50 * time.Millisecond)
405 | }
406 |
407 | // 關閉服務並通知所有工作者
408 | q.Release()
409 | }
410 | ```
411 |
--------------------------------------------------------------------------------
/_example/example01/go.mod:
--------------------------------------------------------------------------------
1 | module example
2 |
3 | go 1.22
4 |
5 | require (
6 | github.com/golang-queue/contrib v0.0.1
7 | github.com/golang-queue/queue v0.2.0
8 | )
9 |
10 | require (
11 | github.com/jpillora/backoff v1.0.0 // indirect
12 | github.com/rs/zerolog v1.26.1 // indirect
13 | )
14 |
15 | replace github.com/golang-queue/queue => ../../
16 |
--------------------------------------------------------------------------------
/_example/example01/go.sum:
--------------------------------------------------------------------------------
1 | github.com/appleboy/com v0.2.1 h1:dHAHauX3eYDuheAahI83HIGFxpi0SEb2ZAu9EZ9hbUM=
2 | github.com/appleboy/com v0.2.1/go.mod h1:kByEI3/vzI5GM1+O5QdBHLsXaOsmFsJcOpCSgASi4sg=
3 | github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
4 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
5 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
6 | github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
7 | github.com/golang-queue/contrib v0.0.1 h1:Jd61HHridQ8ReJ04kgmF3GYCr/oxIUNeJFoDU6Iuxtc=
8 | github.com/golang-queue/contrib v0.0.1/go.mod h1:Vmreu9zyumpRg7ew516NIP3HUUikaAUBAVq35ujulQA=
9 | github.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA=
10 | github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
11 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
12 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
13 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
14 | github.com/rs/xid v1.3.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
15 | github.com/rs/zerolog v1.26.1 h1:/ihwxqH+4z8UxyI70wM1z9yCvkWcfz/a3mj48k/Zngc=
16 | github.com/rs/zerolog v1.26.1/go.mod h1:/wSSJWX7lVrsOwlbyTRSOJvqRlc+WjWlfes+CiJ+tmc=
17 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
18 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
19 | github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
20 | go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
21 | go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
22 | go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU=
23 | go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM=
24 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
25 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
26 | golang.org/x/crypto v0.0.0-20211215165025-cf75a172585e/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
27 | golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
28 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
29 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
30 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
31 | golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
32 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
33 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
34 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
35 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
36 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
37 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
38 | golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
39 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
40 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
41 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
42 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
43 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
44 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
45 | golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo=
46 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
47 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
48 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
49 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
50 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
51 |
--------------------------------------------------------------------------------
/_example/example01/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | "time"
7 |
8 | "github.com/golang-queue/contrib/zerolog"
9 | "github.com/golang-queue/queue"
10 | )
11 |
12 | func main() {
13 | taskN := 100
14 | rets := make(chan string, taskN)
15 |
16 | // initial queue pool
17 | q := queue.NewPool(5, queue.WithLogger(zerolog.New()))
18 | // shutdown the service and notify all the worker
19 | // wait all jobs done.
20 | defer q.Release()
21 |
22 | // assign tasks in queue
23 | for i := 0; i < taskN; i++ {
24 | go func(i int) {
25 | if err := q.QueueTask(func(ctx context.Context) error {
26 | rets <- fmt.Sprintf("Hi Gopher, handle the job: %02d", +i)
27 | return nil
28 | }); err != nil {
29 | panic(err)
30 | }
31 | }(i)
32 | }
33 |
34 | // wait until all tasks done
35 | for i := 0; i < taskN; i++ {
36 | fmt.Println("message:", <-rets)
37 | time.Sleep(20 * time.Millisecond)
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/_example/example02/go.mod:
--------------------------------------------------------------------------------
1 | module example
2 |
3 | go 1.22
4 |
5 | require (
6 | github.com/golang-queue/contrib v1.1.0
7 | github.com/golang-queue/queue v0.2.0
8 | )
9 |
10 | require (
11 | github.com/jpillora/backoff v1.0.0 // indirect
12 | github.com/mattn/go-colorable v0.1.14 // indirect
13 | github.com/mattn/go-isatty v0.0.20 // indirect
14 | github.com/rs/zerolog v1.33.0 // indirect
15 | golang.org/x/sys v0.29.0 // indirect
16 | )
17 |
18 | replace github.com/golang-queue/queue => ../../
19 |
--------------------------------------------------------------------------------
/_example/example02/go.sum:
--------------------------------------------------------------------------------
1 | github.com/appleboy/com v0.2.1 h1:dHAHauX3eYDuheAahI83HIGFxpi0SEb2ZAu9EZ9hbUM=
2 | github.com/appleboy/com v0.2.1/go.mod h1:kByEI3/vzI5GM1+O5QdBHLsXaOsmFsJcOpCSgASi4sg=
3 | github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
4 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
5 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
6 | github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
7 | github.com/golang-queue/contrib v1.1.0 h1:1vydG/z/CKCg84BPNYAqru5sKxmlUv3ZlbUbRI+Hp+4=
8 | github.com/golang-queue/contrib v1.1.0/go.mod h1:bKxkQxxIEOeTperG3l2kgeLmVLLodF5iOrNING5wleo=
9 | github.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA=
10 | github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
11 | github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
12 | github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
13 | github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
14 | github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
15 | github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
16 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
17 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
18 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
19 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
20 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
21 | github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
22 | github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8=
23 | github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
24 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
25 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
26 | go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
27 | go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
28 | go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU=
29 | go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM=
30 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
31 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
32 | golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
33 | golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
34 | golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
35 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
36 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
37 |
--------------------------------------------------------------------------------
/_example/example02/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "context"
5 | "encoding/json"
6 | "fmt"
7 | "log"
8 | "time"
9 |
10 | "github.com/golang-queue/contrib/zerolog"
11 | "github.com/golang-queue/queue"
12 | "github.com/golang-queue/queue/core"
13 | )
14 |
15 | type job struct {
16 | Name string
17 | Message string
18 | }
19 |
20 | func (j *job) Bytes() []byte {
21 | b, err := json.Marshal(j)
22 | if err != nil {
23 | panic(err)
24 | }
25 | return b
26 | }
27 |
28 | func main() {
29 | taskN := 100
30 | rets := make(chan string, taskN)
31 |
32 | // initial queue pool
33 | q := queue.NewPool(5, queue.WithFn(func(ctx context.Context, m core.TaskMessage) error {
34 | var v job
35 | if err := json.Unmarshal(m.Payload(), &v); err != nil {
36 | return err
37 | }
38 | rets <- "Hi, " + v.Name + ", " + v.Message
39 | return nil
40 | }), queue.WithLogger(zerolog.New()))
41 | // shutdown the service and notify all the worker
42 | // wait all jobs done.
43 | defer q.Release()
44 |
45 | // assign tasks in queue
46 | for i := 0; i < taskN; i++ {
47 | go func(i int) {
48 | if err := q.Queue(&job{
49 | Name: "Gopher",
50 | Message: fmt.Sprintf("handle the job: %d", i+1),
51 | }); err != nil {
52 | log.Println(err)
53 | }
54 | }(i)
55 | }
56 |
57 | // wait until all tasks done
58 | for i := 0; i < taskN; i++ {
59 | fmt.Println("message:", <-rets)
60 | time.Sleep(20 * time.Millisecond)
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/bearer.yml:
--------------------------------------------------------------------------------
1 | rule:
2 | # Disable all default rules by setting this value to true.
3 | disable-default-rules: false
4 | # Specify the comma-separated ids of the rules you would like to run;
5 | # skips all other rules.
6 | only-rule: []
7 | # Specify the comma-separated ids of the rules you would like to skip;
8 | # runs all other rules.
9 | skip-rule: ["go_gosec_unsafe_unsafe", "go_gosec_filesystem_filereadtaint"]
10 |
--------------------------------------------------------------------------------
/benchmark_test.go:
--------------------------------------------------------------------------------
1 | package queue
2 |
3 | import (
4 | "context"
5 | "testing"
6 | "time"
7 |
8 | "github.com/golang-queue/queue/core"
9 | "github.com/golang-queue/queue/job"
10 | )
11 |
12 | var count = 1
13 |
14 | type testqueue interface {
15 | Queue(task core.TaskMessage) error
16 | Request() (core.TaskMessage, error)
17 | }
18 |
19 | func testQueue(b *testing.B, pool testqueue) {
20 | message := job.NewTask(func(context.Context) error {
21 | return nil
22 | },
23 | job.AllowOption{
24 | RetryCount: job.Int64(100),
25 | RetryDelay: job.Time(30 * time.Millisecond),
26 | Timeout: job.Time(3 * time.Millisecond),
27 | },
28 | )
29 |
30 | b.ReportAllocs()
31 | b.ResetTimer()
32 | for n := 0; n < b.N; n++ {
33 | for i := 0; i < count; i++ {
34 | _ = pool.Queue(&message)
35 | _, _ = pool.Request()
36 | }
37 | }
38 | }
39 |
40 | func BenchmarkNewRing(b *testing.B) {
41 | pool := NewRing(
42 | WithQueueSize(b.N*count),
43 | WithLogger(emptyLogger{}),
44 | )
45 |
46 | testQueue(b, pool)
47 | }
48 |
49 | func BenchmarkQueueTask(b *testing.B) {
50 | w := NewRing()
51 | q, _ := NewQueue(
52 | WithWorker(w),
53 | WithLogger(emptyLogger{}),
54 | )
55 | b.ReportAllocs()
56 | b.ResetTimer()
57 |
58 | m := job.NewTask(func(context.Context) error {
59 | return nil
60 | })
61 |
62 | for n := 0; n < b.N; n++ {
63 | if err := q.queue(&m); err != nil {
64 | b.Fatal(err)
65 | }
66 | }
67 | }
68 |
69 | func BenchmarkQueue(b *testing.B) {
70 | w := NewRing()
71 | q, _ := NewQueue(
72 | WithWorker(w),
73 | WithLogger(emptyLogger{}),
74 | )
75 | b.ReportAllocs()
76 | b.ResetTimer()
77 |
78 | m := job.NewMessage(&mockMessage{
79 | message: "foo",
80 | })
81 |
82 | for n := 0; n < b.N; n++ {
83 | if err := q.queue(&m); err != nil {
84 | b.Fatal(err)
85 | }
86 | }
87 | }
88 |
89 | // func BenchmarkRingPayload(b *testing.B) {
90 | // b.ReportAllocs()
91 |
92 | // task := &job.Message{
93 | // Timeout: 100 * time.Millisecond,
94 | // Payload: []byte(`{"timeout":3600000000000}`),
95 | // }
96 | // w := NewRing(
97 | // WithFn(func(ctx context.Context, m core.TaskMessage) error {
98 | // return nil
99 | // }),
100 | // )
101 |
102 | // q, _ := NewQueue(
103 | // WithWorker(w),
104 | // WithLogger(emptyLogger{}),
105 | // )
106 |
107 | // for n := 0; n < b.N; n++ {
108 | // _ = q.run(task)
109 | // }
110 | // }
111 |
112 | func BenchmarkRingWithTask(b *testing.B) {
113 | b.ReportAllocs()
114 |
115 | task := job.Message{
116 | Timeout: 100 * time.Millisecond,
117 | Task: func(_ context.Context) error {
118 | return nil
119 | },
120 | }
121 | w := NewRing(
122 | WithFn(func(ctx context.Context, m core.TaskMessage) error {
123 | return nil
124 | }),
125 | )
126 |
127 | q, _ := NewQueue(
128 | WithWorker(w),
129 | WithLogger(emptyLogger{}),
130 | )
131 |
132 | for n := 0; n < b.N; n++ {
133 | _ = q.run(&task)
134 | }
135 | }
136 |
--------------------------------------------------------------------------------
/core/worker.go:
--------------------------------------------------------------------------------
1 | package core
2 |
3 | import (
4 | "context"
5 | )
6 |
7 | // Worker represents an interface for a worker that processes tasks.
8 | // It provides methods to run tasks, shut down the worker, queue tasks, and request tasks from the queue.
9 | type Worker interface {
10 | // Run starts the worker and processes the given task in the provided context.
11 | // It returns an error if the task cannot be processed.
12 | Run(ctx context.Context, task TaskMessage) error
13 |
14 | // Shutdown stops the worker and performs any necessary cleanup.
15 | // It returns an error if the shutdown process fails.
16 | Shutdown() error
17 |
18 | // Queue adds a task to the worker's queue.
19 | // It returns an error if the task cannot be added to the queue.
20 | Queue(task TaskMessage) error
21 |
22 | // Request retrieves a task from the worker's queue.
23 | // It returns the queued message and an error if the retrieval fails.
24 | Request() (TaskMessage, error)
25 | }
26 |
27 | // QueuedMessage represents an interface for a message that can be queued.
28 | // It requires the implementation of a Bytes method, which returns the message
29 | // content as a slice of bytes.
30 | type QueuedMessage interface {
31 | Bytes() []byte
32 | }
33 |
34 | // TaskMessage represents an interface for a task message that can be queued.
35 | // It embeds the QueuedMessage interface and adds a method to retrieve the payload of the message.
36 | type TaskMessage interface {
37 | QueuedMessage
38 | Payload() []byte
39 | }
40 |
--------------------------------------------------------------------------------
/errors.go:
--------------------------------------------------------------------------------
1 | package queue
2 |
3 | import "errors"
4 |
5 | var (
6 | // ErrNoTaskInQueue there is nothing in the queue
7 | ErrNoTaskInQueue = errors.New("golang-queue: no task in queue")
8 | // ErrQueueHasBeenClosed the current queue is closed
9 | ErrQueueHasBeenClosed = errors.New("golang-queue: queue has been closed")
10 | // ErrMaxCapacity Maximum size limit reached
11 | ErrMaxCapacity = errors.New("golang-queue: maximum size limit reached")
12 | )
13 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/golang-queue/queue
2 |
3 | go 1.23
4 |
5 | require (
6 | github.com/appleboy/com v0.3.0
7 | github.com/jpillora/backoff v1.0.0
8 | github.com/stretchr/testify v1.10.0
9 | go.uber.org/goleak v1.3.0
10 | go.uber.org/mock v0.5.2
11 | )
12 |
13 | require (
14 | github.com/davecgh/go-spew v1.1.1 // indirect
15 | github.com/kr/text v0.2.0 // indirect
16 | github.com/pmezard/go-difflib v1.0.0 // indirect
17 | gopkg.in/yaml.v3 v3.0.1 // indirect
18 | )
19 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | github.com/appleboy/com v0.3.0 h1:omze/tJPyi2YVH+m23GSrCGt90A+4vQNpEYBW+GuSr4=
2 | github.com/appleboy/com v0.3.0/go.mod h1:kByEI3/vzI5GM1+O5QdBHLsXaOsmFsJcOpCSgASi4sg=
3 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
4 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
5 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
6 | github.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA=
7 | github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
8 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
9 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
10 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
11 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
12 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
13 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
14 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
15 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
16 | go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
17 | go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
18 | go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko=
19 | go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o=
20 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
21 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
22 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
23 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
24 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
25 |
--------------------------------------------------------------------------------
/images/_base.d2:
--------------------------------------------------------------------------------
1 | classes: {
2 | base: {
3 | style: {
4 | bold: true
5 | font-size: 28
6 | }
7 | }
8 |
9 | person: {
10 | shape: person
11 | }
12 |
13 | animated: {
14 | style: {
15 | animated: true
16 | }
17 | }
18 |
19 | multiple: {
20 | style: {
21 | multiple: true
22 | }
23 | }
24 |
25 | enqueue: {
26 | label: Enqueue Task
27 | }
28 |
29 | dispatch: {
30 | label: Dispatch Task
31 | }
32 |
33 | library: {
34 | style: {
35 | bold: true
36 | font-size: 32
37 | fill: PapayaWhip
38 | fill-pattern: grain
39 | border-radius: 8
40 | font: mono
41 | }
42 | }
43 |
44 | task: {
45 | style: {
46 | bold: true
47 | font-size: 32
48 | }
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/images/flow-01.d2:
--------------------------------------------------------------------------------
1 | direction: right
2 |
3 | ...@_base
4 |
5 | user01: {
6 | label: User01
7 | class: [base; person; multiple]
8 | }
9 |
10 | user02: {
11 | label: User02
12 | class: [base; person; multiple]
13 | }
14 |
15 | user03: {
16 | label: User03
17 | class: [base; person; multiple]
18 | }
19 |
20 | user01 -> container.task01: {
21 | label: Create Task
22 | class: [base; animated]
23 | }
24 | user02 -> container.task02: {
25 | label: Create Task
26 | class: [base; animated]
27 | }
28 | user03 -> container.task03: {
29 | label: Create Task
30 | class: [base; animated]
31 | }
32 |
33 | container: Application {
34 | direction: right
35 | style: {
36 | bold: true
37 | font-size: 28
38 | }
39 | icon: https://icons.terrastruct.com/dev%2Fgo.svg
40 |
41 | task01: {
42 | icon: https://icons.terrastruct.com/essentials%2F092-graph%20bar.svg
43 | class: [task; multiple]
44 | }
45 |
46 | task02: {
47 | icon: https://icons.terrastruct.com/essentials%2F095-download.svg
48 | class: [task; multiple]
49 | }
50 |
51 | task03: {
52 | icon: https://icons.terrastruct.com/essentials%2F195-attachment.svg
53 | class: [task; multiple]
54 | }
55 |
56 | queue: {
57 | label: Queue Library
58 | icon: https://icons.terrastruct.com/dev%2Fgo.svg
59 | style: {
60 | bold: true
61 | font-size: 32
62 | fill: honeydew
63 | }
64 |
65 | producer: {
66 | label: Producer
67 | class: library
68 | }
69 |
70 | consumer: {
71 | label: Consumer
72 | class: library
73 | }
74 |
75 | database: {
76 | label: Ring\nBuffer
77 | shape: cylinder
78 | style: {
79 | bold: true
80 | font-size: 32
81 | fill-pattern: lines
82 | font: mono
83 | }
84 | }
85 |
86 | producer -> database
87 | database -> consumer
88 | }
89 |
90 | worker01: {
91 | icon: https://icons.terrastruct.com/essentials%2F092-graph%20bar.svg
92 | class: [task]
93 | }
94 |
95 | worker02: {
96 | icon: https://icons.terrastruct.com/essentials%2F095-download.svg
97 | class: [task]
98 | }
99 |
100 | worker03: {
101 | icon: https://icons.terrastruct.com/essentials%2F092-graph%20bar.svg
102 | class: [task]
103 | }
104 |
105 | worker04: {
106 | icon: https://icons.terrastruct.com/essentials%2F195-attachment.svg
107 | class: [task]
108 | }
109 |
110 | task01 -> queue.producer: {
111 | class: [base; enqueue]
112 | }
113 | task02 -> queue.producer: {
114 | class: [base; enqueue]
115 | }
116 | task03 -> queue.producer: {
117 | class: [base; enqueue]
118 | }
119 | queue.consumer -> worker01: {
120 | class: [base; dispatch]
121 | }
122 | queue.consumer -> worker02: {
123 | class: [base; dispatch]
124 | }
125 | queue.consumer -> worker03: {
126 | class: [base; dispatch]
127 | }
128 | queue.consumer -> worker04: {
129 | class: [base; dispatch]
130 | }
131 | }
132 |
--------------------------------------------------------------------------------
/images/flow-02.d2:
--------------------------------------------------------------------------------
1 | direction: right
2 |
3 | ...@_base
4 |
5 | user01: User01 {
6 | label: User01
7 | class: [base; person; multiple]
8 | }
9 |
10 | user02: User02 {
11 | label: User02
12 | class: [base; person; multiple]
13 | }
14 |
15 | user03: User03 {
16 | label: User03
17 | class: [base; person; multiple]
18 | }
19 |
20 | user04: User04 {
21 | label: User04
22 | class: [base; person; multiple]
23 | }
24 |
25 | user01 -> container.task01: {
26 | label: Create Task
27 | class: [base; animated]
28 | }
29 | user02 -> container.task02: {
30 | label: Create Task
31 | class: [base; animated]
32 | }
33 |
34 | user03 -> container.task03: {
35 | label: Create Task
36 | class: [base; animated]
37 | }
38 |
39 | user04 -> container.task04: {
40 | label: Create Task
41 | class: [base; animated]
42 | }
43 |
44 | container: Application {
45 | direction: right
46 | style: {
47 | bold: true
48 | font-size: 28
49 | }
50 | icon: https://icons.terrastruct.com/dev%2Fgo.svg
51 |
52 | task01: {
53 | icon: https://icons.terrastruct.com/essentials%2F092-graph%20bar.svg
54 | class: [task; multiple]
55 | }
56 |
57 | task02: {
58 | icon: https://icons.terrastruct.com/essentials%2F095-download.svg
59 | class: [task; multiple]
60 | }
61 |
62 | task03: {
63 | icon: https://icons.terrastruct.com/essentials%2F195-attachment.svg
64 | class: [task; multiple]
65 | }
66 |
67 | task04: {
68 | icon: https://icons.terrastruct.com/essentials%2F213-alarm.svg
69 | class: [task; multiple]
70 | }
71 |
72 | queue: Queue Library {
73 | icon: https://icons.terrastruct.com/dev%2Fgo.svg
74 | style: {
75 | bold: true
76 | font-size: 32
77 | fill: honeydew
78 | }
79 | producer01: {
80 | label: Producer01
81 | class: library
82 | }
83 |
84 | producer02: {
85 | label: Producer02
86 | class: library
87 | }
88 |
89 | producer03: {
90 | label: Producer03
91 | class: library
92 | }
93 |
94 | producer04: {
95 | label: Producer04
96 | class: library
97 | }
98 |
99 | consumer01: {
100 | label: Consumer01
101 | class: library
102 | }
103 |
104 | consumer02: {
105 | label: Consumer02
106 | class: library
107 | }
108 |
109 | consumer03: {
110 | label: Consumer03
111 | class: library
112 | }
113 |
114 | consumer04: {
115 | label: Consumer04
116 | class: library
117 | }
118 |
119 | database: Ring\nBuffer {
120 | shape: cylinder
121 | style: {
122 | bold: true
123 | font-size: 32
124 | fill-pattern: lines
125 | font: mono
126 | }
127 | }
128 |
129 | redis: Redis {
130 | icon: https://icons.terrastruct.com/dev%2Fredis.svg
131 | shape: image
132 | style: {
133 | bold: true
134 | font-size: 32
135 | font: mono
136 | }
137 | }
138 |
139 | rabbitmq: RabbotMQ {
140 | icon: ./rabbitmq.svg
141 | shape: image
142 | style: {
143 | bold: true
144 | font-size: 32
145 | font: mono
146 | }
147 | }
148 |
149 | nats: NATS {
150 | icon: ./nats.svg
151 | shape: image
152 | style: {
153 | bold: true
154 | font-size: 32
155 | font: mono
156 | }
157 | }
158 |
159 | producer01 -> database
160 | producer02 -> redis
161 | producer03 -> rabbitmq
162 | producer04 -> nats
163 | database -> consumer01
164 | redis -> consumer02
165 | rabbitmq -> consumer03
166 | nats -> consumer04
167 | }
168 |
169 | worker01: {
170 | icon: https://icons.terrastruct.com/essentials%2F092-graph%20bar.svg
171 | class: [task]
172 | }
173 |
174 | worker02: {
175 | icon: https://icons.terrastruct.com/essentials%2F095-download.svg
176 | class: [task]
177 | }
178 |
179 | worker03: {
180 | icon: https://icons.terrastruct.com/essentials%2F092-graph%20bar.svg
181 | class: [task]
182 | }
183 |
184 | worker04: {
185 | icon: https://icons.terrastruct.com/essentials%2F213-alarm.svg
186 | class: [task]
187 | }
188 |
189 | task01 -> queue.producer01: {
190 | class: [base; enqueue]
191 | }
192 | task02 -> queue.producer02: {
193 | class: [base; enqueue]
194 | }
195 | task03 -> queue.producer03: {
196 | class: [base; enqueue]
197 | }
198 | task04 -> queue.producer04: {
199 | class: [base; enqueue]
200 | }
201 | queue.consumer01 -> worker01: {
202 | class: [base; dispatch]
203 | }
204 | queue.consumer02 -> worker02: {
205 | class: [base; dispatch]
206 | }
207 | queue.consumer03 -> worker03: {
208 | class: [base; dispatch]
209 | }
210 | queue.consumer04 -> worker04: {
211 | class: [base; dispatch]
212 | }
213 | }
214 |
--------------------------------------------------------------------------------
/images/flow-03.d2:
--------------------------------------------------------------------------------
1 | direction: right
2 |
3 | user01: User01 {
4 | shape: person
5 | style: {
6 | multiple: true
7 | bold: true
8 | font-size: 28
9 | }
10 | }
11 |
12 | user02: User02 {
13 | shape: person
14 | style: {
15 | multiple: true
16 | bold: true
17 | font-size: 28
18 | }
19 | }
20 |
21 | user03: User03 {
22 | shape: person
23 | style: {
24 | multiple: true
25 | bold: true
26 | font-size: 28
27 | }
28 | }
29 |
30 | user04: User04 {
31 | shape: person
32 | style: {
33 | multiple: true
34 | bold: true
35 | font-size: 28
36 | }
37 | }
38 |
39 | container_producer_01: Service01 {
40 | direction: right
41 | style: {
42 | bold: true
43 | font-size: 28
44 | }
45 | icon: https://icons.terrastruct.com/dev%2Fgo.svg
46 |
47 | task01: {
48 | icon: https://icons.terrastruct.com/essentials%2F092-graph%20bar.svg
49 | style: {
50 | multiple: true
51 | bold: true
52 | font-size: 32
53 | }
54 | }
55 |
56 | task02: {
57 | icon: https://icons.terrastruct.com/essentials%2F095-download.svg
58 | style: {
59 | multiple: true
60 | bold: true
61 | font-size: 32
62 | }
63 | }
64 |
65 | queue: Queue Library {
66 | icon: https://icons.terrastruct.com/dev%2Fgo.svg
67 | style: {
68 | bold: true
69 | font-size: 28
70 | fill: honeydew
71 | }
72 |
73 | producer: Producer {
74 | style: {
75 | bold: true
76 | font-size: 32
77 | fill: PapayaWhip
78 | fill-pattern: grain
79 | border-radius: 8
80 | font: mono
81 | }
82 | }
83 | }
84 |
85 | task01 -> queue.producer: Enqueue Task {
86 | style: {
87 | animated: true
88 | bold: true
89 | font-size: 28
90 | }
91 | }
92 |
93 | task02 -> queue.producer: Enqueue Task {
94 | style: {
95 | animated: true
96 | bold: true
97 | font-size: 28
98 | }
99 | }
100 | }
101 |
102 | container_producer_02: Service02 {
103 | direction: right
104 | style: {
105 | bold: true
106 | font-size: 28
107 | }
108 | icon: https://icons.terrastruct.com/dev%2Fgo.svg
109 |
110 | task01: {
111 | icon: https://icons.terrastruct.com/essentials%2F195-attachment.svg
112 | style: {
113 | multiple: true
114 | bold: true
115 | font-size: 32
116 | }
117 | }
118 |
119 | task02: {
120 | icon: https://icons.terrastruct.com/essentials%2F213-alarm.svg
121 | style: {
122 | multiple: true
123 | bold: true
124 | font-size: 32
125 | }
126 | }
127 |
128 | queue: Queue Library {
129 | icon: https://icons.terrastruct.com/dev%2Fgo.svg
130 | style: {
131 | bold: true
132 | font-size: 28
133 | fill: honeydew
134 | }
135 |
136 | producer: Producer {
137 | style: {
138 | bold: true
139 | font-size: 32
140 | fill: PapayaWhip
141 | fill-pattern: grain
142 | border-radius: 8
143 | font: mono
144 | }
145 | }
146 | }
147 |
148 | task01 -> queue.producer: Enqueue Task {
149 | style: {
150 | animated: true
151 | bold: true
152 | font-size: 28
153 | }
154 | }
155 |
156 | task02 -> queue.producer: Enqueue Task {
157 | style: {
158 | animated: true
159 | bold: true
160 | font-size: 28
161 | }
162 | }
163 | }
164 |
165 | database: Ring\nBuffer {
166 | shape: cylinder
167 | style: {
168 | bold: true
169 | font-size: 32
170 | fill-pattern: lines
171 | font: mono
172 | }
173 | }
174 |
175 | other_store: "" {
176 | style: {
177 | opacity: 0
178 | }
179 |
180 | redis: Redis {
181 | icon: https://icons.terrastruct.com/dev%2Fredis.svg
182 | shape: image
183 | style: {
184 | bold: true
185 | font-size: 32
186 | font: mono
187 | opacity: 0.7
188 | }
189 | }
190 |
191 | rabbitmq: RabbotMQ {
192 | icon: ./images/rabbitmq.svg
193 | shape: image
194 | style: {
195 | bold: true
196 | font-size: 32
197 | font: mono
198 | }
199 | }
200 |
201 | nats: NATS {
202 | icon: ./images/nats.svg
203 | shape: image
204 | style: {
205 | bold: true
206 | font-size: 32
207 | font: mono
208 | opacity: 0.7
209 | }
210 | }
211 | }
212 |
213 | container_consumer_01: Worker01 {
214 | direction: right
215 | style: {
216 | bold: true
217 | font-size: 28
218 | }
219 | icon: https://icons.terrastruct.com/dev%2Fgo.svg
220 |
221 | task01: {
222 | icon: https://icons.terrastruct.com/essentials%2F092-graph%20bar.svg
223 | style: {
224 | multiple: true
225 | bold: true
226 | font-size: 32
227 | }
228 | }
229 |
230 | task02: {
231 | icon: https://icons.terrastruct.com/essentials%2F095-download.svg
232 | style: {
233 | multiple: true
234 | bold: true
235 | font-size: 32
236 | }
237 | }
238 |
239 | queue: Queue Library {
240 | icon: https://icons.terrastruct.com/dev%2Fgo.svg
241 | style: {
242 | bold: true
243 | font-size: 28
244 | fill: honeydew
245 | }
246 |
247 | consumer: Consumer {
248 | style: {
249 | bold: true
250 | font-size: 32
251 | fill: PapayaWhip
252 | fill-pattern: grain
253 | border-radius: 8
254 | font: mono
255 | }
256 | }
257 | }
258 |
259 | queue.consumer -> task01: Handle Task {
260 | style: {
261 | animated: true
262 | bold: true
263 | font-size: 28
264 | }
265 | }
266 |
267 | queue.consumer -> task02: Handle Task {
268 | style: {
269 | bold: true
270 | font-size: 28
271 | }
272 | }
273 | }
274 |
275 | container_consumer_02: Worker02 {
276 | direction: right
277 | style: {
278 | bold: true
279 | font-size: 28
280 | }
281 | icon: https://icons.terrastruct.com/dev%2Fgo.svg
282 |
283 | task01: {
284 | icon: https://icons.terrastruct.com/essentials%2F095-download.svg
285 | style: {
286 | multiple: true
287 | bold: true
288 | font-size: 32
289 | }
290 | }
291 |
292 | task02: {
293 | icon: https://icons.terrastruct.com/essentials%2F092-graph%20bar.svg
294 | style: {
295 | multiple: true
296 | bold: true
297 | font-size: 32
298 | }
299 | }
300 |
301 | task03: {
302 | icon: https://icons.terrastruct.com/essentials%2F195-attachment.svg
303 | style: {
304 | multiple: true
305 | bold: true
306 | font-size: 32
307 | }
308 | }
309 |
310 | task04: {
311 | icon: https://icons.terrastruct.com/essentials%2F213-alarm.svg
312 | style: {
313 | multiple: true
314 | bold: true
315 | font-size: 32
316 | }
317 | }
318 |
319 | queue01: Queue Library {
320 | icon: https://icons.terrastruct.com/dev%2Fgo.svg
321 | style: {
322 | bold: true
323 | font-size: 28
324 | fill: honeydew
325 | }
326 |
327 | consumer: Consumer {
328 | style: {
329 | bold: true
330 | font-size: 32
331 | fill: PapayaWhip
332 | fill-pattern: grain
333 | border-radius: 8
334 | font: mono
335 | }
336 | }
337 | }
338 |
339 | queue02: Queue Library {
340 | icon: https://icons.terrastruct.com/dev%2Fgo.svg
341 | style: {
342 | bold: true
343 | font-size: 28
344 | fill: honeydew
345 | }
346 |
347 | consumer: Consumer {
348 | style: {
349 | bold: true
350 | font-size: 32
351 | fill: PapayaWhip
352 | fill-pattern: grain
353 | border-radius: 8
354 | font: mono
355 | }
356 | }
357 | }
358 |
359 | queue01.consumer -> task01: Handle Task {
360 | style: {
361 | animated: true
362 | bold: true
363 | font-size: 28
364 | }
365 | }
366 |
367 | queue01.consumer -> task02: Handle Task {
368 | style: {
369 | animated: true
370 | bold: true
371 | font-size: 28
372 | }
373 | }
374 |
375 | queue02.consumer -> task03: Handle Task {
376 | style: {
377 | animated: true
378 | bold: true
379 | font-size: 28
380 | }
381 | }
382 |
383 | queue02.consumer -> task04: Handle Task {
384 | style: {
385 | animated: true
386 | bold: true
387 | font-size: 28
388 | }
389 | }
390 | }
391 |
392 | # connection
393 |
394 | user01 -> container_producer_01.task01: Create Task {
395 | style: {
396 | animated: true
397 | bold: true
398 | font-size: 28
399 | }
400 | }
401 |
402 | user02 -> container_producer_01.task02: Create Task {
403 | style: {
404 | animated: true
405 | bold: true
406 | font-size: 28
407 | }
408 | }
409 |
410 | user03 -> container_producer_02.task01: Create Task {
411 | style: {
412 | animated: true
413 | bold: true
414 | font-size: 28
415 | }
416 | }
417 |
418 | user04 -> container_producer_02.task02: Create Task {
419 | style: {
420 | animated: true
421 | bold: true
422 | font-size: 28
423 | }
424 | }
425 |
426 | container_producer_01.queue.producer -> database: Store Task {
427 | style: {
428 | animated: true
429 | bold: true
430 | font-size: 28
431 | }
432 | }
433 |
434 | container_producer_02.queue.producer -> other_store.rabbitmq: Store Task {
435 | style: {
436 | animated: true
437 | bold: true
438 | font-size: 28
439 | }
440 | }
441 |
442 | container_producer_02.queue.producer -> other_store.redis: "" {
443 | style: {
444 | bold: true
445 | opacity: 0.3
446 | }
447 | }
448 |
449 | container_producer_02.queue.producer -> other_store.nats: "" {
450 | style: {
451 | bold: true
452 | opacity: 0.3
453 | }
454 | }
455 |
456 | database -> container_consumer_01.queue.consumer: Fetch Task {
457 | style: {
458 | animated: true
459 | bold: true
460 | font-size: 28
461 | }
462 | }
463 |
464 | database -> container_consumer_02.queue01.consumer: Fetch Task {
465 | style: {
466 | animated: true
467 | bold: true
468 | font-size: 28
469 | }
470 | }
471 |
472 | other_store.rabbitmq -> container_consumer_02.queue02.consumer: Fetch Task {
473 | style: {
474 | animated: true
475 | bold: true
476 | font-size: 28
477 | }
478 | }
479 |
480 | other_store.redis -> container_consumer_02.queue02.consumer: "" {
481 | style: {
482 | bold: true
483 | font-size: 28
484 | opacity: 0.3
485 | }
486 | }
487 |
488 | other_store.nats -> container_consumer_02.queue02.consumer: "" {
489 | style: {
490 | bold: true
491 | font-size: 28
492 | opacity: 0.3
493 | }
494 | }
495 |
--------------------------------------------------------------------------------
/images/nats.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/images/rabbitmq.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/job/benchmark_test.go:
--------------------------------------------------------------------------------
1 | package job
2 |
3 | import (
4 | "context"
5 | "testing"
6 | "time"
7 | )
8 |
9 | func BenchmarkNewTask(b *testing.B) {
10 | b.ReportAllocs()
11 | for i := 0; i < b.N; i++ {
12 | NewTask(func(context.Context) error {
13 | return nil
14 | },
15 | AllowOption{
16 | RetryCount: Int64(100),
17 | RetryDelay: Time(30 * time.Millisecond),
18 | Timeout: Time(3 * time.Millisecond),
19 | },
20 | )
21 | }
22 | }
23 |
24 | func BenchmarkNewMessage(b *testing.B) {
25 | b.ReportAllocs()
26 | for i := 0; i < b.N; i++ {
27 | _ = NewMessage(mockMessage{
28 | message: "foo",
29 | },
30 | AllowOption{
31 | RetryCount: Int64(100),
32 | RetryDelay: Time(30 * time.Millisecond),
33 | Timeout: Time(3 * time.Millisecond),
34 | },
35 | )
36 | }
37 | }
38 |
39 | func BenchmarkNewOption(b *testing.B) {
40 | b.ReportAllocs()
41 | for i := 0; i < b.N; i++ {
42 | _ = NewOptions(
43 | AllowOption{
44 | RetryCount: Int64(100),
45 | RetryDelay: Time(30 * time.Millisecond),
46 | Timeout: Time(3 * time.Millisecond),
47 | },
48 | )
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/job/job.go:
--------------------------------------------------------------------------------
1 | package job
2 |
3 | import (
4 | "context"
5 | "encoding/json"
6 | "time"
7 |
8 | "github.com/golang-queue/queue/core"
9 | )
10 |
11 | // TaskFunc is the task function
12 | type TaskFunc func(context.Context) error
13 |
14 | // Message describes a task and its metadata.
15 | type Message struct {
16 | Task TaskFunc `json:"-" msgpack:"-"`
17 |
18 | // Timeout is the duration the task can be processed by Handler.
19 | // zero if not specified
20 | // default is 60 time.Minute
21 | Timeout time.Duration `json:"timeout" msgpack:"timeout"`
22 |
23 | // Payload is the payload data of the task.
24 | Body []byte `json:"body" msgpack:"body"`
25 |
26 | // RetryCount set count of retry
27 | // default is 0, no retry.
28 | RetryCount int64 `json:"retry_count" msgpack:"retry_count"`
29 |
30 | // RetryDelay set delay between retry
31 | // default is 100ms
32 | RetryDelay time.Duration `json:"retry_delay" msgpack:"retry_delay"`
33 |
34 | // RetryFactor is the multiplying factor for each increment step.
35 | //
36 | // Defaults to 2.
37 | RetryFactor float64 `json:"retry_factor" msgpack:"retry_factor"`
38 |
39 | // Minimum value of the counter.
40 | //
41 | // Defaults to 100 milliseconds.
42 | RetryMin time.Duration `json:"retry_min" msgpack:"retry_min"`
43 |
44 | // Maximum value of the counter.
45 | //
46 | // Defaults to 10 seconds.
47 | RetryMax time.Duration `json:"retry_max" msgpack:"retry_max"`
48 |
49 | // Jitter eases contention by randomizing backoff steps
50 | Jitter bool `json:"jitter" msgpack:"jitter"`
51 | }
52 |
53 | // Payload returns the payload data of the Message.
54 | // It returns the byte slice of the payload.
55 | //
56 | // Returns:
57 | // - A byte slice containing the payload data.
58 | func (m *Message) Payload() []byte {
59 | return m.Body
60 | }
61 |
62 | // Bytes returns the byte slice of the Message struct.
63 | // If the marshalling process encounters an error, the function will panic.
64 | // It returns the marshalled byte slice.
65 | //
66 | // Returns:
67 | // - A byte slice containing the msgpack-encoded data.
68 | func (m *Message) Bytes() []byte {
69 | b, err := json.Marshal(m)
70 | if err != nil {
71 | panic(err)
72 | }
73 |
74 | return b
75 | }
76 |
77 | // NewMessage create new message
78 | func NewMessage(m core.QueuedMessage, opts ...AllowOption) Message {
79 | o := NewOptions(opts...)
80 |
81 | return Message{
82 | RetryCount: o.retryCount,
83 | RetryDelay: o.retryDelay,
84 | RetryFactor: o.retryFactor,
85 | RetryMin: o.retryMin,
86 | RetryMax: o.retryMax,
87 | Timeout: o.timeout,
88 | Body: m.Bytes(),
89 | }
90 | }
91 |
92 | func NewTask(task TaskFunc, opts ...AllowOption) Message {
93 | o := NewOptions(opts...)
94 |
95 | return Message{
96 | Timeout: o.timeout,
97 | RetryCount: o.retryCount,
98 | RetryDelay: o.retryDelay,
99 | RetryFactor: o.retryFactor,
100 | RetryMin: o.retryMin,
101 | RetryMax: o.retryMax,
102 | Task: task,
103 | }
104 | }
105 |
106 | // Encode takes a Message struct and marshals it into a byte slice using msgpack.
107 | // If the marshalling process encounters an error, the function will panic.
108 | // It returns the marshalled byte slice.
109 | //
110 | // Parameters:
111 | // - m: A pointer to the Message struct to be encoded.
112 | //
113 | // Returns:
114 | // - A byte slice containing the msgpack-encoded data.
115 | func Encode(m *Message) []byte {
116 | b, err := json.Marshal(m)
117 | if err != nil {
118 | panic(err)
119 | }
120 |
121 | return b
122 | }
123 |
124 | // Decode takes a byte slice and unmarshals it into a Message struct using msgpack.
125 | // If the unmarshalling process encounters an error, the function will panic.
126 | // It returns a pointer to the unmarshalled Message.
127 | //
128 | // Parameters:
129 | // - b: A byte slice containing the msgpack-encoded data.
130 | //
131 | // Returns:
132 | // - A pointer to the decoded Message struct.
133 | func Decode(b []byte) *Message {
134 | var msg Message
135 | err := json.Unmarshal(b, &msg)
136 | if err != nil {
137 | panic(err)
138 | }
139 |
140 | return &msg
141 | }
142 |
--------------------------------------------------------------------------------
/job/job_test.go:
--------------------------------------------------------------------------------
1 | package job
2 |
3 | import (
4 | "testing"
5 | "time"
6 |
7 | "github.com/appleboy/com/bytesconv"
8 | "github.com/stretchr/testify/assert"
9 | )
10 |
11 | type mockMessage struct {
12 | message string
13 | }
14 |
15 | func (m mockMessage) Bytes() []byte {
16 | return bytesconv.StrToBytes(m.message)
17 | }
18 |
19 | func TestMessageEncodeDecode(t *testing.T) {
20 | m := NewMessage(&mockMessage{
21 | message: "foo",
22 | },
23 | AllowOption{
24 | RetryCount: Int64(100),
25 | RetryDelay: Time(30 * time.Millisecond),
26 | Timeout: Time(3 * time.Millisecond),
27 | RetryMin: Time(200 * time.Millisecond),
28 | RetryMax: Time(20 * time.Second),
29 | RetryFactor: Float64(4.0),
30 | },
31 | )
32 |
33 | out := Decode(m.Bytes())
34 |
35 | assert.Equal(t, int64(100), out.RetryCount)
36 | assert.Equal(t, 30*time.Millisecond, out.RetryDelay)
37 | assert.Equal(t, 3*time.Millisecond, out.Timeout)
38 | assert.Equal(t, "foo", string(out.Payload()))
39 | assert.Equal(t, 200*time.Millisecond, out.RetryMin)
40 | assert.Equal(t, 20*time.Second, out.RetryMax)
41 | assert.Equal(t, 4.0, out.RetryFactor)
42 | }
43 |
--------------------------------------------------------------------------------
/job/option.go:
--------------------------------------------------------------------------------
1 | package job
2 |
3 | import "time"
4 |
5 | // Options is a set of options for the queue
6 | type Options struct {
7 | retryCount int64
8 | retryDelay time.Duration
9 | retryFactor float64
10 | retryMin time.Duration
11 | retryMax time.Duration
12 | jitter bool
13 |
14 | timeout time.Duration
15 | }
16 |
17 | // newDefaultOptions create new default options
18 | func newDefaultOptions() Options {
19 | return Options{
20 | retryCount: 0,
21 | retryDelay: 0,
22 | retryFactor: 2,
23 | retryMin: 100 * time.Millisecond,
24 | retryMax: 10 * time.Second,
25 | timeout: 60 * time.Minute,
26 | jitter: false,
27 | }
28 | }
29 |
30 | // AllowOption is a function that sets some option on the Options
31 | type AllowOption struct {
32 | RetryCount *int64
33 | RetryDelay *time.Duration
34 | RetryFactor *float64
35 | RetryMin *time.Duration
36 | RetryMax *time.Duration
37 | Jitter *bool
38 | Timeout *time.Duration
39 | }
40 |
41 | // NewOptions create new options
42 | func NewOptions(opts ...AllowOption) Options {
43 | o := newDefaultOptions()
44 |
45 | if len(opts) != 0 {
46 | if opts[0].RetryCount != nil && *opts[0].RetryCount != o.retryCount {
47 | o.retryCount = *opts[0].RetryCount
48 | }
49 |
50 | if opts[0].RetryDelay != nil && *opts[0].RetryDelay != o.retryDelay {
51 | o.retryDelay = *opts[0].RetryDelay
52 | }
53 |
54 | if opts[0].Timeout != nil && *opts[0].Timeout != o.timeout {
55 | o.timeout = *opts[0].Timeout
56 | }
57 |
58 | if opts[0].RetryFactor != nil && *opts[0].RetryFactor != o.retryFactor {
59 | o.retryFactor = *opts[0].RetryFactor
60 | }
61 |
62 | if opts[0].RetryMin != nil && *opts[0].RetryMin != o.retryMin {
63 | o.retryMin = *opts[0].RetryMin
64 | }
65 |
66 | if opts[0].RetryMax != nil && *opts[0].RetryMax != o.retryMax {
67 | o.retryMax = *opts[0].RetryMax
68 | }
69 |
70 | if opts[0].Jitter != nil && *opts[0].Jitter != o.jitter {
71 | o.jitter = *opts[0].Jitter
72 | }
73 | }
74 |
75 | return o
76 | }
77 |
78 | // Int64 is a helper routine that allocates a new int64 value
79 | func Int64(val int64) *int64 {
80 | return &val
81 | }
82 |
83 | // Float64 is a helper routine that allocates a new float64 value
84 | func Float64(val float64) *float64 {
85 | return &val
86 | }
87 |
88 | // Time is a helper routine that allocates a new time value
89 | func Time(v time.Duration) *time.Duration {
90 | return &v
91 | }
92 |
93 | // Bool is a helper routine that allocates a new bool value
94 | func Bool(val bool) *bool {
95 | return &val
96 | }
97 |
--------------------------------------------------------------------------------
/job/option_test.go:
--------------------------------------------------------------------------------
1 | package job
2 |
3 | import (
4 | "testing"
5 | "time"
6 |
7 | "github.com/stretchr/testify/assert"
8 | )
9 |
10 | // TestMessageEncodeDecode test message encode and decode
11 | func TestOptions(t *testing.T) {
12 | o := NewOptions(
13 | AllowOption{
14 | RetryCount: Int64(100),
15 | RetryDelay: Time(30 * time.Millisecond),
16 | Timeout: Time(3 * time.Millisecond),
17 | Jitter: Bool(true),
18 | },
19 | )
20 |
21 | assert.Equal(t, int64(100), o.retryCount)
22 | assert.Equal(t, 30*time.Millisecond, o.retryDelay)
23 | assert.Equal(t, 3*time.Millisecond, o.timeout)
24 | assert.Equal(t, 100*time.Millisecond, o.retryMin)
25 | assert.Equal(t, 10*time.Second, o.retryMax)
26 | assert.Equal(t, 2.0, o.retryFactor)
27 | assert.True(t, o.jitter)
28 | }
29 |
--------------------------------------------------------------------------------
/logger.go:
--------------------------------------------------------------------------------
1 | package queue
2 |
3 | import (
4 | "fmt"
5 | "log"
6 | "os"
7 | )
8 |
9 | // Logger interface is used throughout gorush
10 | type Logger interface {
11 | Infof(format string, args ...any)
12 | Errorf(format string, args ...any)
13 | Fatalf(format string, args ...any)
14 | Info(args ...any)
15 | Error(args ...any)
16 | Fatal(args ...any)
17 | }
18 |
19 | // NewLogger for simple logger.
20 | func NewLogger() Logger {
21 | return defaultLogger{
22 | infoLogger: log.New(os.Stderr, "INFO: ", log.Ldate|log.Ltime),
23 | errorLogger: log.New(os.Stderr, "ERROR: ", log.Ldate|log.Ltime),
24 | fatalLogger: log.New(os.Stderr, "FATAL: ", log.Ldate|log.Ltime),
25 | }
26 | }
27 |
28 | type defaultLogger struct {
29 | infoLogger *log.Logger
30 | errorLogger *log.Logger
31 | fatalLogger *log.Logger
32 | }
33 |
34 | func (l defaultLogger) logWithCallerf(logger *log.Logger, format string, args ...any) {
35 | stackInfo := stack(3) // Assuming stack(3) returns caller info string
36 | // Prepend stack info to the arguments and adjust the format string
37 | logger.Printf("%s "+format, append([]any{stackInfo}, args...)...)
38 | }
39 |
40 | func (l defaultLogger) logWithCaller(logger *log.Logger, args ...any) {
41 | stack := stack(3)
42 | logger.Println(append([]any{stack}, args...)...)
43 | }
44 |
45 | func (l defaultLogger) Infof(format string, args ...any) {
46 | l.infoLogger.Printf(format, args...)
47 | }
48 |
49 | func (l defaultLogger) Errorf(format string, args ...any) {
50 | l.errorLogger.Printf(format, args...)
51 | }
52 |
53 | func (l defaultLogger) Fatalf(format string, args ...any) {
54 | l.logWithCallerf(l.fatalLogger, format, args...)
55 | }
56 |
57 | func (l defaultLogger) Info(args ...any) {
58 | l.infoLogger.Println(fmt.Sprint(args...))
59 | }
60 |
61 | func (l defaultLogger) Error(args ...any) {
62 | l.errorLogger.Println(fmt.Sprint(args...))
63 | }
64 |
65 | func (l defaultLogger) Fatal(args ...any) {
66 | l.logWithCaller(l.fatalLogger, args...)
67 | }
68 |
69 | // NewEmptyLogger for simple logger.
70 | func NewEmptyLogger() Logger {
71 | return emptyLogger{}
72 | }
73 |
74 | // EmptyLogger no meesgae logger
75 | type emptyLogger struct{}
76 |
77 | func (l emptyLogger) Infof(format string, args ...any) {}
78 | func (l emptyLogger) Errorf(format string, args ...any) {}
79 | func (l emptyLogger) Fatalf(format string, args ...any) {}
80 | func (l emptyLogger) Info(args ...any) {}
81 | func (l emptyLogger) Error(args ...any) {}
82 | func (l emptyLogger) Fatal(args ...any) {}
83 |
--------------------------------------------------------------------------------
/logger_test.go:
--------------------------------------------------------------------------------
1 | package queue
2 |
3 | func ExampleNewEmptyLogger() {
4 | l := NewEmptyLogger()
5 | l.Info("test")
6 | l.Infof("test")
7 | l.Error("test")
8 | l.Errorf("test")
9 | l.Fatal("test")
10 | l.Fatalf("test")
11 | // Output:
12 | }
13 |
--------------------------------------------------------------------------------
/metric.go:
--------------------------------------------------------------------------------
1 | package queue
2 |
3 | import "sync/atomic"
4 |
5 | // Metric interface
6 | type Metric interface {
7 | IncBusyWorker()
8 | DecBusyWorker()
9 | BusyWorkers() int64
10 | SuccessTasks() uint64
11 | FailureTasks() uint64
12 | SubmittedTasks() uint64
13 | CompletedTasks() uint64
14 | IncSuccessTask()
15 | IncFailureTask()
16 | IncSubmittedTask()
17 | }
18 |
19 | var _ Metric = (*metric)(nil)
20 |
21 | type metric struct {
22 | busyWorkers int64
23 | successTasks uint64
24 | failureTasks uint64
25 | submittedTasks uint64
26 | }
27 |
28 | // NewMetric for default metric structure
29 | func NewMetric() Metric {
30 | return &metric{}
31 | }
32 |
33 | func (m *metric) IncBusyWorker() {
34 | atomic.AddInt64(&m.busyWorkers, 1)
35 | }
36 |
37 | func (m *metric) DecBusyWorker() {
38 | atomic.AddInt64(&m.busyWorkers, ^int64(0))
39 | }
40 |
41 | func (m *metric) BusyWorkers() int64 {
42 | return atomic.LoadInt64(&m.busyWorkers)
43 | }
44 |
45 | func (m *metric) IncSuccessTask() {
46 | atomic.AddUint64(&m.successTasks, 1)
47 | }
48 |
49 | func (m *metric) IncFailureTask() {
50 | atomic.AddUint64(&m.failureTasks, 1)
51 | }
52 |
53 | func (m *metric) IncSubmittedTask() {
54 | atomic.AddUint64(&m.submittedTasks, 1)
55 | }
56 |
57 | func (m *metric) SuccessTasks() uint64 {
58 | return atomic.LoadUint64(&m.successTasks)
59 | }
60 |
61 | func (m *metric) FailureTasks() uint64 {
62 | return atomic.LoadUint64(&m.failureTasks)
63 | }
64 |
65 | func (m *metric) SubmittedTasks() uint64 {
66 | return atomic.LoadUint64(&m.submittedTasks)
67 | }
68 |
69 | func (m *metric) CompletedTasks() uint64 {
70 | return atomic.LoadUint64(&m.successTasks) + atomic.LoadUint64(&m.failureTasks)
71 | }
72 |
--------------------------------------------------------------------------------
/metric_test.go:
--------------------------------------------------------------------------------
1 | package queue
2 |
3 | import (
4 | "context"
5 | "errors"
6 | "testing"
7 | "time"
8 |
9 | "github.com/golang-queue/queue/core"
10 |
11 | "github.com/stretchr/testify/assert"
12 | )
13 |
14 | func TestMetricData(t *testing.T) {
15 | w := NewRing(
16 | WithFn(func(ctx context.Context, m core.TaskMessage) error {
17 | switch string(m.Payload()) {
18 | case "foo1":
19 | panic("missing something")
20 | case "foo2":
21 | return errors.New("missing something")
22 | case "foo3":
23 | return nil
24 | }
25 | return nil
26 | }),
27 | )
28 | q, err := NewQueue(
29 | WithWorker(w),
30 | WithWorkerCount(4),
31 | )
32 | assert.NoError(t, err)
33 | assert.NoError(t, q.Queue(mockMessage{
34 | message: "foo1",
35 | }))
36 | assert.NoError(t, q.Queue(mockMessage{
37 | message: "foo2",
38 | }))
39 | assert.NoError(t, q.Queue(mockMessage{
40 | message: "foo3",
41 | }))
42 | assert.NoError(t, q.Queue(mockMessage{
43 | message: "foo4",
44 | }))
45 | q.Start()
46 | time.Sleep(50 * time.Millisecond)
47 | assert.Equal(t, uint64(4), q.SubmittedTasks())
48 | assert.Equal(t, uint64(2), q.SuccessTasks())
49 | assert.Equal(t, uint64(2), q.FailureTasks())
50 | assert.Equal(t, uint64(4), q.CompletedTasks())
51 | q.Release()
52 | }
53 |
--------------------------------------------------------------------------------
/mocks/mock_queued_message.go:
--------------------------------------------------------------------------------
1 | // Code generated by MockGen. DO NOT EDIT.
2 | // Source: github.com/golang-queue/queue/core (interfaces: QueuedMessage)
3 | //
4 | // Generated by this command:
5 | //
6 | // mockgen -package=mocks -destination=mock_queued_message.go github.com/golang-queue/queue/core QueuedMessage
7 | //
8 |
9 | // Package mocks is a generated GoMock package.
10 | package mocks
11 |
12 | import (
13 | reflect "reflect"
14 |
15 | gomock "go.uber.org/mock/gomock"
16 | )
17 |
18 | // MockQueuedMessage is a mock of QueuedMessage interface.
19 | type MockQueuedMessage struct {
20 | ctrl *gomock.Controller
21 | recorder *MockQueuedMessageMockRecorder
22 | }
23 |
24 | // MockQueuedMessageMockRecorder is the mock recorder for MockQueuedMessage.
25 | type MockQueuedMessageMockRecorder struct {
26 | mock *MockQueuedMessage
27 | }
28 |
29 | // NewMockQueuedMessage creates a new mock instance.
30 | func NewMockQueuedMessage(ctrl *gomock.Controller) *MockQueuedMessage {
31 | mock := &MockQueuedMessage{ctrl: ctrl}
32 | mock.recorder = &MockQueuedMessageMockRecorder{mock}
33 | return mock
34 | }
35 |
36 | // EXPECT returns an object that allows the caller to indicate expected use.
37 | func (m *MockQueuedMessage) EXPECT() *MockQueuedMessageMockRecorder {
38 | return m.recorder
39 | }
40 |
41 | // Bytes mocks base method.
42 | func (m *MockQueuedMessage) Bytes() []byte {
43 | m.ctrl.T.Helper()
44 | ret := m.ctrl.Call(m, "Bytes")
45 | ret0, _ := ret[0].([]byte)
46 | return ret0
47 | }
48 |
49 | // Bytes indicates an expected call of Bytes.
50 | func (mr *MockQueuedMessageMockRecorder) Bytes() *gomock.Call {
51 | mr.mock.ctrl.T.Helper()
52 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Bytes", reflect.TypeOf((*MockQueuedMessage)(nil).Bytes))
53 | }
54 |
--------------------------------------------------------------------------------
/mocks/mock_task_message.go:
--------------------------------------------------------------------------------
1 | // Code generated by MockGen. DO NOT EDIT.
2 | // Source: github.com/golang-queue/queue/core (interfaces: TaskMessage)
3 | //
4 | // Generated by this command:
5 | //
6 | // mockgen -package=mocks -destination=mock_task_message.go github.com/golang-queue/queue/core TaskMessage
7 | //
8 |
9 | // Package mocks is a generated GoMock package.
10 | package mocks
11 |
12 | import (
13 | reflect "reflect"
14 |
15 | gomock "go.uber.org/mock/gomock"
16 | )
17 |
18 | // MockTaskMessage is a mock of TaskMessage interface.
19 | type MockTaskMessage struct {
20 | ctrl *gomock.Controller
21 | recorder *MockTaskMessageMockRecorder
22 | }
23 |
24 | // MockTaskMessageMockRecorder is the mock recorder for MockTaskMessage.
25 | type MockTaskMessageMockRecorder struct {
26 | mock *MockTaskMessage
27 | }
28 |
29 | // NewMockTaskMessage creates a new mock instance.
30 | func NewMockTaskMessage(ctrl *gomock.Controller) *MockTaskMessage {
31 | mock := &MockTaskMessage{ctrl: ctrl}
32 | mock.recorder = &MockTaskMessageMockRecorder{mock}
33 | return mock
34 | }
35 |
36 | // EXPECT returns an object that allows the caller to indicate expected use.
37 | func (m *MockTaskMessage) EXPECT() *MockTaskMessageMockRecorder {
38 | return m.recorder
39 | }
40 |
41 | // Bytes mocks base method.
42 | func (m *MockTaskMessage) Bytes() []byte {
43 | m.ctrl.T.Helper()
44 | ret := m.ctrl.Call(m, "Bytes")
45 | ret0, _ := ret[0].([]byte)
46 | return ret0
47 | }
48 |
49 | // Bytes indicates an expected call of Bytes.
50 | func (mr *MockTaskMessageMockRecorder) Bytes() *gomock.Call {
51 | mr.mock.ctrl.T.Helper()
52 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Bytes", reflect.TypeOf((*MockTaskMessage)(nil).Bytes))
53 | }
54 |
55 | // Payload mocks base method.
56 | func (m *MockTaskMessage) Payload() []byte {
57 | m.ctrl.T.Helper()
58 | ret := m.ctrl.Call(m, "Payload")
59 | ret0, _ := ret[0].([]byte)
60 | return ret0
61 | }
62 |
63 | // Payload indicates an expected call of Payload.
64 | func (mr *MockTaskMessageMockRecorder) Payload() *gomock.Call {
65 | mr.mock.ctrl.T.Helper()
66 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Payload", reflect.TypeOf((*MockTaskMessage)(nil).Payload))
67 | }
68 |
--------------------------------------------------------------------------------
/mocks/mock_worker.go:
--------------------------------------------------------------------------------
1 | // Code generated by MockGen. DO NOT EDIT.
2 | // Source: github.com/golang-queue/queue/core (interfaces: Worker)
3 | //
4 | // Generated by this command:
5 | //
6 | // mockgen -package=mocks -destination=mock_worker.go github.com/golang-queue/queue/core Worker
7 | //
8 |
9 | // Package mocks is a generated GoMock package.
10 | package mocks
11 |
12 | import (
13 | context "context"
14 | reflect "reflect"
15 |
16 | core "github.com/golang-queue/queue/core"
17 | gomock "go.uber.org/mock/gomock"
18 | )
19 |
20 | // MockWorker is a mock of Worker interface.
21 | type MockWorker struct {
22 | ctrl *gomock.Controller
23 | recorder *MockWorkerMockRecorder
24 | }
25 |
26 | // MockWorkerMockRecorder is the mock recorder for MockWorker.
27 | type MockWorkerMockRecorder struct {
28 | mock *MockWorker
29 | }
30 |
31 | // NewMockWorker creates a new mock instance.
32 | func NewMockWorker(ctrl *gomock.Controller) *MockWorker {
33 | mock := &MockWorker{ctrl: ctrl}
34 | mock.recorder = &MockWorkerMockRecorder{mock}
35 | return mock
36 | }
37 |
38 | // EXPECT returns an object that allows the caller to indicate expected use.
39 | func (m *MockWorker) EXPECT() *MockWorkerMockRecorder {
40 | return m.recorder
41 | }
42 |
43 | // Queue mocks base method.
44 | func (m *MockWorker) Queue(arg0 core.TaskMessage) error {
45 | m.ctrl.T.Helper()
46 | ret := m.ctrl.Call(m, "Queue", arg0)
47 | ret0, _ := ret[0].(error)
48 | return ret0
49 | }
50 |
51 | // Queue indicates an expected call of Queue.
52 | func (mr *MockWorkerMockRecorder) Queue(arg0 any) *gomock.Call {
53 | mr.mock.ctrl.T.Helper()
54 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Queue", reflect.TypeOf((*MockWorker)(nil).Queue), arg0)
55 | }
56 |
57 | // Request mocks base method.
58 | func (m *MockWorker) Request() (core.TaskMessage, error) {
59 | m.ctrl.T.Helper()
60 | ret := m.ctrl.Call(m, "Request")
61 | ret0, _ := ret[0].(core.TaskMessage)
62 | ret1, _ := ret[1].(error)
63 | return ret0, ret1
64 | }
65 |
66 | // Request indicates an expected call of Request.
67 | func (mr *MockWorkerMockRecorder) Request() *gomock.Call {
68 | mr.mock.ctrl.T.Helper()
69 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Request", reflect.TypeOf((*MockWorker)(nil).Request))
70 | }
71 |
72 | // Run mocks base method.
73 | func (m *MockWorker) Run(arg0 context.Context, arg1 core.TaskMessage) error {
74 | m.ctrl.T.Helper()
75 | ret := m.ctrl.Call(m, "Run", arg0, arg1)
76 | ret0, _ := ret[0].(error)
77 | return ret0
78 | }
79 |
80 | // Run indicates an expected call of Run.
81 | func (mr *MockWorkerMockRecorder) Run(arg0, arg1 any) *gomock.Call {
82 | mr.mock.ctrl.T.Helper()
83 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Run", reflect.TypeOf((*MockWorker)(nil).Run), arg0, arg1)
84 | }
85 |
86 | // Shutdown mocks base method.
87 | func (m *MockWorker) Shutdown() error {
88 | m.ctrl.T.Helper()
89 | ret := m.ctrl.Call(m, "Shutdown")
90 | ret0, _ := ret[0].(error)
91 | return ret0
92 | }
93 |
94 | // Shutdown indicates an expected call of Shutdown.
95 | func (mr *MockWorkerMockRecorder) Shutdown() *gomock.Call {
96 | mr.mock.ctrl.T.Helper()
97 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Shutdown", reflect.TypeOf((*MockWorker)(nil).Shutdown))
98 | }
99 |
--------------------------------------------------------------------------------
/mocks/mocks.go:
--------------------------------------------------------------------------------
1 | package mocks
2 |
3 | //go:generate mockgen -package=mocks -destination=mock_worker.go github.com/golang-queue/queue/core Worker
4 | //go:generate mockgen -package=mocks -destination=mock_queued_message.go github.com/golang-queue/queue/core QueuedMessage
5 | //go:generate mockgen -package=mocks -destination=mock_task_message.go github.com/golang-queue/queue/core TaskMessage
6 |
--------------------------------------------------------------------------------
/options.go:
--------------------------------------------------------------------------------
1 | package queue
2 |
3 | import (
4 | "context"
5 | "runtime"
6 | "time"
7 |
8 | "github.com/golang-queue/queue/core"
9 | )
10 |
11 | var (
12 | defaultCapacity = 0
13 | defaultWorkerCount = int64(runtime.NumCPU())
14 | defaultNewLogger = NewLogger()
15 | defaultFn = func(context.Context, core.TaskMessage) error { return nil }
16 | defaultMetric = NewMetric()
17 | )
18 |
19 | // An Option configures a mutex.
20 | type Option interface {
21 | apply(*Options)
22 | }
23 |
24 | // OptionFunc is a function that configures a queue.
25 | type OptionFunc func(*Options)
26 |
27 | // Apply calls f(option)
28 | func (f OptionFunc) apply(option *Options) {
29 | f(option)
30 | }
31 |
32 | // WithWorkerCount set worker count
33 | func WithWorkerCount(num int64) Option {
34 | return OptionFunc(func(q *Options) {
35 | if num <= 0 {
36 | num = defaultWorkerCount
37 | }
38 | q.workerCount = num
39 | })
40 | }
41 |
42 | // WithQueueSize set worker count
43 | func WithQueueSize(num int) Option {
44 | return OptionFunc(func(q *Options) {
45 | q.queueSize = num
46 | })
47 | }
48 |
49 | // WithLogger set custom logger
50 | func WithLogger(l Logger) Option {
51 | return OptionFunc(func(q *Options) {
52 | q.logger = l
53 | })
54 | }
55 |
56 | // WithMetric set custom Metric
57 | func WithMetric(m Metric) Option {
58 | return OptionFunc(func(q *Options) {
59 | q.metric = m
60 | })
61 | }
62 |
63 | // WithWorker set custom worker
64 | func WithWorker(w core.Worker) Option {
65 | return OptionFunc(func(q *Options) {
66 | q.worker = w
67 | })
68 | }
69 |
70 | // WithFn set custom job function
71 | func WithFn(fn func(context.Context, core.TaskMessage) error) Option {
72 | return OptionFunc(func(q *Options) {
73 | q.fn = fn
74 | })
75 | }
76 |
77 | // WithAfterFn set callback function after job done
78 | func WithAfterFn(afterFn func()) Option {
79 | return OptionFunc(func(q *Options) {
80 | q.afterFn = afterFn
81 | })
82 | }
83 |
84 | // WithRetryInterval sets the retry interval
85 | func WithRetryInterval(d time.Duration) Option {
86 | return OptionFunc(func(q *Options) {
87 | q.retryInterval = d
88 | })
89 | }
90 |
91 | // Options for custom args in Queue
92 | type Options struct {
93 | workerCount int64
94 | logger Logger
95 | queueSize int
96 | worker core.Worker
97 | fn func(context.Context, core.TaskMessage) error
98 | afterFn func()
99 | metric Metric
100 | retryInterval time.Duration
101 | }
102 |
103 | // NewOptions initialize the default value for the options
104 | func NewOptions(opts ...Option) *Options {
105 | o := &Options{
106 | workerCount: defaultWorkerCount,
107 | queueSize: defaultCapacity,
108 | logger: defaultNewLogger,
109 | worker: nil,
110 | fn: defaultFn,
111 | metric: defaultMetric,
112 | retryInterval: time.Second,
113 | }
114 |
115 | // Loop through each option
116 | for _, opt := range opts {
117 | // Call the option giving the instantiated
118 | opt.apply(o)
119 | }
120 |
121 | return o
122 | }
123 |
--------------------------------------------------------------------------------
/options_test.go:
--------------------------------------------------------------------------------
1 | package queue
2 |
3 | import (
4 | "testing"
5 | "time"
6 | )
7 |
8 | func TestWithRetryInterval(t *testing.T) {
9 | tests := []struct {
10 | name string
11 | duration time.Duration
12 | want time.Duration
13 | }{
14 | {
15 | name: "Set 2 seconds retry interval",
16 | duration: 2 * time.Second,
17 | want: 2 * time.Second,
18 | },
19 | {
20 | name: "Set 500ms retry interval",
21 | duration: 500 * time.Millisecond,
22 | want: 500 * time.Millisecond,
23 | },
24 | {
25 | name: "Set zero retry interval",
26 | duration: 0,
27 | want: 0,
28 | },
29 | {
30 | name: "Set negative retry interval",
31 | duration: -1 * time.Second,
32 | want: -1 * time.Second,
33 | },
34 | }
35 |
36 | for _, tt := range tests {
37 | t.Run(tt.name, func(t *testing.T) {
38 | opts := NewOptions(WithRetryInterval(tt.duration))
39 | if opts.retryInterval != tt.want {
40 | t.Errorf("WithRetryInterval() = %v, want %v", opts.retryInterval, tt.want)
41 | }
42 | })
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/pool.go:
--------------------------------------------------------------------------------
1 | package queue
2 |
3 | // NewPool initializes a new pool
4 | func NewPool(size int64, opts ...Option) *Queue {
5 | o := []Option{
6 | WithWorkerCount(size),
7 | WithWorker(NewRing(opts...)),
8 | }
9 | o = append(
10 | o,
11 | opts...,
12 | )
13 |
14 | q, err := NewQueue(o...)
15 | if err != nil {
16 | panic(err)
17 | }
18 |
19 | q.Start()
20 |
21 | return q
22 | }
23 |
--------------------------------------------------------------------------------
/pool_test.go:
--------------------------------------------------------------------------------
1 | package queue
2 |
3 | import (
4 | "context"
5 | "testing"
6 |
7 | "github.com/stretchr/testify/assert"
8 | )
9 |
10 | func TestNewPoolWithQueueTask(t *testing.T) {
11 | totalN := int64(5)
12 | taskN := 100
13 | rets := make(chan struct{}, taskN)
14 |
15 | p := NewPool(totalN)
16 | for i := 0; i < taskN; i++ {
17 | assert.NoError(t, p.QueueTask(func(context.Context) error {
18 | rets <- struct{}{}
19 | return nil
20 | }))
21 | }
22 |
23 | for i := 0; i < taskN; i++ {
24 | <-rets
25 | }
26 |
27 | // shutdown all, and now running worker is 0
28 | p.Release()
29 | assert.Equal(t, int64(0), p.BusyWorkers())
30 | }
31 |
32 | func TestPoolNumber(t *testing.T) {
33 | p := NewPool(0)
34 | p.Start()
35 | // shutdown all, and now running worker is 0
36 | p.Release()
37 | assert.Equal(t, int64(0), p.BusyWorkers())
38 | }
39 |
--------------------------------------------------------------------------------
/queue.go:
--------------------------------------------------------------------------------
1 | package queue
2 |
3 | // Package queue provides a high-performance, extensible message queue implementation
4 | // supporting multiple workers, job retries, dynamic scaling, and graceful shutdown.
5 |
6 | import (
7 | "context"
8 | "errors"
9 | "sync"
10 | "sync/atomic"
11 | "time"
12 |
13 | "github.com/golang-queue/queue/core"
14 | "github.com/golang-queue/queue/job"
15 |
16 | "github.com/jpillora/backoff"
17 | )
18 |
19 | /*
20 | ErrQueueShutdown is returned when an operation is attempted on a queue
21 | that has already been closed and released.
22 | */
23 | var ErrQueueShutdown = errors.New("queue has been closed and released")
24 |
25 | type (
26 | // Queue represents a message queue with worker management, job scheduling,
27 | // retry logic, and graceful shutdown capabilities.
28 | Queue struct {
29 | sync.Mutex // Mutex to protect concurrent access to queue state
30 | metric *metric // Metrics collector for tracking queue and worker stats
31 | logger Logger // Logger for queue events and errors
32 | workerCount int64 // Number of worker goroutines to process jobs
33 | routineGroup *routineGroup // Group to manage and wait for goroutines
34 | quit chan struct{} // Channel to signal shutdown to all goroutines
35 | ready chan struct{} // Channel to signal worker readiness
36 | notify chan struct{} // Channel to notify workers of new jobs
37 | worker core.Worker // The worker implementation that processes jobs
38 | stopOnce sync.Once // Ensures shutdown is only performed once
39 | stopFlag int32 // Atomic flag indicating if shutdown has started
40 | afterFn func() // Optional callback after each job execution
41 | retryInterval time.Duration // Interval for retrying job requests
42 | }
43 | )
44 |
45 | /*
46 | ErrMissingWorker is returned when a queue is created without a worker implementation.
47 | */
48 | var ErrMissingWorker = errors.New("missing worker module")
49 |
50 | // NewQueue creates and returns a new Queue instance with the provided options.
51 | // Returns an error if no worker is specified.
52 | func NewQueue(opts ...Option) (*Queue, error) {
53 | o := NewOptions(opts...)
54 | q := &Queue{
55 | routineGroup: newRoutineGroup(), // Manages all goroutines spawned by the queue
56 | quit: make(chan struct{}), // Signals shutdown to all goroutines
57 | ready: make(chan struct{}, 1), // Signals when a worker is ready to process a job
58 | notify: make(chan struct{}, 1), // Notifies workers of new jobs
59 | workerCount: o.workerCount, // Number of worker goroutines
60 | logger: o.logger, // Logger for queue events
61 | worker: o.worker, // Worker implementation
62 | metric: &metric{}, // Metrics collector
63 | afterFn: o.afterFn, // Optional post-job callback
64 | retryInterval: o.retryInterval, // Interval for retrying job requests
65 | }
66 |
67 | if q.worker == nil {
68 | return nil, ErrMissingWorker
69 | }
70 |
71 | return q, nil
72 | }
73 |
74 | // Start launches all worker goroutines and begins processing jobs.
75 | // If workerCount is zero, Start is a no-op.
76 | func (q *Queue) Start() {
77 | q.Lock()
78 | count := q.workerCount
79 | q.Unlock()
80 | if count == 0 {
81 | return
82 | }
83 | q.routineGroup.Run(func() {
84 | q.start()
85 | })
86 | }
87 |
88 | // Shutdown initiates a graceful shutdown of the queue.
89 | // It signals all goroutines to stop, shuts down the worker, and closes the quit channel.
90 | // Shutdown is idempotent and safe to call multiple times.
91 | func (q *Queue) Shutdown() {
92 | if !atomic.CompareAndSwapInt32(&q.stopFlag, 0, 1) {
93 | return
94 | }
95 |
96 | q.stopOnce.Do(func() {
97 | if q.metric.BusyWorkers() > 0 {
98 | q.logger.Infof("shutdown all tasks: %d workers", q.metric.BusyWorkers())
99 | }
100 |
101 | if err := q.worker.Shutdown(); err != nil {
102 | q.logger.Error(err)
103 | }
104 | close(q.quit)
105 | })
106 | }
107 |
108 | // Release performs a graceful shutdown and waits for all goroutines to finish.
109 | func (q *Queue) Release() {
110 | q.Shutdown()
111 | q.Wait()
112 | }
113 |
114 | // BusyWorkers returns the number of workers currently processing jobs.
115 | func (q *Queue) BusyWorkers() int64 {
116 | return q.metric.BusyWorkers()
117 | }
118 |
119 | // SuccessTasks returns the number of successfully completed tasks.
120 | func (q *Queue) SuccessTasks() uint64 {
121 | return q.metric.SuccessTasks()
122 | }
123 |
124 | // FailureTasks returns the number of failed tasks.
125 | func (q *Queue) FailureTasks() uint64 {
126 | return q.metric.FailureTasks()
127 | }
128 |
129 | // SubmittedTasks returns the number of tasks submitted to the queue.
130 | func (q *Queue) SubmittedTasks() uint64 {
131 | return q.metric.SubmittedTasks()
132 | }
133 |
134 | // CompletedTasks returns the total number of completed tasks (success + failure).
135 | func (q *Queue) CompletedTasks() uint64 {
136 | return q.metric.CompletedTasks()
137 | }
138 |
139 | // Wait blocks until all goroutines in the routine group have finished.
140 | func (q *Queue) Wait() {
141 | q.routineGroup.Wait()
142 | }
143 |
144 | // Queue enqueues a single job (core.QueuedMessage) into the queue.
145 | // Accepts job options for customization.
146 | func (q *Queue) Queue(message core.QueuedMessage, opts ...job.AllowOption) error {
147 | data := job.NewMessage(message, opts...)
148 |
149 | return q.queue(&data)
150 | }
151 |
152 | // QueueTask enqueues a single task function into the queue.
153 | // Accepts job options for customization.
154 | func (q *Queue) QueueTask(task job.TaskFunc, opts ...job.AllowOption) error {
155 | data := job.NewTask(task, opts...)
156 | return q.queue(&data)
157 | }
158 |
159 | // queue is an internal helper to enqueue a job.Message into the worker.
160 | // It increments the submitted task metric and notifies workers if possible.
161 | func (q *Queue) queue(m *job.Message) error {
162 | if atomic.LoadInt32(&q.stopFlag) == 1 {
163 | return ErrQueueShutdown
164 | }
165 |
166 | if err := q.worker.Queue(m); err != nil {
167 | return err
168 | }
169 |
170 | q.metric.IncSubmittedTask()
171 | // Notify a worker that a new job is available.
172 | // If the notify channel is full, the worker is busy and we avoid blocking.
173 | select {
174 | case q.notify <- struct{}{}:
175 | default:
176 | }
177 |
178 | return nil
179 | }
180 |
181 | // work executes a single task, handling panics and updating metrics accordingly.
182 | // After execution, it schedules the next worker if needed.
183 | func (q *Queue) work(task core.TaskMessage) {
184 | var err error
185 | // Defer block to handle panics, update metrics, and run afterFn callback.
186 | defer func() {
187 | q.metric.DecBusyWorker()
188 | e := recover()
189 | if e != nil {
190 | q.logger.Fatalf("panic error: %v", e)
191 | }
192 | q.schedule()
193 |
194 | // Update success or failure metrics based on execution result.
195 | if err == nil && e == nil {
196 | q.metric.IncSuccessTask()
197 | } else {
198 | q.metric.IncFailureTask()
199 | }
200 | if q.afterFn != nil {
201 | q.afterFn()
202 | }
203 | }()
204 |
205 | if err = q.run(task); err != nil {
206 | q.logger.Errorf("runtime error: %s", err.Error())
207 | }
208 | }
209 |
210 | // run dispatches the task to the appropriate handler based on its type.
211 | // Returns an error if the task type is invalid.
212 | func (q *Queue) run(task core.TaskMessage) error {
213 | switch t := task.(type) {
214 | case *job.Message:
215 | return q.handle(t)
216 | default:
217 | return errors.New("invalid task type")
218 | }
219 | }
220 |
221 | // handle executes a job.Message, supporting retries, timeouts, and panic recovery.
222 | // Returns an error if the job fails or times out.
223 | func (q *Queue) handle(m *job.Message) error {
224 | // done: receives the result of the job execution
225 | // panicChan: receives any panic that occurs in the job goroutine
226 | done := make(chan error, 1)
227 | panicChan := make(chan any, 1)
228 | startTime := time.Now()
229 | ctx, cancel := context.WithTimeout(context.Background(), m.Timeout)
230 | defer func() {
231 | cancel()
232 | }()
233 |
234 | // Run the job in a separate goroutine to support timeout and panic recovery.
235 | go func() {
236 | // Defer block to catch panics and send to panicChan
237 | defer func() {
238 | if p := recover(); p != nil {
239 | panicChan <- p
240 | }
241 | }()
242 |
243 | var err error
244 |
245 | // Set up backoff for retry logic
246 | b := &backoff.Backoff{
247 | Min: m.RetryMin,
248 | Max: m.RetryMax,
249 | Factor: m.RetryFactor,
250 | Jitter: m.Jitter,
251 | }
252 | delay := m.RetryDelay
253 | loop:
254 | for {
255 | // If a custom Task function is provided, use it; otherwise, use the worker's Run method.
256 | if m.Task != nil {
257 | err = m.Task(ctx)
258 | } else {
259 | err = q.worker.Run(ctx, m)
260 | }
261 |
262 | // If no error or no retries left, exit loop.
263 | if err == nil || m.RetryCount == 0 {
264 | break
265 | }
266 | m.RetryCount--
267 |
268 | // If no fixed retry delay, use backoff.
269 | if m.RetryDelay == 0 {
270 | delay = b.Duration()
271 | }
272 |
273 | select {
274 | case <-time.After(delay): // Wait before retrying
275 | q.logger.Infof("retry remaining times: %d, delay time: %s", m.RetryCount, delay)
276 | case <-ctx.Done(): // Timeout reached
277 | err = ctx.Err()
278 | break loop
279 | }
280 | }
281 |
282 | done <- err
283 | }()
284 |
285 | select {
286 | case p := <-panicChan:
287 | panic(p)
288 | case <-ctx.Done(): // Timeout reached
289 | return ctx.Err()
290 | case <-q.quit: // Queue is shutting down
291 | // Cancel job and wait for remaining time or job completion
292 | cancel()
293 | leftTime := m.Timeout - time.Since(startTime)
294 | select {
295 | case <-time.After(leftTime):
296 | return context.DeadlineExceeded
297 | case err := <-done: // Job finished
298 | return err
299 | case p := <-panicChan:
300 | panic(p)
301 | }
302 | case err := <-done: // Job finished
303 | return err
304 | }
305 | }
306 |
307 | // UpdateWorkerCount dynamically updates the number of worker goroutines.
308 | // Triggers scheduling to adjust to the new worker count.
309 | func (q *Queue) UpdateWorkerCount(num int64) {
310 | q.Lock()
311 | q.workerCount = num
312 | q.Unlock()
313 | q.schedule()
314 | }
315 |
316 | // schedule checks if more workers can be started based on the current busy count.
317 | // If so, it signals readiness to start a new worker.
318 | func (q *Queue) schedule() {
319 | q.Lock()
320 | defer q.Unlock()
321 | if q.BusyWorkers() >= q.workerCount {
322 | return
323 | }
324 |
325 | select {
326 | case q.ready <- struct{}{}:
327 | default:
328 | }
329 | }
330 |
331 | /*
332 | start launches the main worker loop, which manages job scheduling and execution.
333 |
334 | - It uses a ticker to periodically retry job requests if the queue is empty.
335 | - For each available worker slot, it requests a new task from the worker.
336 | - If a task is available, it is sent to the tasks channel and processed by a new goroutine.
337 | - The loop exits when the quit channel is closed.
338 | */
339 | func (q *Queue) start() {
340 | tasks := make(chan core.TaskMessage, 1)
341 | ticker := time.NewTicker(q.retryInterval)
342 | defer ticker.Stop()
343 |
344 | for {
345 | // Ensure the number of busy workers does not exceed the configured worker count.
346 | q.schedule()
347 |
348 | select {
349 | case <-q.ready: // Wait for a worker slot to become available
350 | case <-q.quit: // Shutdown signal received
351 | return
352 | }
353 |
354 | // Request a task from the worker in a background goroutine.
355 | q.routineGroup.Run(func() {
356 | for {
357 | t, err := q.worker.Request()
358 | if t == nil || err != nil {
359 | if err != nil {
360 | select {
361 | case <-q.quit:
362 | if !errors.Is(err, ErrNoTaskInQueue) {
363 | close(tasks)
364 | return
365 | }
366 | case <-ticker.C:
367 | case <-q.notify:
368 | }
369 | }
370 | }
371 | if t != nil {
372 | tasks <- t
373 | return
374 | }
375 |
376 | select {
377 | case <-q.quit:
378 | if !errors.Is(err, ErrNoTaskInQueue) {
379 | close(tasks)
380 | return
381 | }
382 | default:
383 | }
384 | }
385 | })
386 |
387 | task, ok := <-tasks
388 | if !ok {
389 | return
390 | }
391 |
392 | // Start processing the new task in a separate goroutine.
393 | q.metric.IncBusyWorker()
394 | q.routineGroup.Run(func() {
395 | q.work(task)
396 | })
397 | }
398 | }
399 |
--------------------------------------------------------------------------------
/queue_example_test.go:
--------------------------------------------------------------------------------
1 | package queue_test
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | "log"
7 | "time"
8 |
9 | "github.com/golang-queue/queue"
10 | "github.com/golang-queue/queue/job"
11 | )
12 |
13 | func ExampleNewPool_queueTask() {
14 | taskN := 7
15 | rets := make(chan int, taskN)
16 | // allocate a pool with 5 goroutines to deal with those tasks
17 | p := queue.NewPool(5)
18 | // don't forget to release the pool in the end
19 | defer p.Release()
20 |
21 | // assign tasks to asynchronous goroutine pool
22 | for i := 0; i < taskN; i++ {
23 | idx := i
24 | if err := p.QueueTask(func(context.Context) error {
25 | // sleep and return the index
26 | time.Sleep(20 * time.Millisecond)
27 | rets <- idx
28 | return nil
29 | }); err != nil {
30 | log.Println(err)
31 | }
32 | }
33 |
34 | // wait until all tasks done
35 | for i := 0; i < taskN; i++ {
36 | fmt.Println("index:", <-rets)
37 | }
38 |
39 | // Unordered output:
40 | // index: 3
41 | // index: 0
42 | // index: 2
43 | // index: 4
44 | // index: 5
45 | // index: 6
46 | // index: 1
47 | }
48 |
49 | func ExampleNewPool_queueTaskTimeout() {
50 | taskN := 7
51 | rets := make(chan int, taskN)
52 | resps := make(chan error, 1)
53 | // allocate a pool with 5 goroutines to deal with those tasks
54 | q := queue.NewPool(5)
55 | // don't forget to release the pool in the end
56 | defer q.Release()
57 |
58 | // assign tasks to asynchronous goroutine pool
59 | for i := 0; i < taskN; i++ {
60 | idx := i
61 | if err := q.QueueTask(func(ctx context.Context) error {
62 | // panic job
63 | if idx == 5 {
64 | panic("system error")
65 | }
66 | // timeout job
67 | if idx == 6 {
68 | time.Sleep(105 * time.Millisecond)
69 | }
70 | select {
71 | case <-ctx.Done():
72 | resps <- ctx.Err()
73 | default:
74 | }
75 |
76 | rets <- idx
77 | return nil
78 | }, job.AllowOption{
79 | Timeout: job.Time(100 * time.Millisecond),
80 | }); err != nil {
81 | log.Println(err)
82 | }
83 | }
84 |
85 | // wait until all tasks done
86 | for i := 0; i < taskN-1; i++ {
87 | fmt.Println("index:", <-rets)
88 | }
89 | close(resps)
90 | for e := range resps {
91 | fmt.Println(e.Error())
92 | }
93 |
94 | fmt.Println("success task count:", q.SuccessTasks())
95 | fmt.Println("failure task count:", q.FailureTasks())
96 | fmt.Println("submitted task count:", q.SubmittedTasks())
97 |
98 | // Unordered output:
99 | // index: 3
100 | // index: 0
101 | // index: 2
102 | // index: 4
103 | // index: 6
104 | // index: 1
105 | // context deadline exceeded
106 | // success task count: 5
107 | // failure task count: 2
108 | // submitted task count: 7
109 | }
110 |
--------------------------------------------------------------------------------
/queue_test.go:
--------------------------------------------------------------------------------
1 | package queue
2 |
3 | import (
4 | "context"
5 | "errors"
6 | "testing"
7 | "time"
8 |
9 | "github.com/appleboy/com/bytesconv"
10 | "github.com/golang-queue/queue/core"
11 | "github.com/golang-queue/queue/job"
12 | "github.com/golang-queue/queue/mocks"
13 |
14 | "github.com/stretchr/testify/assert"
15 | "go.uber.org/goleak"
16 | "go.uber.org/mock/gomock"
17 | )
18 |
19 | func TestMain(m *testing.M) {
20 | goleak.VerifyTestMain(m)
21 | }
22 |
23 | type mockMessage struct {
24 | message string
25 | }
26 |
27 | func (m mockMessage) Bytes() []byte {
28 | return bytesconv.StrToBytes(m.message)
29 | }
30 |
31 | func (m mockMessage) Payload() []byte {
32 | return bytesconv.StrToBytes(m.message)
33 | }
34 |
35 | func TestNewQueueWithZeroWorker(t *testing.T) {
36 | controller := gomock.NewController(t)
37 | defer controller.Finish()
38 |
39 | q, err := NewQueue()
40 | assert.Error(t, err)
41 | assert.Nil(t, q)
42 |
43 | w := mocks.NewMockWorker(controller)
44 | w.EXPECT().Shutdown().Return(nil)
45 | w.EXPECT().Request().Return(nil, nil).AnyTimes()
46 | q, err = NewQueue(
47 | WithWorker(w),
48 | WithWorkerCount(0),
49 | )
50 | assert.NoError(t, err)
51 | assert.NotNil(t, q)
52 |
53 | q.Start()
54 | time.Sleep(50 * time.Millisecond)
55 | assert.Equal(t, int64(0), q.BusyWorkers())
56 | q.Release()
57 | }
58 |
59 | func TestNewQueueWithDefaultWorker(t *testing.T) {
60 | controller := gomock.NewController(t)
61 | defer controller.Finish()
62 |
63 | q, err := NewQueue()
64 | assert.Error(t, err)
65 | assert.Nil(t, q)
66 |
67 | w := mocks.NewMockWorker(controller)
68 | m := mocks.NewMockTaskMessage(controller)
69 | m.EXPECT().Bytes().Return([]byte("test")).AnyTimes()
70 | m.EXPECT().Payload().Return([]byte("test")).AnyTimes()
71 | w.EXPECT().Shutdown().Return(nil)
72 | w.EXPECT().Request().Return(m, nil).AnyTimes()
73 | w.EXPECT().Run(context.Background(), m).Return(nil).AnyTimes()
74 | q, err = NewQueue(
75 | WithWorker(w),
76 | )
77 | assert.NoError(t, err)
78 | assert.NotNil(t, q)
79 |
80 | q.Start()
81 | q.Release()
82 | assert.Equal(t, int64(0), q.BusyWorkers())
83 | }
84 |
85 | func TestHandleTimeout(t *testing.T) {
86 | m := &job.Message{
87 | Timeout: 100 * time.Millisecond,
88 | Body: []byte("foo"),
89 | }
90 | w := NewRing(
91 | WithFn(func(ctx context.Context, m core.TaskMessage) error {
92 | time.Sleep(200 * time.Millisecond)
93 | return nil
94 | }),
95 | )
96 |
97 | q, err := NewQueue(
98 | WithWorker(w),
99 | )
100 | assert.NoError(t, err)
101 | assert.NotNil(t, q)
102 |
103 | err = q.handle(m)
104 | assert.Error(t, err)
105 | assert.Equal(t, context.DeadlineExceeded, err)
106 |
107 | done := make(chan error)
108 | go func() {
109 | done <- q.handle(m)
110 | }()
111 |
112 | err = <-done
113 | assert.Error(t, err)
114 | assert.Equal(t, context.DeadlineExceeded, err)
115 | }
116 |
117 | func TestJobComplete(t *testing.T) {
118 | m := &job.Message{
119 | Timeout: 100 * time.Millisecond,
120 | Body: []byte("foo"),
121 | }
122 | w := NewRing(
123 | WithFn(func(ctx context.Context, m core.TaskMessage) error {
124 | return errors.New("job completed")
125 | }),
126 | )
127 |
128 | q, err := NewQueue(
129 | WithWorker(w),
130 | )
131 | assert.NoError(t, err)
132 | assert.NotNil(t, q)
133 |
134 | err = q.handle(m)
135 | assert.Error(t, err)
136 | assert.Equal(t, errors.New("job completed"), err)
137 |
138 | m = &job.Message{
139 | Timeout: 250 * time.Millisecond,
140 | Body: []byte("foo"),
141 | }
142 |
143 | w = NewRing(
144 | WithFn(func(ctx context.Context, m core.TaskMessage) error {
145 | time.Sleep(200 * time.Millisecond)
146 | return errors.New("job completed")
147 | }),
148 | )
149 |
150 | q, err = NewQueue(
151 | WithWorker(w),
152 | )
153 | assert.NoError(t, err)
154 | assert.NotNil(t, q)
155 |
156 | err = q.handle(m)
157 | assert.Error(t, err)
158 | assert.Equal(t, errors.New("job completed"), err)
159 | }
160 |
161 | func TestTaskJobComplete(t *testing.T) {
162 | m := &job.Message{
163 | Timeout: 100 * time.Millisecond,
164 | Task: func(ctx context.Context) error {
165 | return errors.New("job completed")
166 | },
167 | }
168 | w := NewRing()
169 |
170 | q, err := NewQueue(
171 | WithWorker(w),
172 | )
173 | assert.NoError(t, err)
174 | assert.NotNil(t, q)
175 |
176 | err = q.handle(m)
177 | assert.Error(t, err)
178 | assert.Equal(t, errors.New("job completed"), err)
179 |
180 | m = &job.Message{
181 | Timeout: 250 * time.Millisecond,
182 | Task: func(ctx context.Context) error {
183 | return nil
184 | },
185 | }
186 |
187 | assert.NoError(t, q.handle(m))
188 |
189 | // job timeout
190 | m = &job.Message{
191 | Timeout: 50 * time.Millisecond,
192 | Task: func(ctx context.Context) error {
193 | time.Sleep(60 * time.Millisecond)
194 | return nil
195 | },
196 | }
197 | assert.Equal(t, context.DeadlineExceeded, q.handle(m))
198 | }
199 |
200 | func TestMockWorkerAndMessage(t *testing.T) {
201 | controller := gomock.NewController(t)
202 | defer controller.Finish()
203 |
204 | m := mocks.NewMockTaskMessage(controller)
205 |
206 | w := mocks.NewMockWorker(controller)
207 | w.EXPECT().Shutdown().Return(nil)
208 | w.EXPECT().Request().DoAndReturn(func() (core.TaskMessage, error) {
209 | return m, errors.New("nil")
210 | })
211 |
212 | q, err := NewQueue(
213 | WithWorker(w),
214 | WithWorkerCount(1),
215 | )
216 | assert.NoError(t, err)
217 | assert.NotNil(t, q)
218 | q.Start()
219 | time.Sleep(50 * time.Millisecond)
220 | q.Release()
221 | }
222 |
--------------------------------------------------------------------------------
/recovery.go:
--------------------------------------------------------------------------------
1 | package queue
2 |
3 | import (
4 | "bytes"
5 | "fmt"
6 | "os"
7 | "runtime"
8 | "strings"
9 | "sync"
10 | )
11 |
12 | const (
13 | dunno = "???"
14 | centerDot = "·"
15 | dot = "."
16 | slash = "/"
17 | )
18 |
19 | // bufferPool is a pool of byte buffers that can be reused to reduce the number
20 | // of allocations and improve performance. It uses sync.Pool to manage a pool
21 | // of reusable *bytes.Buffer objects. When a buffer is requested from the pool,
22 | // if one is available, it is returned; otherwise, a new buffer is created.
23 | // When a buffer is no longer needed, it should be put back into the pool to be
24 | // reused.
25 | var bufferPool = sync.Pool{
26 | New: func() any {
27 | return new(bytes.Buffer)
28 | },
29 | }
30 |
31 | // stack captures and returns the current stack trace, skipping the specified number of frames.
32 | // It retrieves the stack trace information, including the file name, line number, and function name,
33 | // and formats it into a byte slice. The function uses a buffer pool to manage memory efficiently.
34 | //
35 | // Parameters:
36 | // - skip: The number of stack frames to skip before recording the trace.
37 | //
38 | // Returns:
39 | // - A byte slice containing the formatted stack trace.
40 | func stack(skip int) []byte {
41 | buf := bufferPool.Get().(*bytes.Buffer)
42 | defer bufferPool.Put(buf)
43 | buf.Reset()
44 |
45 | var lines [][]byte
46 | var lastFile string
47 | for i := skip; ; i++ {
48 | pc, file, line, ok := runtime.Caller(i)
49 | if !ok {
50 | break
51 | }
52 | fmt.Fprintf(buf, "%s:%d (0x%x)\n", file, line, pc)
53 | if file != lastFile {
54 | data, err := os.ReadFile(file)
55 | if err != nil {
56 | continue
57 | }
58 | lines = bytes.Split(data, []byte{'\n'})
59 | lastFile = file
60 | }
61 | fmt.Fprintf(buf, "\t%s: %s\n", function(pc), source(lines, line))
62 | }
63 | return buf.Bytes()
64 | }
65 |
66 | // source retrieves the n-th line from the provided slice of byte slices,
67 | // trims any leading and trailing whitespace, and returns it as a byte slice.
68 | // If n is out of range, it returns a default "dunno" byte slice.
69 | //
70 | // Parameters:
71 | //
72 | // lines - a slice of byte slices representing lines of text
73 | // n - the 1-based index of the line to retrieve
74 | //
75 | // Returns:
76 | //
77 | // A byte slice containing the trimmed n-th line, or a default "dunno" byte slice if n is out of range.
78 | func source(lines [][]byte, n int) []byte {
79 | n--
80 | if n < 0 || n >= len(lines) {
81 | return []byte(dunno)
82 | }
83 | return bytes.TrimSpace(lines[n])
84 | }
85 |
86 | // function takes a program counter (pc) value and returns the name of the function
87 | // corresponding to that program counter as a byte slice. It uses runtime.FuncForPC
88 | // to retrieve the function information and processes the function name to remove
89 | // any path and package information, returning only the base function name.
90 | func function(pc uintptr) []byte {
91 | fn := runtime.FuncForPC(pc)
92 | if fn == nil {
93 | return []byte(dunno)
94 | }
95 | name := fn.Name()
96 | if lastSlash := strings.LastIndex(name, slash); lastSlash >= 0 {
97 | name = name[lastSlash+1:]
98 | }
99 | if period := strings.Index(name, dot); period >= 0 {
100 | name = name[period+1:]
101 | }
102 | name = strings.ReplaceAll(name, centerDot, dot)
103 | return []byte(name)
104 | }
105 |
--------------------------------------------------------------------------------
/ring.go:
--------------------------------------------------------------------------------
1 | package queue
2 |
3 | import (
4 | "context"
5 | "sync"
6 | "sync/atomic"
7 |
8 | "github.com/golang-queue/queue/core"
9 | )
10 |
11 | var _ core.Worker = (*Ring)(nil)
12 |
13 | // Ring represents a simple queue using a buffer channel.
14 | type Ring struct {
15 | sync.Mutex
16 | taskQueue []core.TaskMessage // taskQueue holds the tasks in the ring buffer.
17 | runFunc func(context.Context, core.TaskMessage) error // runFunc is the function responsible for processing tasks.
18 | capacity int // capacity is the maximum number of tasks the queue can hold.
19 | count int // count is the current number of tasks in the queue.
20 | head int // head is the index of the first task in the queue.
21 | tail int // tail is the index where the next task will be added.
22 | exit chan struct{} // exit is used to signal when the queue is shutting down.
23 | logger Logger // logger is used for logging messages.
24 | stopOnce sync.Once // stopOnce ensures the shutdown process only runs once.
25 | stopFlag int32 // stopFlag indicates whether the queue is shutting down.
26 | }
27 |
28 | // Run executes a new task using the provided context and task message.
29 | // It calls the runFunc function, which is responsible for processing the task.
30 | // The context allows for cancellation and timeout control of the task execution.
31 | func (s *Ring) Run(ctx context.Context, task core.TaskMessage) error {
32 | return s.runFunc(ctx, task)
33 | }
34 |
35 | // Shutdown gracefully shuts down the worker.
36 | // It sets the stopFlag to indicate that the queue is shutting down and prevents new tasks from being added.
37 | // If the queue is already shut down, it returns ErrQueueShutdown.
38 | // It waits for all tasks to be processed before completing the shutdown.
39 | func (s *Ring) Shutdown() error {
40 | // Attempt to set the stopFlag from 0 to 1. If it fails, the queue is already shut down.
41 | if !atomic.CompareAndSwapInt32(&s.stopFlag, 0, 1) {
42 | return ErrQueueShutdown
43 | }
44 |
45 | // Ensure the shutdown process only runs once.
46 | s.stopOnce.Do(func() {
47 | s.Lock()
48 | count := s.count
49 | s.Unlock()
50 | // If there are tasks in the queue, wait for them to be processed.
51 | if count > 0 {
52 | <-s.exit
53 | }
54 | })
55 | return nil
56 | }
57 |
58 | // Queue adds a task to the ring buffer queue.
59 | // It returns an error if the queue is shut down or has reached its maximum capacity.
60 | func (s *Ring) Queue(task core.TaskMessage) error { //nolint:stylecheck
61 | // Check if the queue is shut down
62 | if atomic.LoadInt32(&s.stopFlag) == 1 {
63 | return ErrQueueShutdown
64 | }
65 | // Check if the queue has reached its maximum capacity
66 | if s.capacity > 0 && s.count >= s.capacity {
67 | return ErrMaxCapacity
68 | }
69 |
70 | s.Lock()
71 | // Resize the queue if necessary
72 | if s.count == len(s.taskQueue) {
73 | s.resize(s.count * 2)
74 | }
75 | // Add the task to the queue
76 | s.taskQueue[s.tail] = task
77 | s.tail = (s.tail + 1) % len(s.taskQueue)
78 | s.count++
79 | s.Unlock()
80 |
81 | return nil
82 | }
83 |
84 | // Request retrieves the next task message from the ring queue.
85 | // If the queue has been stopped and is empty, it signals the exit channel
86 | // and returns an error indicating the queue has been closed.
87 | // If the queue is empty but not stopped, it returns an error indicating
88 | // there are no tasks in the queue.
89 | // If a task is successfully retrieved, it is removed from the queue,
90 | // and the queue may be resized if it is less than half full.
91 | // Returns the task message and nil on success, or an error if the queue
92 | // is empty or has been closed.
93 | func (s *Ring) Request() (core.TaskMessage, error) {
94 | if atomic.LoadInt32(&s.stopFlag) == 1 && s.count == 0 {
95 | select {
96 | case s.exit <- struct{}{}:
97 | default:
98 | }
99 | return nil, ErrQueueHasBeenClosed
100 | }
101 |
102 | s.Lock()
103 | defer s.Unlock()
104 | if s.count == 0 {
105 | return nil, ErrNoTaskInQueue
106 | }
107 | data := s.taskQueue[s.head]
108 | s.taskQueue[s.head] = nil
109 | s.head = (s.head + 1) % len(s.taskQueue)
110 | s.count--
111 |
112 | if n := len(s.taskQueue) / 2; n >= 2 && s.count <= n {
113 | s.resize(n)
114 | }
115 |
116 | return data, nil
117 | }
118 |
119 | // resize adjusts the size of the ring buffer to the specified capacity n.
120 | // It reallocates the underlying slice to the new size and copies the existing
121 | // elements to the new slice in the correct order. The head and tail pointers
122 | // are updated accordingly to maintain the correct order of elements in the
123 | // resized buffer.
124 | //
125 | // Parameters:
126 | //
127 | // n - the new capacity of the ring buffer.
128 | func (q *Ring) resize(n int) {
129 | nodes := make([]core.TaskMessage, n)
130 | if q.head < q.tail {
131 | copy(nodes, q.taskQueue[q.head:q.tail])
132 | } else {
133 | copy(nodes, q.taskQueue[q.head:])
134 | copy(nodes[len(q.taskQueue)-q.head:], q.taskQueue[:q.tail])
135 | }
136 |
137 | q.tail = q.count % n
138 | q.head = 0
139 | q.taskQueue = nodes
140 | }
141 |
142 | // NewRing creates a new Ring instance with the provided options.
143 | // It initializes the task queue with a default size of 2, sets the capacity
144 | // based on the provided options, and configures the logger and run function.
145 | // The function returns a pointer to the newly created Ring instance.
146 | //
147 | // Parameters:
148 | //
149 | // opts - A variadic list of Option functions to configure the Ring instance.
150 | //
151 | // Returns:
152 | //
153 | // *Ring - A pointer to the newly created Ring instance.
154 | func NewRing(opts ...Option) *Ring {
155 | o := NewOptions(opts...)
156 | w := &Ring{
157 | taskQueue: make([]core.TaskMessage, 2),
158 | capacity: o.queueSize,
159 | exit: make(chan struct{}),
160 | logger: o.logger,
161 | runFunc: o.fn,
162 | }
163 |
164 | return w
165 | }
166 |
--------------------------------------------------------------------------------
/ring_test.go:
--------------------------------------------------------------------------------
1 | package queue
2 |
3 | import (
4 | "context"
5 | "errors"
6 | "fmt"
7 | "log"
8 | "runtime"
9 | "testing"
10 | "time"
11 |
12 | "github.com/golang-queue/queue/core"
13 | "github.com/golang-queue/queue/job"
14 | "github.com/golang-queue/queue/mocks"
15 |
16 | "github.com/stretchr/testify/assert"
17 | "go.uber.org/mock/gomock"
18 | )
19 |
20 | func TestMaxCapacity(t *testing.T) {
21 | w := NewRing(WithQueueSize(2))
22 |
23 | assert.NoError(t, w.Queue(&mockMessage{}))
24 | assert.NoError(t, w.Queue(&mockMessage{}))
25 | assert.Error(t, w.Queue(&mockMessage{}))
26 |
27 | err := w.Queue(&mockMessage{})
28 | assert.Equal(t, ErrMaxCapacity, err)
29 | }
30 |
31 | func TestCustomFuncAndWait(t *testing.T) {
32 | m := mockMessage{
33 | message: "foo",
34 | }
35 | w := NewRing(
36 | WithFn(func(ctx context.Context, m core.TaskMessage) error {
37 | time.Sleep(500 * time.Millisecond)
38 | return nil
39 | }),
40 | )
41 | q, err := NewQueue(
42 | WithWorker(w),
43 | WithWorkerCount(2),
44 | WithLogger(NewLogger()),
45 | )
46 | assert.NoError(t, err)
47 | assert.NoError(t, q.Queue(m))
48 | assert.NoError(t, q.Queue(m))
49 | assert.NoError(t, q.Queue(m))
50 | assert.NoError(t, q.Queue(m))
51 | q.Start()
52 | time.Sleep(100 * time.Millisecond)
53 | assert.Equal(t, 2, int(q.metric.BusyWorkers()))
54 | time.Sleep(600 * time.Millisecond)
55 | q.Shutdown()
56 | q.Wait()
57 | // you will see the execute time > 1000ms
58 | }
59 |
60 | func TestEnqueueJobAfterShutdown(t *testing.T) {
61 | m := mockMessage{
62 | message: "foo",
63 | }
64 | w := NewRing()
65 | q, err := NewQueue(
66 | WithWorker(w),
67 | WithWorkerCount(2),
68 | )
69 | assert.NoError(t, err)
70 | q.Start()
71 | time.Sleep(50 * time.Millisecond)
72 | q.Shutdown()
73 | // can't queue task after shutdown
74 | err = q.Queue(m)
75 | assert.Error(t, err)
76 | assert.Equal(t, ErrQueueShutdown, err)
77 | q.Wait()
78 | }
79 |
80 | func TestJobReachTimeout(t *testing.T) {
81 | m := mockMessage{
82 | message: "foo",
83 | }
84 | w := NewRing(
85 | WithFn(func(ctx context.Context, m core.TaskMessage) error {
86 | for {
87 | select {
88 | case <-ctx.Done():
89 | log.Println("get data:", string(m.Payload()))
90 | if errors.Is(ctx.Err(), context.Canceled) {
91 | log.Println("queue has been shutdown and cancel the job")
92 | } else if errors.Is(ctx.Err(), context.DeadlineExceeded) {
93 | log.Println("job deadline exceeded")
94 | }
95 | return nil
96 | default:
97 | }
98 | time.Sleep(50 * time.Millisecond)
99 | }
100 | }),
101 | )
102 | q, err := NewQueue(
103 | WithWorker(w),
104 | WithWorkerCount(2),
105 | )
106 | assert.NoError(t, err)
107 | assert.NoError(t, q.Queue(m, job.AllowOption{Timeout: job.Time(30 * time.Millisecond)}))
108 | q.Start()
109 | time.Sleep(50 * time.Millisecond)
110 | q.Release()
111 | }
112 |
113 | func TestCancelJobAfterShutdown(t *testing.T) {
114 | m := mockMessage{
115 | message: "foo",
116 | }
117 | w := NewRing(
118 | WithLogger(NewEmptyLogger()),
119 | WithFn(func(ctx context.Context, m core.TaskMessage) error {
120 | for {
121 | select {
122 | case <-ctx.Done():
123 | log.Println("get data:", string(m.Payload()))
124 | if errors.Is(ctx.Err(), context.Canceled) {
125 | log.Println("queue has been shutdown and cancel the job")
126 | } else if errors.Is(ctx.Err(), context.DeadlineExceeded) {
127 | log.Println("job deadline exceeded")
128 | }
129 | return nil
130 | default:
131 | }
132 | time.Sleep(50 * time.Millisecond)
133 | }
134 | }),
135 | )
136 | q, err := NewQueue(
137 | WithWorker(w),
138 | WithWorkerCount(2),
139 | )
140 | assert.NoError(t, err)
141 | assert.NoError(t, q.Queue(m, job.AllowOption{Timeout: job.Time(100 * time.Millisecond)}))
142 | assert.NoError(t, q.Queue(m, job.AllowOption{Timeout: job.Time(100 * time.Millisecond)}))
143 | q.Start()
144 | time.Sleep(10 * time.Millisecond)
145 | assert.Equal(t, int64(2), q.BusyWorkers())
146 | q.Release()
147 | }
148 |
149 | func TestGoroutineLeak(t *testing.T) {
150 | w := NewRing(
151 | WithLogger(NewLogger()),
152 | WithFn(func(ctx context.Context, m core.TaskMessage) error {
153 | for {
154 | select {
155 | case <-ctx.Done():
156 | if errors.Is(ctx.Err(), context.Canceled) {
157 | log.Println("queue has been shutdown and cancel the job: " + string(m.Payload()))
158 | } else if errors.Is(ctx.Err(), context.DeadlineExceeded) {
159 | log.Println("job deadline exceeded: " + string(m.Payload()))
160 | }
161 | return nil
162 | default:
163 | log.Println("get data:", string(m.Payload()))
164 | time.Sleep(50 * time.Millisecond)
165 | return nil
166 | }
167 | }
168 | }),
169 | )
170 | q, err := NewQueue(
171 | WithLogger(NewLogger()),
172 | WithWorker(w),
173 | WithWorkerCount(10),
174 | )
175 | assert.NoError(t, err)
176 | for i := 0; i < 400; i++ {
177 | m := mockMessage{
178 | message: fmt.Sprintf("new message: %d", i+1),
179 | }
180 |
181 | assert.NoError(t, q.Queue(m))
182 | }
183 |
184 | q.Start()
185 | time.Sleep(1 * time.Second)
186 | q.Release()
187 | fmt.Println("number of goroutines:", runtime.NumGoroutine())
188 | }
189 |
190 | func TestGoroutinePanic(t *testing.T) {
191 | m := mockMessage{
192 | message: "foo",
193 | }
194 | w := NewRing(
195 | WithFn(func(ctx context.Context, m core.TaskMessage) error {
196 | panic("missing something")
197 | }),
198 | )
199 | q, err := NewQueue(
200 | WithWorker(w),
201 | WithWorkerCount(2),
202 | )
203 | assert.NoError(t, err)
204 | assert.NoError(t, q.Queue(m))
205 | q.Start()
206 | time.Sleep(10 * time.Millisecond)
207 | q.Release()
208 | }
209 |
210 | func TestIncreaseWorkerCount(t *testing.T) {
211 | w := NewRing(
212 | WithLogger(NewEmptyLogger()),
213 | WithFn(func(ctx context.Context, m core.TaskMessage) error {
214 | time.Sleep(500 * time.Millisecond)
215 | return nil
216 | }),
217 | )
218 | q, err := NewQueue(
219 | WithLogger(NewLogger()),
220 | WithWorker(w),
221 | WithWorkerCount(5),
222 | )
223 | assert.NoError(t, err)
224 |
225 | for i := 1; i <= 10; i++ {
226 | m := mockMessage{
227 | message: fmt.Sprintf("new message: %d", i),
228 | }
229 | assert.NoError(t, q.Queue(m))
230 | }
231 |
232 | q.Start()
233 | time.Sleep(100 * time.Millisecond)
234 | assert.Equal(t, int64(5), q.BusyWorkers())
235 | q.UpdateWorkerCount(10)
236 | time.Sleep(100 * time.Millisecond)
237 | assert.Equal(t, int64(10), q.BusyWorkers())
238 | q.Release()
239 | }
240 |
241 | func TestDecreaseWorkerCount(t *testing.T) {
242 | w := NewRing(
243 | WithFn(func(ctx context.Context, m core.TaskMessage) error {
244 | time.Sleep(100 * time.Millisecond)
245 | return nil
246 | }),
247 | )
248 | q, err := NewQueue(
249 | WithLogger(NewLogger()),
250 | WithWorker(w),
251 | WithWorkerCount(5),
252 | )
253 | assert.NoError(t, err)
254 |
255 | for i := 1; i <= 10; i++ {
256 | m := mockMessage{
257 | message: fmt.Sprintf("test message: %d", i),
258 | }
259 | assert.NoError(t, q.Queue(m))
260 | }
261 |
262 | q.Start()
263 | time.Sleep(20 * time.Millisecond)
264 | assert.Equal(t, int64(5), q.BusyWorkers())
265 | q.UpdateWorkerCount(3)
266 | time.Sleep(100 * time.Millisecond)
267 | assert.Equal(t, int64(3), q.BusyWorkers())
268 | time.Sleep(100 * time.Millisecond)
269 | assert.Equal(t, int64(2), q.BusyWorkers())
270 | q.Release()
271 | }
272 |
273 | func TestHandleAllJobBeforeShutdownRing(t *testing.T) {
274 | controller := gomock.NewController(t)
275 | defer controller.Finish()
276 |
277 | m := mocks.NewMockTaskMessage(controller)
278 |
279 | w := NewRing(
280 | WithFn(func(ctx context.Context, m core.TaskMessage) error {
281 | time.Sleep(10 * time.Millisecond)
282 | return nil
283 | }),
284 | )
285 |
286 | done := make(chan struct{})
287 | assert.NoError(t, w.Queue(m))
288 | assert.NoError(t, w.Queue(m))
289 | go func() {
290 | assert.NoError(t, w.Shutdown())
291 | done <- struct{}{}
292 | }()
293 | time.Sleep(50 * time.Millisecond)
294 | task, err := w.Request()
295 | assert.NotNil(t, task)
296 | assert.NoError(t, err)
297 | task, err = w.Request()
298 | assert.NotNil(t, task)
299 | assert.NoError(t, err)
300 | task, err = w.Request()
301 | assert.Nil(t, task)
302 | assert.True(t, errors.Is(err, ErrQueueHasBeenClosed))
303 | <-done
304 | }
305 |
306 | func TestHandleAllJobBeforeShutdownRingInQueue(t *testing.T) {
307 | controller := gomock.NewController(t)
308 | defer controller.Finish()
309 |
310 | m := mocks.NewMockTaskMessage(controller)
311 | m.EXPECT().Bytes().Return([]byte("test")).AnyTimes()
312 | m.EXPECT().Payload().Return([]byte("test")).AnyTimes()
313 |
314 | messages := make(chan string, 10)
315 |
316 | w := NewRing(
317 | WithFn(func(ctx context.Context, m core.TaskMessage) error {
318 | time.Sleep(10 * time.Millisecond)
319 | messages <- string(m.Payload())
320 | return nil
321 | }),
322 | )
323 |
324 | q, err := NewQueue(
325 | WithLogger(NewLogger()),
326 | WithWorker(w),
327 | WithWorkerCount(1),
328 | )
329 | assert.NoError(t, err)
330 |
331 | assert.NoError(t, q.Queue(m))
332 | assert.NoError(t, q.Queue(m))
333 | assert.Len(t, messages, 0)
334 | q.Start()
335 | q.Release()
336 | assert.Len(t, messages, 2)
337 | }
338 |
339 | func TestRetryCountWithNewMessage(t *testing.T) {
340 | controller := gomock.NewController(t)
341 | defer controller.Finish()
342 |
343 | m := mocks.NewMockQueuedMessage(controller)
344 | m.EXPECT().Bytes().Return([]byte("test")).AnyTimes()
345 |
346 | messages := make(chan string, 10)
347 | keep := make(chan struct{})
348 | count := 1
349 |
350 | w := NewRing(
351 | WithFn(func(ctx context.Context, m core.TaskMessage) error {
352 | if count%3 != 0 {
353 | count++
354 | return errors.New("count not correct")
355 | }
356 | close(keep)
357 | messages <- string(m.Payload())
358 | return nil
359 | }),
360 | )
361 |
362 | q, err := NewQueue(
363 | WithLogger(NewLogger()),
364 | WithWorker(w),
365 | WithWorkerCount(1),
366 | )
367 | assert.NoError(t, err)
368 |
369 | assert.NoError(t, q.Queue(
370 | m,
371 | job.AllowOption{
372 | RetryCount: job.Int64(3),
373 | RetryDelay: job.Time(50 * time.Millisecond),
374 | },
375 | ))
376 | assert.Len(t, messages, 0)
377 | q.Start()
378 | // wait retry twice.
379 | <-keep
380 | q.Release()
381 | assert.Len(t, messages, 1)
382 | }
383 |
384 | func TestRetryCountWithNewTask(t *testing.T) {
385 | messages := make(chan string, 10)
386 | count := 1
387 |
388 | w := NewRing()
389 |
390 | q, err := NewQueue(
391 | WithLogger(NewLogger()),
392 | WithWorker(w),
393 | WithWorkerCount(1),
394 | )
395 | assert.NoError(t, err)
396 |
397 | keep := make(chan struct{})
398 |
399 | assert.NoError(t, q.QueueTask(
400 | func(ctx context.Context) error {
401 | if count%3 != 0 {
402 | count++
403 | return errors.New("count not correct")
404 | }
405 | close(keep)
406 | messages <- "foobar"
407 | return nil
408 | },
409 | job.AllowOption{
410 | RetryCount: job.Int64(3),
411 | },
412 | ))
413 | assert.Len(t, messages, 0)
414 | q.Start()
415 | // wait retry twice.
416 | <-keep
417 | q.Release()
418 | assert.Len(t, messages, 1)
419 | }
420 |
421 | func TestCancelRetryCountWithNewTask(t *testing.T) {
422 | messages := make(chan string, 10)
423 | count := 1
424 |
425 | w := NewRing()
426 |
427 | q, err := NewQueue(
428 | WithLogger(NewLogger()),
429 | WithWorker(w),
430 | WithWorkerCount(1),
431 | )
432 | assert.NoError(t, err)
433 |
434 | assert.NoError(t, q.QueueTask(
435 | func(ctx context.Context) error {
436 | if count%3 != 0 {
437 | count++
438 | q.logger.Info("add count")
439 | return errors.New("count not correct")
440 | }
441 | messages <- "foobar"
442 | return nil
443 | },
444 | job.AllowOption{
445 | RetryCount: job.Int64(3),
446 | RetryDelay: job.Time(100 * time.Millisecond),
447 | },
448 | ))
449 | assert.Len(t, messages, 0)
450 | q.Start()
451 | time.Sleep(50 * time.Millisecond)
452 | q.Release()
453 | assert.Len(t, messages, 0)
454 | assert.Equal(t, 2, count)
455 | }
456 |
457 | func TestCancelRetryCountWithNewMessage(t *testing.T) {
458 | controller := gomock.NewController(t)
459 | defer controller.Finish()
460 |
461 | m := mocks.NewMockQueuedMessage(controller)
462 | m.EXPECT().Bytes().Return([]byte("test")).AnyTimes()
463 |
464 | messages := make(chan string, 10)
465 | count := 1
466 |
467 | w := NewRing(
468 | WithFn(func(ctx context.Context, m core.TaskMessage) error {
469 | if count%3 != 0 {
470 | count++
471 | return errors.New("count not correct")
472 | }
473 | messages <- string(m.Payload())
474 | return nil
475 | }),
476 | )
477 |
478 | q, err := NewQueue(
479 | WithLogger(NewLogger()),
480 | WithWorker(w),
481 | WithWorkerCount(1),
482 | )
483 | assert.NoError(t, err)
484 |
485 | assert.NoError(t, q.Queue(
486 | m,
487 | job.AllowOption{
488 | RetryCount: job.Int64(3),
489 | RetryDelay: job.Time(100 * time.Millisecond),
490 | },
491 | ))
492 | assert.Len(t, messages, 0)
493 | q.Start()
494 | time.Sleep(50 * time.Millisecond)
495 | q.Release()
496 | assert.Len(t, messages, 0)
497 | assert.Equal(t, 2, count)
498 | }
499 |
500 | func TestErrNoTaskInQueue(t *testing.T) {
501 | w := NewRing(
502 | WithFn(func(ctx context.Context, m core.TaskMessage) error {
503 | return nil
504 | }),
505 | )
506 | task, err := w.Request()
507 | assert.Nil(t, task)
508 | assert.Error(t, err)
509 | assert.Equal(t, ErrNoTaskInQueue, err)
510 | }
511 |
512 | func BenchmarkRingQueue(b *testing.B) {
513 | b.Run("queue and request operations", func(b *testing.B) {
514 | w := NewRing(WithQueueSize(1000))
515 | m := mockMessage{message: "test"}
516 |
517 | b.ResetTimer()
518 | b.RunParallel(func(pb *testing.PB) {
519 | for pb.Next() {
520 | _ = w.Queue(&m)
521 | _, _ = w.Request()
522 | }
523 | })
524 | })
525 |
526 | b.Run("concurrent queue operations", func(b *testing.B) {
527 | w := NewRing(WithQueueSize(1000))
528 | m := mockMessage{message: "test"}
529 |
530 | b.ResetTimer()
531 | b.RunParallel(func(pb *testing.PB) {
532 | for pb.Next() {
533 | _ = w.Queue(&m)
534 | }
535 | })
536 | })
537 |
538 | b.Run("resize operations", func(b *testing.B) {
539 | w := NewRing()
540 | m := mockMessage{message: "test"}
541 |
542 | b.ResetTimer()
543 | for i := 0; i < b.N; i++ {
544 | for j := 0; j < 100; j++ {
545 | _ = w.Queue(&m)
546 | }
547 | for j := 0; j < 100; j++ {
548 | _, _ = w.Request()
549 | }
550 | }
551 | })
552 | }
553 |
--------------------------------------------------------------------------------
/thread.go:
--------------------------------------------------------------------------------
1 | package queue
2 |
3 | import "sync"
4 |
5 | type routineGroup struct {
6 | waitGroup sync.WaitGroup
7 | }
8 |
9 | func newRoutineGroup() *routineGroup {
10 | return new(routineGroup)
11 | }
12 |
13 | func (g *routineGroup) Run(fn func()) {
14 | g.waitGroup.Add(1)
15 |
16 | go func() {
17 | defer g.waitGroup.Done()
18 | fn()
19 | }()
20 | }
21 |
22 | func (g *routineGroup) Wait() {
23 | g.waitGroup.Wait()
24 | }
25 |
--------------------------------------------------------------------------------
/thread_test.go:
--------------------------------------------------------------------------------
1 | package queue
2 |
3 | import (
4 | "sync/atomic"
5 | "testing"
6 | "time"
7 | )
8 |
9 | func TestRoutineGroupRun(t *testing.T) {
10 | t.Run("execute single function", func(t *testing.T) {
11 | g := newRoutineGroup()
12 | var counter int32
13 |
14 | g.Run(func() {
15 | atomic.AddInt32(&counter, 1)
16 | })
17 |
18 | g.Wait()
19 |
20 | if atomic.LoadInt32(&counter) != 1 {
21 | t.Errorf("expected counter to be 1, got %d", counter)
22 | }
23 | })
24 |
25 | t.Run("execute multiple functions", func(t *testing.T) {
26 | g := newRoutineGroup()
27 | var counter int32
28 | numRoutines := 10
29 |
30 | for i := 0; i < numRoutines; i++ {
31 | g.Run(func() {
32 | atomic.AddInt32(&counter, 1)
33 | time.Sleep(10 * time.Millisecond)
34 | })
35 | }
36 |
37 | g.Wait()
38 |
39 | if atomic.LoadInt32(&counter) != int32(numRoutines) {
40 | t.Errorf("expected counter to be %d, got %d", numRoutines, counter)
41 | }
42 | })
43 | }
44 |
--------------------------------------------------------------------------------