├── images └── screen.png ├── .github ├── dependabot.yml └── workflows │ ├── goreleaser.yml │ ├── go.yml │ └── codeql.yaml ├── .gitignore ├── _example ├── worker │ ├── go.mod │ ├── main.go │ └── go.sum └── producer-consumer │ ├── go.mod │ ├── producer │ └── main.go │ ├── README.md │ ├── consumer │ └── main.go │ └── go.sum ├── .golangci.yml ├── LICENSE ├── .goreleaser.yaml ├── options.go ├── README.md ├── go.mod ├── nsq.go ├── nsq_test.go └── go.sum /images/screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/golang-queue/nsq/HEAD/images/screen.png -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 | .dat 17 | -------------------------------------------------------------------------------- /_example/worker/go.mod: -------------------------------------------------------------------------------- 1 | module example 2 | 3 | go 1.22 4 | 5 | replace github.com/golang-queue/nsq => ../../ 6 | 7 | require ( 8 | github.com/golang-queue/nsq v0.0.0-00010101000000-000000000000 9 | github.com/golang-queue/queue v0.3.0 10 | ) 11 | 12 | require ( 13 | github.com/golang/snappy v0.0.4 // indirect 14 | github.com/jpillora/backoff v1.0.0 // indirect 15 | github.com/nsqio/go-nsq v1.1.0 // indirect 16 | ) 17 | -------------------------------------------------------------------------------- /_example/producer-consumer/go.mod: -------------------------------------------------------------------------------- 1 | module example 2 | 3 | go 1.22 4 | 5 | require ( 6 | github.com/appleboy/graceful v0.0.4 7 | github.com/golang-queue/nsq v0.0.0-00010101000000-000000000000 8 | github.com/golang-queue/queue v0.3.0 9 | ) 10 | 11 | require ( 12 | github.com/golang/snappy v0.0.4 // indirect 13 | github.com/jpillora/backoff v1.0.0 // indirect 14 | github.com/nsqio/go-nsq v1.1.0 // indirect 15 | ) 16 | 17 | replace github.com/golang-queue/nsq => ../../ 18 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | linters: 2 | enable-all: false 3 | disable-all: true 4 | fast: false 5 | enable: 6 | - bodyclose 7 | - dogsled 8 | - dupl 9 | - errcheck 10 | - exportloopref 11 | - exhaustive 12 | - gochecknoinits 13 | - goconst 14 | - gocritic 15 | - gocyclo 16 | - gofmt 17 | - goimports 18 | - goprintffuncname 19 | - gosec 20 | - gosimple 21 | - govet 22 | - ineffassign 23 | - lll 24 | - misspell 25 | - nakedret 26 | - noctx 27 | - nolintlint 28 | - staticcheck 29 | - stylecheck 30 | - typecheck 31 | - unconvert 32 | - unparam 33 | - unused 34 | - whitespace 35 | - gofumpt 36 | 37 | run: 38 | timeout: 3m 39 | -------------------------------------------------------------------------------- /.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 repository 16 | uses: actions/checkout@v4 17 | with: 18 | fetch-depth: 0 19 | 20 | - name: Setup go 21 | uses: actions/setup-go@v5 22 | with: 23 | go-version-file: go.mod 24 | check-latest: true 25 | 26 | - name: Run GoReleaser 27 | uses: goreleaser/goreleaser-action@v5 28 | with: 29 | # either 'goreleaser' (default) or 'goreleaser-pro' 30 | distribution: goreleaser 31 | version: latest 32 | args: release --clean 33 | env: 34 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 35 | -------------------------------------------------------------------------------- /_example/producer-consumer/producer/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "log" 7 | "time" 8 | 9 | "github.com/golang-queue/nsq" 10 | "github.com/golang-queue/queue" 11 | ) 12 | 13 | type job struct { 14 | Message string 15 | } 16 | 17 | func (j *job) Bytes() []byte { 18 | b, err := json.Marshal(j) 19 | if err != nil { 20 | panic(err) 21 | } 22 | return b 23 | } 24 | 25 | func main() { 26 | taskN := 5 27 | 28 | // define the worker 29 | w := nsq.NewWorker( 30 | nsq.WithAddr("127.0.0.1:4150"), 31 | nsq.WithTopic("example"), 32 | nsq.WithChannel("foobar"), 33 | ) 34 | 35 | // define the queue 36 | q := queue.NewPool( 37 | 0, 38 | queue.WithWorker(w), 39 | ) 40 | 41 | // assign tasks in queue 42 | for i := 0; i < taskN; i++ { 43 | go func(i int) { 44 | if err := q.Queue(&job{ 45 | Message: fmt.Sprintf("handle the job: %d", i+1), 46 | }); err != nil { 47 | log.Fatal(err) 48 | } 49 | }(i) 50 | } 51 | 52 | time.Sleep(1 * time.Second) 53 | // shutdown the service and notify all the worker 54 | q.Release() 55 | } 56 | -------------------------------------------------------------------------------- /_example/producer-consumer/README.md: -------------------------------------------------------------------------------- 1 | # Producer-Consumer Example 2 | 3 | This example demonstrates how to use the `golang-queue` library with NSQ to implement a producer-consumer pattern. 4 | 5 | ## Prerequisites 6 | 7 | - Go 1.22 or later 8 | - [NSQ](https://nsq.io/) installed and running 9 | 10 | ## Running the Producer 11 | 12 | The producer sends tasks to the NSQ topic. 13 | 14 | Navigate to the producer directory: 15 | 16 | ```sh 17 | cd _example/producer-consumer/producer 18 | ``` 19 | 20 | Run the producer: 21 | 22 | ```sh 23 | go run main.go 24 | ``` 25 | 26 | ## Running the Consumer 27 | 28 | The consumer processes tasks from the NSQ topic. 29 | 30 | Navigate to the consumer directory: 31 | 32 | ```sh 33 | cd _example/producer-consumer/consumer 34 | ``` 35 | 36 | Run the consumer: 37 | 38 | ```sh 39 | go run main.go 40 | ``` 41 | 42 | ## Explanation 43 | 44 | - The producer creates a pool of workers and assigns tasks to the queue. 45 | - The consumer listens to the NSQ topic and processes the tasks. 46 | 47 | Ensure that NSQ is running and accessible at `127.0.0.1:4150` before starting the producer and consumer. 48 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 golang-queue 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 | -------------------------------------------------------------------------------- /_example/worker/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/nsq" 11 | "github.com/golang-queue/queue" 12 | "github.com/golang-queue/queue/core" 13 | ) 14 | 15 | type job struct { 16 | Message string 17 | } 18 | 19 | func (j *job) Bytes() []byte { 20 | b, err := json.Marshal(j) 21 | if err != nil { 22 | panic(err) 23 | } 24 | return b 25 | } 26 | 27 | func main() { 28 | taskN := 100 29 | rets := make(chan string, taskN) 30 | 31 | // define the worker 32 | w := nsq.NewWorker( 33 | nsq.WithAddr("127.0.0.1:4150"), 34 | nsq.WithTopic("example"), 35 | nsq.WithChannel("foobar"), 36 | // concurrent job number 37 | nsq.WithMaxInFlight(10), 38 | nsq.WithRunFunc(func(ctx context.Context, m core.TaskMessage) error { 39 | var v *job 40 | if err := json.Unmarshal(m.Payload(), &v); err != nil { 41 | return err 42 | } 43 | rets <- v.Message 44 | return nil 45 | }), 46 | ) 47 | 48 | // define the queue 49 | q := queue.NewPool( 50 | 10, 51 | queue.WithWorker(w), 52 | ) 53 | defer q.Release() 54 | 55 | // assign tasks in queue 56 | for i := 0; i < taskN; i++ { 57 | go func(i int) { 58 | if err := q.Queue(&job{ 59 | Message: fmt.Sprintf("handle the job: %d", i+1), 60 | }); err != nil { 61 | log.Fatal(err) 62 | } 63 | }(i) 64 | } 65 | 66 | // wait until all tasks done 67 | for i := 0; i < taskN; i++ { 68 | fmt.Println("message:", <-rets) 69 | time.Sleep(10 * time.Millisecond) 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: Run Testing 2 | on: push 3 | 4 | jobs: 5 | lint: 6 | runs-on: ubuntu-latest 7 | steps: 8 | - name: Setup go 9 | uses: actions/setup-go@v5 10 | with: 11 | go-version: "^1" 12 | - name: Checkout repository 13 | uses: actions/checkout@v4 14 | 15 | - name: Setup golangci-lint 16 | uses: golangci/golangci-lint-action@v6 17 | with: 18 | version: latest 19 | args: --verbose 20 | 21 | # Label of the container job 22 | test: 23 | strategy: 24 | matrix: 25 | os: [ubuntu-latest] 26 | go: [1.22, 1.23] 27 | include: 28 | - os: ubuntu-latest 29 | go-build: ~/.cache/go-build 30 | name: ${{ matrix.os }} @ Go ${{ matrix.go }} 31 | runs-on: ${{ matrix.os }} 32 | 33 | steps: 34 | - name: Set up Go ${{ matrix.go }} 35 | uses: actions/setup-go@v3 36 | with: 37 | go-version: ${{ matrix.go }} 38 | 39 | - name: Checkout Code 40 | uses: actions/checkout@v4 41 | with: 42 | ref: ${{ github.ref }} 43 | 44 | - uses: actions/cache@v4 45 | with: 46 | path: | 47 | ${{ matrix.go-build }} 48 | ~/go/pkg/mod 49 | key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} 50 | restore-keys: | 51 | ${{ runner.os }}-go- 52 | - name: Run Tests 53 | run: | 54 | go test -v -covermode=atomic -coverprofile=coverage.out 55 | 56 | - name: Upload coverage to Codecov 57 | uses: codecov/codecov-action@v5 58 | with: 59 | flags: ${{ matrix.os }},go-${{ matrix.go }} 60 | -------------------------------------------------------------------------------- /_example/producer-consumer/consumer/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "time" 8 | 9 | "github.com/golang-queue/nsq" 10 | "github.com/golang-queue/queue" 11 | "github.com/golang-queue/queue/core" 12 | 13 | "github.com/appleboy/graceful" 14 | ) 15 | 16 | type job struct { 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 := 10000 30 | rets := make(chan string, taskN) 31 | 32 | m := graceful.NewManager() 33 | 34 | // define the worker 35 | w := nsq.NewWorker( 36 | nsq.WithAddr("127.0.0.1:4150"), 37 | nsq.WithTopic("example"), 38 | nsq.WithChannel("foobar"), 39 | nsq.WithRunFunc(func(ctx context.Context, m core.TaskMessage) error { 40 | var v *job 41 | if err := json.Unmarshal(m.Payload(), &v); err != nil { 42 | return err 43 | } 44 | rets <- v.Message 45 | fmt.Println("got message:", v.Message) 46 | fmt.Println("wait 10 seconds") 47 | time.Sleep(10 * time.Second) 48 | return nil 49 | }), 50 | ) 51 | 52 | // define the queue 53 | q := queue.NewPool( 54 | 1, 55 | queue.WithWorker(w), 56 | ) 57 | 58 | m.AddRunningJob(func(ctx context.Context) error { 59 | for { 60 | select { 61 | case <-ctx.Done(): 62 | select { 63 | case m := <-rets: 64 | fmt.Println("message:", m) 65 | default: 66 | } 67 | return nil 68 | case m := <-rets: 69 | fmt.Println("message:", m) 70 | time.Sleep(50 * time.Millisecond) 71 | } 72 | } 73 | }) 74 | 75 | m.AddShutdownJob(func() error { 76 | // shutdown the service and notify all the worker 77 | q.Release() 78 | return nil 79 | }) 80 | 81 | <-m.Done() 82 | } 83 | -------------------------------------------------------------------------------- /.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: [main] 11 | pull_request: 12 | # The branches below must be a subset of the branches above 13 | branches: [main] 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@v3 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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /_example/worker/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/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 4 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 5 | github.com/golang-queue/queue v0.3.0 h1:gyBLNT9EDOsChazYScp8iLiwLfG0SdnCDmNUybcHig4= 6 | github.com/golang-queue/queue v0.3.0/go.mod h1:SkjMwz1TjxZOrF7kABvbar1CagcMxwRtXt5Tx00wb4g= 7 | github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= 8 | github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= 9 | github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= 10 | github.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA= 11 | github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= 12 | github.com/nsqio/go-nsq v1.1.0 h1:PQg+xxiUjA7V+TLdXw7nVrJ5Jbl3sN86EhGCQj4+FYE= 13 | github.com/nsqio/go-nsq v1.1.0/go.mod h1:vKq36oyeVXgsS5Q8YEO7WghqidAVXQlcFxzQbQTuDEY= 14 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 15 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 16 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 17 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 18 | go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= 19 | go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= 20 | go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU= 21 | go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM= 22 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 23 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 24 | -------------------------------------------------------------------------------- /_example/producer-consumer/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/appleboy/graceful v0.0.4 h1:Q4LCeq4DFy59qiACLtuH+mSqDERtUzwkQbCWpRaWwvQ= 4 | github.com/appleboy/graceful v0.0.4/go.mod h1:Q2mVx0t+N0lCDZc5MJudbcpTm6cgGM/J2gZCZIqD9dc= 5 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 6 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 7 | github.com/golang-queue/queue v0.3.0 h1:gyBLNT9EDOsChazYScp8iLiwLfG0SdnCDmNUybcHig4= 8 | github.com/golang-queue/queue v0.3.0/go.mod h1:SkjMwz1TjxZOrF7kABvbar1CagcMxwRtXt5Tx00wb4g= 9 | github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= 10 | github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= 11 | github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= 12 | github.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA= 13 | github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= 14 | github.com/nsqio/go-nsq v1.1.0 h1:PQg+xxiUjA7V+TLdXw7nVrJ5Jbl3sN86EhGCQj4+FYE= 15 | github.com/nsqio/go-nsq v1.1.0/go.mod h1:vKq36oyeVXgsS5Q8YEO7WghqidAVXQlcFxzQbQTuDEY= 16 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 17 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 18 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 19 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 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 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 25 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 26 | -------------------------------------------------------------------------------- /options.go: -------------------------------------------------------------------------------- 1 | package nsq 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/golang-queue/queue" 7 | "github.com/golang-queue/queue/core" 8 | 9 | nsq "github.com/nsqio/go-nsq" 10 | ) 11 | 12 | // An Option configures a mutex. 13 | type Option interface { 14 | Apply(*Options) 15 | } 16 | 17 | // OptionFunc is a function that configures a queue. 18 | type OptionFunc func(*Options) 19 | 20 | // Apply calls f(option) 21 | func (f OptionFunc) Apply(option *Options) { 22 | f(option) 23 | } 24 | 25 | type Options struct { 26 | maxInFlight int 27 | addr string 28 | topic string 29 | channel string 30 | runFunc func(context.Context, core.TaskMessage) error 31 | logger queue.Logger 32 | logLevel nsq.LogLevel 33 | } 34 | 35 | // WithAddr setup the addr of NSQ 36 | func WithAddr(addr string) Option { 37 | return OptionFunc(func(o *Options) { 38 | o.addr = addr 39 | }) 40 | } 41 | 42 | // WithTopic setup the topic of NSQ 43 | func WithTopic(topic string) Option { 44 | return OptionFunc(func(o *Options) { 45 | o.topic = topic 46 | }) 47 | } 48 | 49 | // WithChannel setup the channel of NSQ 50 | func WithChannel(channel string) Option { 51 | return OptionFunc(func(o *Options) { 52 | o.channel = channel 53 | }) 54 | } 55 | 56 | // WithRunFunc setup the run func of queue 57 | func WithRunFunc(fn func(context.Context, core.TaskMessage) error) Option { 58 | return OptionFunc(func(o *Options) { 59 | o.runFunc = fn 60 | }) 61 | } 62 | 63 | // WithMaxInFlight Maximum number of messages to allow in flight (concurrency knob) 64 | func WithMaxInFlight(num int) Option { 65 | return OptionFunc(func(o *Options) { 66 | o.maxInFlight = num 67 | }) 68 | } 69 | 70 | // WithLogger set custom logger 71 | func WithLogger(l queue.Logger) Option { 72 | return OptionFunc(func(o *Options) { 73 | o.logger = l 74 | }) 75 | } 76 | 77 | // WithLogLevel set custom [nsq] log level 78 | func WithLogLevel(lvl nsq.LogLevel) Option { 79 | return OptionFunc(func(o *Options) { 80 | o.logLevel = lvl 81 | }) 82 | } 83 | 84 | func newOptions(opts ...Option) Options { 85 | defaultOpts := Options{ 86 | addr: "127.0.0.1:4150", 87 | topic: "gorush", 88 | channel: "ch", 89 | maxInFlight: 1, 90 | 91 | logger: queue.NewLogger(), 92 | logLevel: nsq.LogLevelInfo, 93 | runFunc: func(context.Context, core.TaskMessage) error { 94 | return nil 95 | }, 96 | } 97 | 98 | // Loop through each option 99 | for _, opt := range opts { 100 | // Call the option giving the instantiated 101 | opt.Apply(&defaultOpts) 102 | } 103 | 104 | return defaultOpts 105 | } 106 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # NSQ 2 | 3 | [![Run Testing](https://github.com/golang-queue/nsq/actions/workflows/go.yml/badge.svg)](https://github.com/golang-queue/nsq/actions/workflows/go.yml) 4 | [![Go Report Card](https://goreportcard.com/badge/github.com/golang-queue/queue)](https://goreportcard.com/report/github.com/golang-queue/queue) 5 | [![codecov](https://codecov.io/gh/golang-queue/nsq/branch/main/graph/badge.svg?token=D3CUES8M62)](https://codecov.io/gh/golang-queue/nsq) 6 | 7 | NSQ as backend with [Queue package](https://github.com/golang-queue/queue) (A realtime distributed messaging platform) 8 | 9 | ![screen](./images/screen.png) 10 | 11 | ## Setup 12 | 13 | start the NSQ lookupd 14 | 15 | ```sh 16 | nsqlookupd 17 | ``` 18 | 19 | start the NSQ server 20 | 21 | ```sh 22 | nsqd --lookupd-tcp-address=localhost:4160 23 | ``` 24 | 25 | start the NSQ admin dashboard 26 | 27 | ```sh 28 | nsqadmin --lookupd-http-address localhost:4161 29 | ``` 30 | 31 | ## Testing 32 | 33 | ```sh 34 | go test -v ./... 35 | ``` 36 | 37 | ## Example 38 | 39 | ```go 40 | package main 41 | 42 | import ( 43 | "context" 44 | "encoding/json" 45 | "fmt" 46 | "log" 47 | "time" 48 | 49 | "github.com/golang-queue/nsq" 50 | "github.com/golang-queue/queue" 51 | "github.com/golang-queue/queue/core" 52 | ) 53 | 54 | type job struct { 55 | Message string 56 | } 57 | 58 | func (j *job) Bytes() []byte { 59 | b, err := json.Marshal(j) 60 | if err != nil { 61 | panic(err) 62 | } 63 | return b 64 | } 65 | 66 | func main() { 67 | taskN := 100 68 | rets := make(chan string, taskN) 69 | 70 | // define the worker 71 | w := nsq.NewWorker( 72 | nsq.WithAddr("127.0.0.1:4150"), 73 | nsq.WithTopic("example"), 74 | nsq.WithChannel("foobar"), 75 | // concurrent job number 76 | nsq.WithMaxInFlight(10), 77 | nsq.WithRunFunc(func(ctx context.Context, m core.TaskMessage) error { 78 | var v *job 79 | if err := json.Unmarshal(m.Payload(), &v); err != nil { 80 | return err 81 | } 82 | rets <- v.Message 83 | return nil 84 | }), 85 | ) 86 | 87 | // define the queue 88 | q := queue.NewPool( 89 | 10, 90 | queue.WithWorker(w), 91 | ) 92 | defer q.Release() 93 | 94 | // assign tasks in queue 95 | for i := 0; i < taskN; i++ { 96 | go func(i int) { 97 | if err := q.Queue(&job{ 98 | Message: fmt.Sprintf("handle the job: %d", i+1), 99 | }); err != nil { 100 | log.Fatal(err) 101 | } 102 | }(i) 103 | } 104 | 105 | // wait until all tasks done 106 | for i := 0; i < taskN; i++ { 107 | fmt.Println("message:", <-rets) 108 | time.Sleep(10 * time.Millisecond) 109 | } 110 | } 111 | ``` 112 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/golang-queue/nsq 2 | 3 | go 1.22 4 | 5 | require ( 6 | github.com/golang-queue/queue v0.3.0 7 | github.com/nsqio/go-nsq v1.1.0 8 | github.com/stretchr/testify v1.10.0 9 | github.com/testcontainers/testcontainers-go v0.35.0 10 | go.uber.org/goleak v1.3.0 11 | ) 12 | 13 | require ( 14 | dario.cat/mergo v1.0.0 // indirect 15 | github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect 16 | github.com/Microsoft/go-winio v0.6.2 // indirect 17 | github.com/cenkalti/backoff/v4 v4.2.1 // indirect 18 | github.com/containerd/containerd v1.7.18 // indirect 19 | github.com/containerd/log v0.1.0 // indirect 20 | github.com/containerd/platforms v0.2.1 // indirect 21 | github.com/cpuguy83/dockercfg v0.3.2 // indirect 22 | github.com/davecgh/go-spew v1.1.1 // indirect 23 | github.com/distribution/reference v0.6.0 // indirect 24 | github.com/docker/docker v27.1.1+incompatible // indirect 25 | github.com/docker/go-connections v0.5.0 // indirect 26 | github.com/docker/go-units v0.5.0 // indirect 27 | github.com/felixge/httpsnoop v1.0.4 // indirect 28 | github.com/go-logr/logr v1.4.1 // indirect 29 | github.com/go-logr/stdr v1.2.2 // indirect 30 | github.com/go-ole/go-ole v1.2.6 // indirect 31 | github.com/gogo/protobuf v1.3.2 // indirect 32 | github.com/golang/snappy v0.0.4 // indirect 33 | github.com/google/uuid v1.6.0 // indirect 34 | github.com/jpillora/backoff v1.0.0 // indirect 35 | github.com/klauspost/compress v1.17.4 // indirect 36 | github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect 37 | github.com/magiconair/properties v1.8.7 // indirect 38 | github.com/moby/docker-image-spec v1.3.1 // indirect 39 | github.com/moby/patternmatcher v0.6.0 // indirect 40 | github.com/moby/sys/sequential v0.5.0 // indirect 41 | github.com/moby/sys/user v0.1.0 // indirect 42 | github.com/moby/term v0.5.0 // indirect 43 | github.com/morikuni/aec v1.0.0 // indirect 44 | github.com/opencontainers/go-digest v1.0.0 // indirect 45 | github.com/opencontainers/image-spec v1.1.0 // indirect 46 | github.com/pkg/errors v0.9.1 // indirect 47 | github.com/pmezard/go-difflib v1.0.0 // indirect 48 | github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect 49 | github.com/shirou/gopsutil/v3 v3.23.12 // indirect 50 | github.com/shoenig/go-m1cpu v0.1.6 // indirect 51 | github.com/sirupsen/logrus v1.9.3 // indirect 52 | github.com/tklauser/go-sysconf v0.3.12 // indirect 53 | github.com/tklauser/numcpus v0.6.1 // indirect 54 | github.com/yusufpapurcu/wmi v1.2.3 // indirect 55 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect 56 | go.opentelemetry.io/otel v1.24.0 // indirect 57 | go.opentelemetry.io/otel/metric v1.24.0 // indirect 58 | go.opentelemetry.io/otel/trace v1.24.0 // indirect 59 | golang.org/x/crypto v0.31.0 // indirect 60 | golang.org/x/sys v0.28.0 // indirect 61 | gopkg.in/yaml.v3 v3.0.1 // indirect 62 | ) 63 | -------------------------------------------------------------------------------- /nsq.go: -------------------------------------------------------------------------------- 1 | package nsq 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "sync" 7 | "sync/atomic" //nolint:typecheck,nolintlint 8 | "time" 9 | 10 | "github.com/golang-queue/queue" 11 | "github.com/golang-queue/queue/core" 12 | "github.com/golang-queue/queue/job" 13 | 14 | nsq "github.com/nsqio/go-nsq" 15 | ) 16 | 17 | var _ core.Worker = (*Worker)(nil) 18 | 19 | // Worker for NSQ 20 | type Worker struct { 21 | q *nsq.Consumer 22 | p *nsq.Producer 23 | cfg *nsq.Config 24 | stopOnce sync.Once 25 | startOnce sync.Once 26 | stop chan struct{} 27 | stopFlag int32 28 | opts Options 29 | tasks chan *nsq.Message 30 | } 31 | 32 | // NewWorker for struc 33 | func NewWorker(opts ...Option) *Worker { 34 | w := &Worker{ 35 | opts: newOptions(opts...), 36 | stop: make(chan struct{}), 37 | tasks: make(chan *nsq.Message), 38 | } 39 | 40 | w.cfg = nsq.NewConfig() 41 | w.cfg.MaxInFlight = w.opts.maxInFlight 42 | 43 | if err := w.startProducer(); err != nil { 44 | panic(err) 45 | } 46 | 47 | w.p.SetLoggerLevel(w.opts.logLevel) 48 | 49 | return w 50 | } 51 | 52 | func (w *Worker) startProducer() error { 53 | var err error 54 | 55 | w.p, err = nsq.NewProducer(w.opts.addr, w.cfg) 56 | 57 | return err 58 | } 59 | 60 | func (w *Worker) startConsumer() (err error) { 61 | w.startOnce.Do(func() { 62 | w.q, err = nsq.NewConsumer(w.opts.topic, w.opts.channel, w.cfg) 63 | if err != nil { 64 | return 65 | } 66 | 67 | w.q.SetLoggerLevel(w.opts.logLevel) 68 | w.q.AddHandler(nsq.HandlerFunc(func(msg *nsq.Message) error { 69 | if len(msg.Body) == 0 { 70 | // Returning nil will automatically send a FIN command to NSQ to mark the message as processed. 71 | // In this case, a message with an empty body is simply ignored/discarded. 72 | return nil 73 | } 74 | 75 | loop: 76 | for { 77 | select { 78 | case w.tasks <- msg: 79 | break loop 80 | case <-w.stop: 81 | if msg != nil { 82 | // re-queue the job if worker has been shutdown. 83 | msg.Requeue(-1) 84 | } 85 | break loop 86 | case <-time.After(2 * time.Second): 87 | msg.Touch() 88 | } 89 | } 90 | 91 | return nil 92 | })) 93 | 94 | err = w.q.ConnectToNSQD(w.opts.addr) 95 | if err != nil { 96 | return 97 | } 98 | }) 99 | 100 | return err 101 | } 102 | 103 | // Run start the worker 104 | func (w *Worker) Run(ctx context.Context, task core.TaskMessage) error { 105 | return w.opts.runFunc(ctx, task) 106 | } 107 | 108 | // Shutdown worker 109 | func (w *Worker) Shutdown() error { 110 | if !atomic.CompareAndSwapInt32(&w.stopFlag, 0, 1) { 111 | return queue.ErrQueueShutdown 112 | } 113 | 114 | w.stopOnce.Do(func() { 115 | // notify shtdown event to worker and consumer 116 | close(w.stop) 117 | // stop producer and consumer 118 | if w.q != nil { 119 | w.q.ChangeMaxInFlight(0) 120 | w.q.Stop() 121 | <-w.q.StopChan 122 | } 123 | w.p.Stop() 124 | 125 | // close task channel 126 | close(w.tasks) 127 | }) 128 | return nil 129 | } 130 | 131 | // Queue send notification to queue 132 | func (w *Worker) Queue(job core.TaskMessage) error { 133 | if atomic.LoadInt32(&w.stopFlag) == 1 { 134 | return queue.ErrQueueShutdown 135 | } 136 | 137 | return w.p.Publish(w.opts.topic, job.Bytes()) 138 | } 139 | 140 | // Request fetch new task from queue 141 | func (w *Worker) Request() (core.TaskMessage, error) { 142 | if err := w.startConsumer(); err != nil { 143 | return nil, err 144 | } 145 | 146 | clock := 0 147 | loop: 148 | for { 149 | select { 150 | case task, ok := <-w.tasks: 151 | if !ok { 152 | return nil, queue.ErrQueueHasBeenClosed 153 | } 154 | var data job.Message 155 | _ = json.Unmarshal(task.Body, &data) 156 | return &data, nil 157 | case <-time.After(1 * time.Second): 158 | if clock == 5 { 159 | break loop 160 | } 161 | clock += 1 162 | } 163 | } 164 | 165 | return nil, queue.ErrNoTaskInQueue 166 | } 167 | 168 | // Stats retrieves the current connection and message statistics for a Consumer 169 | func (w *Worker) Stats() *nsq.ConsumerStats { 170 | if w.q == nil { 171 | return nil 172 | } 173 | 174 | return w.q.Stats() 175 | } 176 | -------------------------------------------------------------------------------- /nsq_test.go: -------------------------------------------------------------------------------- 1 | package nsq 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "log" 8 | "runtime" 9 | "testing" 10 | "time" 11 | 12 | "github.com/golang-queue/queue" 13 | "github.com/golang-queue/queue/core" 14 | "github.com/golang-queue/queue/job" 15 | "github.com/testcontainers/testcontainers-go" 16 | "github.com/testcontainers/testcontainers-go/wait" 17 | 18 | "github.com/stretchr/testify/assert" 19 | "github.com/stretchr/testify/require" 20 | "go.uber.org/goleak" 21 | ) 22 | 23 | func TestMain(m *testing.M) { 24 | goleak.VerifyTestMain(m) 25 | } 26 | 27 | type mockMessage struct { 28 | Message string 29 | } 30 | 31 | func (m mockMessage) Bytes() []byte { 32 | return []byte(m.Message) 33 | } 34 | 35 | func (m mockMessage) Payload() []byte { 36 | return []byte(m.Message) 37 | } 38 | 39 | func setupNSQContainer(ctx context.Context, t *testing.T) (testcontainers.Container, string) { 40 | req := testcontainers.ContainerRequest{ 41 | Image: "nsqio/nsq:v1.3.0", 42 | ExposedPorts: []string{ 43 | "4150/tcp", // nsqd port 44 | "4151/tcp", // http port 45 | }, 46 | WaitingFor: wait.ForLog("TCP: listening on"), 47 | Cmd: []string{"nsqd"}, 48 | } 49 | nsqC, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ 50 | ContainerRequest: req, 51 | Started: true, 52 | }) 53 | require.NoError(t, err) 54 | 55 | endpoint, err := nsqC.Endpoint(ctx, "") 56 | require.NoError(t, err) 57 | 58 | return nsqC, endpoint 59 | } 60 | 61 | func TestNSQDefaultFlow(t *testing.T) { 62 | ctx := context.Background() 63 | natsC, endpoint := setupNSQContainer(ctx, t) 64 | defer testcontainers.CleanupContainer(t, natsC) 65 | m := &mockMessage{ 66 | Message: "foo", 67 | } 68 | w := NewWorker( 69 | WithAddr(endpoint), 70 | WithTopic("test1"), 71 | WithChannel("test1"), 72 | ) 73 | q, err := queue.NewQueue( 74 | queue.WithWorker(w), 75 | queue.WithWorkerCount(2), 76 | ) 77 | assert.NoError(t, err) 78 | q.Start() 79 | assert.NoError(t, q.Queue(m)) 80 | m.Message = "bar" 81 | assert.NoError(t, q.Queue(m)) 82 | time.Sleep(1 * time.Second) 83 | q.Release() 84 | } 85 | 86 | func TestNSQShutdown(t *testing.T) { 87 | ctx := context.Background() 88 | natsC, endpoint := setupNSQContainer(ctx, t) 89 | defer testcontainers.CleanupContainer(t, natsC) 90 | w := NewWorker( 91 | WithAddr(endpoint), 92 | WithTopic("test2"), 93 | ) 94 | q, err := queue.NewQueue( 95 | queue.WithWorker(w), 96 | queue.WithWorkerCount(2), 97 | ) 98 | assert.NoError(t, err) 99 | q.Start() 100 | time.Sleep(1 * time.Second) 101 | q.Shutdown() 102 | // check shutdown once 103 | assert.Error(t, w.Shutdown()) 104 | assert.Equal(t, queue.ErrQueueShutdown, w.Shutdown()) 105 | q.Wait() 106 | } 107 | 108 | func TestNSQCustomFuncAndWait(t *testing.T) { 109 | ctx := context.Background() 110 | natsC, endpoint := setupNSQContainer(ctx, t) 111 | defer testcontainers.CleanupContainer(t, natsC) 112 | m := &mockMessage{ 113 | Message: "foo", 114 | } 115 | w := NewWorker( 116 | WithAddr(endpoint), 117 | WithTopic("test3"), 118 | WithMaxInFlight(10), 119 | WithRunFunc(func(ctx context.Context, m core.TaskMessage) error { 120 | time.Sleep(500 * time.Millisecond) 121 | return nil 122 | }), 123 | ) 124 | q, err := queue.NewQueue( 125 | queue.WithWorker(w), 126 | queue.WithWorkerCount(10), 127 | ) 128 | assert.NoError(t, err) 129 | q.Start() 130 | time.Sleep(100 * time.Millisecond) 131 | assert.NoError(t, q.Queue(m)) 132 | assert.NoError(t, q.Queue(m)) 133 | assert.NoError(t, q.Queue(m)) 134 | assert.NoError(t, q.Queue(m)) 135 | time.Sleep(1000 * time.Millisecond) 136 | q.Release() 137 | // you will see the execute time > 1000ms 138 | } 139 | 140 | func TestEnqueueJobAfterShutdown(t *testing.T) { 141 | ctx := context.Background() 142 | natsC, endpoint := setupNSQContainer(ctx, t) 143 | defer testcontainers.CleanupContainer(t, natsC) 144 | m := mockMessage{ 145 | Message: "foo", 146 | } 147 | w := NewWorker( 148 | WithAddr(endpoint), 149 | ) 150 | q, err := queue.NewQueue( 151 | queue.WithWorker(w), 152 | queue.WithWorkerCount(2), 153 | ) 154 | assert.NoError(t, err) 155 | q.Start() 156 | time.Sleep(400 * time.Millisecond) 157 | q.Shutdown() 158 | // can't queue task after shutdown 159 | err = q.Queue(m) 160 | assert.Error(t, err) 161 | assert.Equal(t, queue.ErrQueueShutdown, err) 162 | q.Wait() 163 | } 164 | 165 | func TestJobReachTimeout(t *testing.T) { 166 | ctx := context.Background() 167 | natsC, endpoint := setupNSQContainer(ctx, t) 168 | defer testcontainers.CleanupContainer(t, natsC) 169 | m := mockMessage{ 170 | Message: "foo", 171 | } 172 | w := NewWorker( 173 | WithAddr(endpoint), 174 | WithTopic("timeout"), 175 | WithMaxInFlight(2), 176 | WithRunFunc(func(ctx context.Context, m core.TaskMessage) error { 177 | for { 178 | select { 179 | case <-ctx.Done(): 180 | log.Println("get data:", string(m.Payload())) 181 | if errors.Is(ctx.Err(), context.Canceled) { 182 | log.Println("queue has been shutdown and cancel the job") 183 | } else if errors.Is(ctx.Err(), context.DeadlineExceeded) { 184 | log.Println("job deadline exceeded") 185 | } 186 | return nil 187 | default: 188 | } 189 | time.Sleep(50 * time.Millisecond) 190 | } 191 | }), 192 | ) 193 | q, err := queue.NewQueue( 194 | queue.WithWorker(w), 195 | queue.WithWorkerCount(2), 196 | ) 197 | assert.NoError(t, err) 198 | q.Start() 199 | time.Sleep(400 * time.Millisecond) 200 | assert.NoError(t, q.Queue(m, job.AllowOption{ 201 | Timeout: job.Time(20 * time.Millisecond), 202 | })) 203 | time.Sleep(2 * time.Second) 204 | q.Release() 205 | } 206 | 207 | func TestCancelJobAfterShutdown(t *testing.T) { 208 | ctx := context.Background() 209 | natsC, endpoint := setupNSQContainer(ctx, t) 210 | defer testcontainers.CleanupContainer(t, natsC) 211 | m := mockMessage{ 212 | Message: "test", 213 | } 214 | w := NewWorker( 215 | WithAddr(endpoint), 216 | WithTopic("cancel"), 217 | WithLogger(queue.NewLogger()), 218 | WithRunFunc(func(ctx context.Context, m core.TaskMessage) error { 219 | for { 220 | select { 221 | case <-ctx.Done(): 222 | log.Println("get data:", string(m.Payload())) 223 | if errors.Is(ctx.Err(), context.Canceled) { 224 | log.Println("queue has been shutdown and cancel the job") 225 | } else if errors.Is(ctx.Err(), context.DeadlineExceeded) { 226 | log.Println("job deadline exceeded") 227 | } 228 | return nil 229 | default: 230 | } 231 | time.Sleep(50 * time.Millisecond) 232 | } 233 | }), 234 | ) 235 | q, err := queue.NewQueue( 236 | queue.WithWorker(w), 237 | queue.WithWorkerCount(2), 238 | ) 239 | assert.NoError(t, err) 240 | q.Start() 241 | time.Sleep(400 * time.Millisecond) 242 | assert.NoError(t, q.Queue(m, job.AllowOption{ 243 | Timeout: job.Time(3 * time.Second), 244 | })) 245 | time.Sleep(2 * time.Second) 246 | q.Release() 247 | } 248 | 249 | func TestGoroutineLeak(t *testing.T) { 250 | ctx := context.Background() 251 | natsC, endpoint := setupNSQContainer(ctx, t) 252 | defer testcontainers.CleanupContainer(t, natsC) 253 | m := mockMessage{ 254 | Message: "foo", 255 | } 256 | w := NewWorker( 257 | WithAddr(endpoint), 258 | WithTopic("GoroutineLeak"), 259 | WithLogger(queue.NewEmptyLogger()), 260 | WithRunFunc(func(ctx context.Context, m core.TaskMessage) error { 261 | for { 262 | select { 263 | case <-ctx.Done(): 264 | log.Println("get data:", string(m.Payload())) 265 | if errors.Is(ctx.Err(), context.Canceled) { 266 | log.Println("queue has been shutdown and cancel the job") 267 | } else if errors.Is(ctx.Err(), context.DeadlineExceeded) { 268 | log.Println("job deadline exceeded") 269 | } 270 | return nil 271 | default: 272 | log.Println("get data:", string(m.Payload())) 273 | time.Sleep(50 * time.Millisecond) 274 | return nil 275 | } 276 | } 277 | }), 278 | ) 279 | q, err := queue.NewQueue( 280 | queue.WithLogger(queue.NewEmptyLogger()), 281 | queue.WithWorker(w), 282 | queue.WithWorkerCount(10), 283 | ) 284 | assert.NoError(t, err) 285 | q.Start() 286 | time.Sleep(400 * time.Millisecond) 287 | for i := 0; i < 500; i++ { 288 | m.Message = fmt.Sprintf("foobar: %d", i+1) 289 | assert.NoError(t, q.Queue(m)) 290 | } 291 | time.Sleep(2 * time.Second) 292 | q.Release() 293 | time.Sleep(2 * time.Second) 294 | fmt.Println("number of goroutines:", runtime.NumGoroutine()) 295 | } 296 | 297 | func TestGoroutinePanic(t *testing.T) { 298 | ctx := context.Background() 299 | natsC, endpoint := setupNSQContainer(ctx, t) 300 | defer testcontainers.CleanupContainer(t, natsC) 301 | m := mockMessage{ 302 | Message: "foo", 303 | } 304 | w := NewWorker( 305 | WithAddr(endpoint), 306 | WithTopic("GoroutinePanic"), 307 | WithRunFunc(func(ctx context.Context, m core.TaskMessage) error { 308 | panic("missing something") 309 | }), 310 | ) 311 | q, err := queue.NewQueue( 312 | queue.WithWorker(w), 313 | queue.WithWorkerCount(2), 314 | ) 315 | assert.NoError(t, err) 316 | q.Start() 317 | time.Sleep(400 * time.Millisecond) 318 | assert.NoError(t, q.Queue(m)) 319 | assert.NoError(t, q.Queue(m)) 320 | time.Sleep(2 * time.Second) 321 | q.Shutdown() 322 | assert.Error(t, q.Queue(m)) 323 | q.Wait() 324 | } 325 | 326 | func TestNSQStatsinQueue(t *testing.T) { 327 | ctx := context.Background() 328 | natsC, endpoint := setupNSQContainer(ctx, t) 329 | defer testcontainers.CleanupContainer(t, natsC) 330 | m := mockMessage{ 331 | Message: "foo", 332 | } 333 | w := NewWorker( 334 | WithAddr(endpoint), 335 | WithTopic("nsq_stats"), 336 | WithRunFunc(func(ctx context.Context, m core.TaskMessage) error { 337 | log.Println("get message") 338 | return nil 339 | }), 340 | ) 341 | q, err := queue.NewQueue( 342 | queue.WithWorker(w), 343 | queue.WithWorkerCount(1), 344 | ) 345 | assert.NoError(t, err) 346 | assert.NoError(t, q.Queue(m)) 347 | assert.NoError(t, q.Queue(m)) 348 | q.Start() 349 | time.Sleep(200 * time.Millisecond) 350 | assert.Equal(t, int(1), w.Stats().Connections) 351 | time.Sleep(500 * time.Millisecond) 352 | assert.Equal(t, uint64(2), w.Stats().MessagesReceived) 353 | assert.Equal(t, uint64(2), w.Stats().MessagesFinished) 354 | q.Release() 355 | assert.Equal(t, int(0), w.Stats().Connections) 356 | } 357 | 358 | func TestNSQStatsInWorker(t *testing.T) { 359 | ctx := context.Background() 360 | natsC, endpoint := setupNSQContainer(ctx, t) 361 | defer testcontainers.CleanupContainer(t, natsC) 362 | m := mockMessage{ 363 | Message: "foo", 364 | } 365 | w := NewWorker( 366 | WithAddr(endpoint), 367 | WithTopic("nsq_stats_queue"), 368 | ) 369 | 370 | assert.Equal(t, int(0), len(w.tasks)) 371 | assert.NoError(t, w.Queue(m)) 372 | assert.NoError(t, w.Queue(m)) 373 | assert.NoError(t, w.Queue(m)) 374 | assert.Nil(t, w.Stats()) 375 | 376 | task, err := w.Request() 377 | assert.Equal(t, int(1), w.Stats().Connections) 378 | assert.NotNil(t, task) 379 | assert.NoError(t, err) 380 | 381 | assert.Equal(t, uint64(1), w.Stats().MessagesReceived) 382 | assert.Equal(t, uint64(1), w.Stats().MessagesFinished) 383 | assert.Equal(t, uint64(0), w.Stats().MessagesRequeued) 384 | time.Sleep(50 * time.Millisecond) 385 | _ = w.Shutdown() 386 | time.Sleep(50 * time.Millisecond) 387 | assert.Equal(t, uint64(1), w.Stats().MessagesRequeued) 388 | } 389 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= 2 | dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= 3 | github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 h1:bvDV9vkmnHYOMsOr4WLk+Vo07yKIzd94sVoIqshQ4bU= 4 | github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8= 5 | github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8= 6 | github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= 7 | github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= 8 | github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= 9 | github.com/appleboy/com v0.2.1 h1:dHAHauX3eYDuheAahI83HIGFxpi0SEb2ZAu9EZ9hbUM= 10 | github.com/appleboy/com v0.2.1/go.mod h1:kByEI3/vzI5GM1+O5QdBHLsXaOsmFsJcOpCSgASi4sg= 11 | github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM= 12 | github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= 13 | github.com/containerd/containerd v1.7.18 h1:jqjZTQNfXGoEaZdW1WwPU0RqSn1Bm2Ay/KJPUuO8nao= 14 | github.com/containerd/containerd v1.7.18/go.mod h1:IYEk9/IO6wAPUz2bCMVUbsfXjzw5UNP5fLz4PsUygQ4= 15 | github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= 16 | github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= 17 | github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A= 18 | github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw= 19 | github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA= 20 | github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= 21 | github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= 22 | github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= 23 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 24 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 25 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 26 | github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= 27 | github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= 28 | github.com/docker/docker v27.1.1+incompatible h1:hO/M4MtV36kzKldqnA37IWhebRA+LnqqcqDja6kVaKY= 29 | github.com/docker/docker v27.1.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= 30 | github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= 31 | github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= 32 | github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= 33 | github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= 34 | github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= 35 | github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= 36 | github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= 37 | github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= 38 | github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 39 | github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= 40 | github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= 41 | github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= 42 | github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= 43 | github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= 44 | github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= 45 | github.com/golang-queue/queue v0.3.0 h1:gyBLNT9EDOsChazYScp8iLiwLfG0SdnCDmNUybcHig4= 46 | github.com/golang-queue/queue v0.3.0/go.mod h1:SkjMwz1TjxZOrF7kABvbar1CagcMxwRtXt5Tx00wb4g= 47 | github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= 48 | github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= 49 | github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= 50 | github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 51 | github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 52 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 53 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 54 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 55 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 56 | github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0 h1:YBftPWNWd4WwGqtY2yeZL2ef8rHAxPBD8KFhJpmcqms= 57 | github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0/go.mod h1:YN5jB8ie0yfIUg6VvR9Kz84aCaG7AsGZnLjhHbUqwPg= 58 | github.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA= 59 | github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= 60 | github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= 61 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 62 | github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4= 63 | github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM= 64 | github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= 65 | github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= 66 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 67 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 68 | github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= 69 | github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= 70 | github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= 71 | github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= 72 | github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= 73 | github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= 74 | github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk= 75 | github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc= 76 | github.com/moby/sys/sequential v0.5.0 h1:OPvI35Lzn9K04PBbCLW0g4LcFAJgHsvXsRyewg5lXtc= 77 | github.com/moby/sys/sequential v0.5.0/go.mod h1:tH2cOOs5V9MlPiXcQzRC+eEyab644PWKGRYaaV5ZZlo= 78 | github.com/moby/sys/user v0.1.0 h1:WmZ93f5Ux6het5iituh9x2zAG7NFY9Aqi49jjE1PaQg= 79 | github.com/moby/sys/user v0.1.0/go.mod h1:fKJhFOnsCN6xZ5gSfbM6zaHGgDJMrqt9/reuj4T7MmU= 80 | github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= 81 | github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= 82 | github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= 83 | github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= 84 | github.com/nsqio/go-nsq v1.1.0 h1:PQg+xxiUjA7V+TLdXw7nVrJ5Jbl3sN86EhGCQj4+FYE= 85 | github.com/nsqio/go-nsq v1.1.0/go.mod h1:vKq36oyeVXgsS5Q8YEO7WghqidAVXQlcFxzQbQTuDEY= 86 | github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= 87 | github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= 88 | github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= 89 | github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= 90 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 91 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 92 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 93 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 94 | github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw= 95 | github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= 96 | github.com/rogpeppe/go-internal v1.8.1 h1:geMPLpDpQOgVyCg5z5GoRwLHepNdb71NXb67XFkP+Eg= 97 | github.com/rogpeppe/go-internal v1.8.1/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4nPKWu0nJ5d+o= 98 | github.com/shirou/gopsutil/v3 v3.23.12 h1:z90NtUkp3bMtmICZKpC4+WaknU1eXtp5vtbQ11DgpE4= 99 | github.com/shirou/gopsutil/v3 v3.23.12/go.mod h1:1FrWgea594Jp7qmjHUUPlJDTPgcsb9mGnXDxavtikzM= 100 | github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM= 101 | github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ= 102 | github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU= 103 | github.com/shoenig/test v0.6.4/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k= 104 | github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= 105 | github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= 106 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 107 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 108 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 109 | github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= 110 | github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= 111 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 112 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 113 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 114 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 115 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 116 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 117 | github.com/testcontainers/testcontainers-go v0.35.0 h1:uADsZpTKFAtp8SLK+hMwSaa+X+JiERHtd4sQAFmXeMo= 118 | github.com/testcontainers/testcontainers-go v0.35.0/go.mod h1:oEVBj5zrfJTrgjwONs1SsRbnBtH9OKl+IGl3UMcr2B4= 119 | github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU= 120 | github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI= 121 | github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk= 122 | github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY= 123 | github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 124 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 125 | github.com/yusufpapurcu/wmi v1.2.3 h1:E1ctvB7uKFMOJw3fdOW32DwGE9I7t++CRUEMKvFoFiw= 126 | github.com/yusufpapurcu/wmi v1.2.3/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= 127 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 h1:jq9TW8u3so/bN+JPT166wjOI6/vQPF6Xe7nMNIltagk= 128 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0/go.mod h1:p8pYQP+m5XfbZm9fxtSKAbM6oIllS7s2AfxrChvc7iw= 129 | go.opentelemetry.io/otel v1.24.0 h1:0LAOdjNmQeSTzGBzduGe/rU4tZhMwL5rWgtp9Ku5Jfo= 130 | go.opentelemetry.io/otel v1.24.0/go.mod h1:W7b9Ozg4nkF5tWI5zsXkaKKDjdVjpD4oAt9Qi/MArHo= 131 | go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0 h1:Mne5On7VWdx7omSrSSZvM4Kw7cS7NQkOOmLcgscI51U= 132 | go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0/go.mod h1:IPtUMKL4O3tH5y+iXVyAXqpAwMuzC1IrxVS81rummfE= 133 | go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0 h1:IeMeyr1aBvBiPVYihXIaeIZba6b8E1bYp7lbdxK8CQg= 134 | go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0/go.mod h1:oVdCUtjq9MK9BlS7TtucsQwUcXcymNiEDjgDD2jMtZU= 135 | go.opentelemetry.io/otel/metric v1.24.0 h1:6EhoGWWK28x1fbpA4tYTOWBkPefTDQnb8WSGXlc88kI= 136 | go.opentelemetry.io/otel/metric v1.24.0/go.mod h1:VYhLe1rFfxuTXLgj4CBiyz+9WYBA8pNGJgDcSFRKBco= 137 | go.opentelemetry.io/otel/sdk v1.19.0 h1:6USY6zH+L8uMH8L3t1enZPR3WFEmSTADlqldyHtJi3o= 138 | go.opentelemetry.io/otel/sdk v1.19.0/go.mod h1:NedEbbS4w3C6zElbLdPJKOpJQOrGUJ+GfzpjUvI0v1A= 139 | go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y1YELI= 140 | go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU= 141 | go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lIVU/I= 142 | go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM= 143 | go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= 144 | go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= 145 | go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU= 146 | go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM= 147 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 148 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 149 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 150 | golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= 151 | golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= 152 | golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 153 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 154 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 155 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 156 | golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 157 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 158 | golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs= 159 | golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= 160 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 161 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 162 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 163 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 164 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 165 | golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 166 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 167 | golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 168 | golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 169 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 170 | golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 171 | golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 172 | golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 173 | golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= 174 | golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 175 | golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q= 176 | golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= 177 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 178 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 179 | golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= 180 | golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= 181 | golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 h1:vVKdlvoWBphwdxWKrFZEuM0kGgGLxUOYcY4U/2Vjg44= 182 | golang.org/x/time v0.0.0-20220210224613-90d013bbcef8/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 183 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 184 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 185 | golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 186 | golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 187 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 188 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 189 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 190 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 191 | google.golang.org/genproto v0.0.0-20230920204549-e6e6cdab5c13 h1:vlzZttNJGVqTsRFU9AmdnrcO1Znh8Ew9kCD//yjigk0= 192 | google.golang.org/genproto/googleapis/api v0.0.0-20230913181813-007df8e322eb h1:lK0oleSc7IQsUxO3U5TjL9DWlsxpEBemh+zpB7IqhWI= 193 | google.golang.org/genproto/googleapis/api v0.0.0-20230913181813-007df8e322eb/go.mod h1:KjSP20unUpOx5kyQUFa7k4OJg0qeJ7DEZflGDu2p6Bk= 194 | google.golang.org/genproto/googleapis/rpc v0.0.0-20231002182017-d307bd883b97 h1:6GQBEOdGkX6MMTLT9V+TjtIRZCw9VPD5Z+yHY9wMgS0= 195 | google.golang.org/genproto/googleapis/rpc v0.0.0-20231002182017-d307bd883b97/go.mod h1:v7nGkzlmW8P3n/bKmWBn2WpBjpOEx8Q6gMueudAmKfY= 196 | google.golang.org/grpc v1.64.1 h1:LKtvyfbX3UGVPFcGqJ9ItpVWW6oN/2XqTxfAnwRRXiA= 197 | google.golang.org/grpc v1.64.1/go.mod h1:hiQF4LFZelK2WKaP6W0L92zGHtiQdZxk8CrSdvyjeP0= 198 | google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= 199 | google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= 200 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 201 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 202 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 203 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 204 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 205 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 206 | gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU= 207 | gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU= 208 | --------------------------------------------------------------------------------