├── internal ├── app │ ├── notify │ │ ├── queue_supplier │ │ │ ├── rabbitmq_test.go │ │ │ ├── kafka.go │ │ │ ├── supplier_test.go │ │ │ ├── supplier.go │ │ │ └── rabbitmq.go │ │ ├── executor.go │ │ ├── factory.go │ │ ├── pub_notify.go │ │ ├── pub_notify_test.go │ │ ├── http_notify.go │ │ └── http_notify_test.go │ ├── message │ │ ├── command.go │ │ ├── client_message.go │ │ ├── processor_test.go │ │ └── processor.go │ └── core │ │ ├── persistence.go │ │ ├── task.go │ │ ├── redis_db_test.go │ │ ├── redis_db.go │ │ ├── delay_queue_test.go │ │ └── delay_queue.go └── pkg │ └── common │ ├── env.go │ └── env_test.go ├── Dockerfile ├── docker-compose.yml ├── go.mod ├── .gitignore ├── Makefile ├── .github ├── workflows │ └── build.yml └── FUNDING.yml ├── docker-compose-run-sample.yml ├── LICENSE ├── cmd └── server │ └── main.go ├── README.md └── go.sum /internal/app/notify/queue_supplier/rabbitmq_test.go: -------------------------------------------------------------------------------- 1 | package queue_supplier 2 | 3 | import "testing" 4 | 5 | func TestRabbitMqPush(t *testing.T) { 6 | 7 | } 8 | -------------------------------------------------------------------------------- /internal/app/message/command.go: -------------------------------------------------------------------------------- 1 | package message 2 | 3 | type Command uint 4 | 5 | const ( 6 | Test Command = iota + 1 7 | Push 8 | Update 9 | Delete 10 | ) 11 | -------------------------------------------------------------------------------- /internal/app/notify/queue_supplier/kafka.go: -------------------------------------------------------------------------------- 1 | package queue_supplier 2 | 3 | type kafka struct { 4 | } 5 | 6 | func (r *kafka) Push(contents string) error { 7 | return nil 8 | } 9 | -------------------------------------------------------------------------------- /internal/app/notify/executor.go: -------------------------------------------------------------------------------- 1 | package notify 2 | 3 | //executor interface, the business instance need to implement it 4 | type Executor interface { 5 | DoDelayTask(contents string) error 6 | } 7 | -------------------------------------------------------------------------------- /internal/app/message/client_message.go: -------------------------------------------------------------------------------- 1 | package message 2 | 3 | type ClientMessage struct { 4 | AuthToken string `json:"auth_token"` 5 | CMD Command `json:"cmd"` 6 | Contents string `json:"contents"` 7 | } 8 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine:3.17 2 | RUN apk --no-cache add ca-certificates 3 | RUN apk update && apk add tzdata 4 | 5 | # 6 | # FROM scratch 7 | WORKDIR /root/ 8 | 9 | COPY ./go-delayqueue ./ 10 | 11 | EXPOSE 3450 12 | 13 | ENTRYPOINT ["./go-delayqueue"] 14 | -------------------------------------------------------------------------------- /internal/pkg/common/env.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import "os" 4 | 5 | func GetEvnWithDefaultVal(key string, defaultVal string) string { 6 | val := os.Getenv(key) 7 | if val != "" { 8 | return val 9 | } else { 10 | return defaultVal 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.5' 2 | 3 | x-logging: 4 | &default-logging 5 | driver: "json-file" 6 | options: 7 | max-size: "100m" 8 | max-file: "10" 9 | 10 | services: 11 | go-delayqueue: 12 | build: . 13 | image: 0raymond0/go-delayqueue:1.0 14 | -------------------------------------------------------------------------------- /internal/app/core/persistence.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | type Persistence interface { 4 | Save(task *Task) error 5 | GetList() []*Task 6 | Delete(taskId string) error 7 | RemoveAll() error 8 | GetWheelTimePointer() int 9 | SaveWheelTimePointer(index int) error 10 | } 11 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/raymondmars/go-delayqueue 2 | 3 | go 1.16 4 | 5 | require ( 6 | github.com/go-redis/redis/v8 v8.11.5 7 | github.com/google/uuid v1.1.2 8 | github.com/rabbitmq/amqp091-go v1.7.0 9 | github.com/sirupsen/logrus v1.8.1 10 | github.com/stretchr/testify v1.8.0 11 | ) 12 | -------------------------------------------------------------------------------- /internal/app/notify/factory.go: -------------------------------------------------------------------------------- 1 | package notify 2 | 3 | type NotifyMode uint 4 | 5 | const ( 6 | HTTP NotifyMode = iota + 1 7 | SubPub 8 | ) 9 | 10 | func BuildExecutor(mode NotifyMode) Executor { 11 | switch mode { 12 | case HTTP: 13 | return NewHttpNotify() 14 | case SubPub: 15 | return &pubNotify{} 16 | default: 17 | return nil 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /.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 | .DS_Store 17 | .vscode 18 | go-delayqueue 19 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | 2 | test: 3 | GOFLAGS="-count=1" go test -v ./... 4 | 5 | build: 6 | cd cmd/server && CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags "-s -w" -a -o ../../go-delayqueue . && docker-compose build 7 | 8 | deploy: 9 | docker-compose push 10 | 11 | local-down: 12 | docker stop go-delayqueue 13 | 14 | local-up: 15 | docker-compose -f ./docker-compose-run-sample.yml up -d go-delayqueue 16 | 17 | -------------------------------------------------------------------------------- /internal/pkg/common/env_test.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestGetEvnWithDefaultVal(t *testing.T) { 11 | assert.Equal(t, "default", GetEvnWithDefaultVal("NOT_EXIST", "default")) 12 | os.Setenv("EXIST", "exist") 13 | defer os.Unsetenv("EXIST") 14 | assert.Equal(t, "exist", GetEvnWithDefaultVal("EXIST", "default")) 15 | } 16 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | 11 | build: 12 | runs-on: ubuntu-latest 13 | 14 | services: 15 | redis: 16 | image: redis 17 | options: >- 18 | --health-cmd "redis-cli ping" --health-interval 10s --health-timeout 5s --health-retries 5 19 | ports: 20 | - 6379:6379 21 | 22 | steps: 23 | - uses: actions/checkout@v3 24 | 25 | - name: Set up Go 26 | uses: actions/setup-go@v3 27 | with: 28 | go-version: 1.16 29 | 30 | - name: Test 31 | run: go test -v ./... 32 | -------------------------------------------------------------------------------- /internal/app/notify/queue_supplier/supplier_test.go: -------------------------------------------------------------------------------- 1 | package queue_supplier 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestStringToQueue(t *testing.T) { 10 | assert.Equal(t, QueueSupplier(0), StringToQueue("")) 11 | assert.Equal(t, RABBIT_QUEUE, StringToQueue("rabbitmq")) 12 | assert.Equal(t, KAFAK_QUEUE, StringToQueue("kafka")) 13 | assert.Equal(t, QueueSupplier(0), StringToQueue("3")) 14 | } 15 | 16 | func TestNewPubService(t *testing.T) { 17 | service := NewPubService(RABBIT_QUEUE) 18 | assert.IsType(t, &rabbitmq{}, service) 19 | 20 | service = NewPubService(KAFAK_QUEUE) 21 | assert.IsType(t, &kafka{}, service) 22 | 23 | service = NewPubService(KAFAK_QUEUE + 1) 24 | assert.Equal(t, nil, service) 25 | } 26 | -------------------------------------------------------------------------------- /internal/app/core/task.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/raymondmars/go-delayqueue/internal/app/notify" 7 | ) 8 | 9 | type Task struct { 10 | Id string 11 | // the number of cycles of the task on the time wheel, 12 | // when it is equal to 0, the task is executed 13 | CycleCount int 14 | // the position of the task on the time wheel 15 | WheelPosition int 16 | // the task mode, 17 | // which is used by the factory method to determine which implementation object to use 18 | TaskMode notify.NotifyMode 19 | // task method parameters 20 | TaskData string 21 | 22 | Next *Task 23 | } 24 | 25 | func (t *Task) String() string { 26 | return fmt.Sprintf("%s %d %d %d %s", t.Id, t.CycleCount, t.WheelPosition, t.TaskMode, t.TaskData) 27 | } 28 | -------------------------------------------------------------------------------- /internal/app/notify/queue_supplier/supplier.go: -------------------------------------------------------------------------------- 1 | package queue_supplier 2 | 3 | import ( 4 | "strings" 5 | ) 6 | 7 | type QueueSupplier uint 8 | 9 | const ( 10 | RABBIT_QUEUE QueueSupplier = iota + 1 11 | KAFAK_QUEUE 12 | ) 13 | 14 | type PubService interface { 15 | Push(contents string) error 16 | } 17 | 18 | func NewPubService(supplier QueueSupplier) PubService { 19 | switch supplier { 20 | case RABBIT_QUEUE: 21 | return &rabbitmq{} 22 | case KAFAK_QUEUE: 23 | return &kafka{} 24 | default: 25 | return nil 26 | } 27 | } 28 | 29 | func StringToQueue(q string) QueueSupplier { 30 | switch strings.ToLower(q) { 31 | case "rabbitmq": 32 | return RABBIT_QUEUE 33 | case "kafka": 34 | return KAFAK_QUEUE 35 | default: 36 | return QueueSupplier(0) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /docker-compose-run-sample.yml: -------------------------------------------------------------------------------- 1 | version: '3.5' 2 | 3 | x-logging: 4 | &default-logging 5 | driver: "json-file" 6 | options: 7 | max-size: "100m" 8 | max-file: "10" 9 | 10 | services: 11 | go-delayqueue: 12 | image: 0raymond0/go-delayqueue:1.0 13 | container_name: go-delayqueue 14 | restart: always 15 | logging: *default-logging 16 | environment: 17 | REFRESH_POINTER_DEFAULT_SECONDS: 5 18 | REDIS_ADDR: 'redis:6379' 19 | REDIS_DB: 0 20 | REDIS_PWD: '' 21 | DELAY_QUEUE_LIST_KEY: '__delay_queue_list__' 22 | QUEUE_SUPPLIER: 'rabbitmq' 23 | AMQP_URI: 'amqp://admin:Start@321@rabbitmq:5672' 24 | 25 | networks: 26 | - raymond-locals 27 | 28 | ports: 29 | - "3450:3450" 30 | 31 | networks: 32 | raymond-locals: 33 | name: custom_network 34 | -------------------------------------------------------------------------------- /.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: paypal.me/0raymondjiang0 # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 14 | 15 | -------------------------------------------------------------------------------- /internal/app/notify/pub_notify.go: -------------------------------------------------------------------------------- 1 | package notify 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "log" 7 | 8 | "github.com/raymondmars/go-delayqueue/internal/app/notify/queue_supplier" 9 | "github.com/raymondmars/go-delayqueue/internal/pkg/common" 10 | ) 11 | 12 | type pubNotify struct { 13 | Service queue_supplier.PubService 14 | } 15 | 16 | func (nt *pubNotify) DoDelayTask(contents string) error { 17 | log.Println(fmt.Sprintf("Do task.....%s", contents)) 18 | configSupplier := common.GetEvnWithDefaultVal("QUEUE_SUPPLIER", "") 19 | if configSupplier != "" { 20 | if nt.Service == nil { 21 | nt.Service = queue_supplier.NewPubService(queue_supplier.StringToQueue(configSupplier)) 22 | } 23 | if nt.Service != nil { 24 | return nt.Service.Push(contents) 25 | } else { 26 | return errors.New("PubService is nil, please check QUEUE_SUPPLIER") 27 | } 28 | } 29 | 30 | return errors.New("QUEUE_SUPPLIER is emtpy") 31 | } 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 raymondmars 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 | -------------------------------------------------------------------------------- /internal/app/notify/pub_notify_test.go: -------------------------------------------------------------------------------- 1 | package notify 2 | 3 | import ( 4 | "errors" 5 | "os" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | type mockPubService struct{} 12 | 13 | func (c *mockPubService) Push(contents string) error { 14 | return nil 15 | } 16 | 17 | type mockErrPubService struct{} 18 | 19 | func (c *mockErrPubService) Push(contents string) error { 20 | return errors.New("push failed with errors") 21 | } 22 | 23 | func TestPubSubNofityDoDelayTask(t *testing.T) { 24 | normalMockPubService := &mockPubService{} 25 | notify := &pubNotify{Service: normalMockPubService} 26 | assert.Error(t, errors.New("QUEUE_SUPPLIER is emtpy"), notify.DoDelayTask("test")) 27 | 28 | os.Setenv("QUEUE_SUPPLIER", "rabbitmq-err") 29 | defer os.Unsetenv("QUEUE_SUPPLIER") 30 | 31 | notify2 := &pubNotify{} 32 | assert.Error(t, errors.New("PubService is nil, please check QUEUE_SUPPLIER"), notify2.DoDelayTask("test")) 33 | 34 | normalMockPubService2 := &mockPubService{} 35 | okNotify := &pubNotify{Service: normalMockPubService2} 36 | os.Setenv("QUEUE_SUPPLIER", "rabbitmq") 37 | assert.Equal(t, nil, okNotify.DoDelayTask("test")) 38 | 39 | errMockPubService := &mockErrPubService{} 40 | errNotify := &pubNotify{Service: errMockPubService} 41 | assert.Error(t, errors.New("push failed with errors"), errNotify.DoDelayTask("test")) 42 | } 43 | -------------------------------------------------------------------------------- /internal/app/notify/http_notify.go: -------------------------------------------------------------------------------- 1 | package notify 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "fmt" 7 | "net/http" 8 | "strings" 9 | "time" 10 | 11 | log "github.com/sirupsen/logrus" 12 | ) 13 | 14 | type HttpClient interface { 15 | Do(req *http.Request) (*http.Response, error) 16 | } 17 | type httpNotify struct { 18 | Client HttpClient 19 | } 20 | 21 | func NewHttpNotify() *httpNotify { 22 | return &httpNotify{ 23 | Client: &http.Client{ 24 | Timeout: time.Second * 10, 25 | }, 26 | } 27 | } 28 | 29 | func (nt *httpNotify) DoDelayTask(contents string) error { 30 | log.Info(fmt.Sprintf("Do task.....%s", contents)) 31 | splitValue := strings.Split(contents, "|") 32 | if len(splitValue) > 1 { 33 | req, _ := http.NewRequest(http.MethodPost, splitValue[0], bytes.NewBuffer([]byte(splitValue[1]))) 34 | 35 | resp, err := nt.Client.Do(req) 36 | if err != nil { 37 | log.Warnln(fmt.Sprintf("http notify error: %v", err)) 38 | return errors.New(fmt.Sprintf("http notify error: %s", err.Error())) 39 | } 40 | defer resp.Body.Close() 41 | if resp.StatusCode != 200 { 42 | log.Warnln(fmt.Sprintf("http request response is %d", resp.StatusCode)) 43 | return errors.New("http request response is not 200") 44 | } 45 | return nil 46 | } else { 47 | log.Warnln(fmt.Sprintf("invalid http notify contents: %s", contents)) 48 | return errors.New("invalid notify contents") 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /internal/app/notify/http_notify_test.go: -------------------------------------------------------------------------------- 1 | package notify 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "fmt" 7 | "io/ioutil" 8 | "net/http" 9 | "testing" 10 | 11 | "github.com/stretchr/testify/assert" 12 | ) 13 | 14 | var mockBody = ioutil.NopCloser(bytes.NewBufferString("Hello World")) 15 | 16 | type mockHttpOkClient struct{} 17 | 18 | func (c *mockHttpOkClient) Do(req *http.Request) (*http.Response, error) { 19 | return &http.Response{ 20 | StatusCode: http.StatusOK, 21 | Body: mockBody, 22 | }, nil 23 | } 24 | 25 | type mockHttpErrClient struct{} 26 | 27 | func (c *mockHttpErrClient) Do(req *http.Request) (*http.Response, error) { 28 | return &http.Response{}, errors.New("invalid request data") 29 | } 30 | 31 | type mockHttpNoneOkClient struct{} 32 | 33 | func (c *mockHttpNoneOkClient) Do(req *http.Request) (*http.Response, error) { 34 | return &http.Response{ 35 | StatusCode: http.StatusInternalServerError, 36 | Body: mockBody, 37 | }, nil 38 | } 39 | 40 | func TestHttpNofityDoDelayTask(t *testing.T) { 41 | notifyOk := &httpNotify{ 42 | Client: &mockHttpOkClient{}, 43 | } 44 | notifyErr := &httpNotify{ 45 | Client: &mockHttpErrClient{}, 46 | } 47 | notifyNoneOk := &httpNotify{ 48 | Client: &mockHttpNoneOkClient{}, 49 | } 50 | 51 | assert.Error(t, errors.New("invalid notify contents"), notifyOk.DoDelayTask("test")) 52 | assert.Equal(t, nil, notifyOk.DoDelayTask("https://google.com|test")) 53 | assert.Error(t, errors.New(fmt.Sprintf("http notify error: %s", "invalid request data")), notifyErr.DoDelayTask("https://google.com|test")) 54 | assert.Error(t, errors.New("http request response is not 200"), notifyNoneOk.DoDelayTask("https://google.com|test")) 55 | } 56 | -------------------------------------------------------------------------------- /internal/app/notify/queue_supplier/rabbitmq.go: -------------------------------------------------------------------------------- 1 | package queue_supplier 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "strings" 7 | 8 | amqp "github.com/rabbitmq/amqp091-go" 9 | "github.com/raymondmars/go-delayqueue/internal/pkg/common" 10 | log "github.com/sirupsen/logrus" 11 | ) 12 | 13 | var amqpURI = common.GetEvnWithDefaultVal("AMQP_URI", "amqp://guest:guest@localhost:5672/") 14 | 15 | const ( 16 | exchangeName = "" 17 | ) 18 | 19 | type rabbitmq struct { 20 | } 21 | 22 | func (r *rabbitmq) Push(contents string) error { 23 | splitValue := strings.Split(contents, "|") 24 | if len(splitValue) > 1 { 25 | queueName := splitValue[0] 26 | pushContents := splitValue[1] 27 | return r.pushToQueue(queueName, []byte(pushContents)) 28 | 29 | } else { 30 | log.Warnln(fmt.Sprintf("invalid rabbitmq queue notify contents: %s", contents)) 31 | return errors.New("invalid notify contents") 32 | } 33 | } 34 | 35 | func (r *rabbitmq) pushToQueue(queueName string, body []byte) error { 36 | connection, err := amqp.Dial(amqpURI) 37 | if err != nil { 38 | log.Printf("connect mq failed: %v", err) 39 | return err 40 | } 41 | defer connection.Close() 42 | 43 | //创建一个Channel 44 | channel, err := connection.Channel() 45 | if err != nil { 46 | log.Printf("create channel failed: %v", err) 47 | return err 48 | } 49 | defer channel.Close() 50 | 51 | //创建一个queue 52 | q, err := channel.QueueDeclare( 53 | string(queueName), // name 54 | true, // durable 55 | false, // delete when unused 56 | false, // exclusive 57 | false, // no-wait 58 | nil, // arguments 59 | ) 60 | if err != nil { 61 | log.Printf("create queue failed: %v", err) 62 | return err 63 | } 64 | 65 | err = channel.Publish( 66 | exchangeName, // exchange 67 | q.Name, // routing key 68 | false, // mandatory 69 | false, // immediate 70 | amqp.Publishing{ 71 | Headers: amqp.Table{}, 72 | DeliveryMode: amqp.Persistent, 73 | ContentType: "text/plain", 74 | ContentEncoding: "", 75 | Body: body, 76 | }) 77 | 78 | if err != nil { 79 | log.Printf("error: %v", err) 80 | } 81 | 82 | return err 83 | } 84 | -------------------------------------------------------------------------------- /cmd/server/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "io" 7 | "net" 8 | "os" 9 | "strings" 10 | 11 | "github.com/raymondmars/go-delayqueue/internal/app/core" 12 | "github.com/raymondmars/go-delayqueue/internal/app/message" 13 | "github.com/raymondmars/go-delayqueue/internal/app/notify" 14 | "github.com/raymondmars/go-delayqueue/internal/pkg/common" 15 | 16 | log "github.com/sirupsen/logrus" 17 | ) 18 | 19 | const ( 20 | DEFAULT_HOST = "0.0.0.0" 21 | DEFAULT_PORT = "3450" 22 | DEFAULT_CONN_TYPE = "tcp" 23 | ) 24 | 25 | var delayQueue *core.DelayQueue 26 | 27 | // client send command echo -n "ping" | nc localhost 3450 28 | // telnet localhost 3450 29 | // quit: ctrl+] and input quit 30 | //telnet x.x.x.x xxxx < 0 { 79 | for _, item := range listArray { 80 | key := fmt.Sprintf("%s%s", TASK_KEY_PREFIX, item) 81 | taskCmd := rd.Client.Get(rd.Context, key) 82 | if val, err := taskCmd.Result(); err == nil { 83 | entity := Task{} 84 | err := json.Unmarshal([]byte(val), &entity) 85 | if err == nil { 86 | tasks = append(tasks, &entity) 87 | } 88 | } 89 | } 90 | } 91 | return tasks 92 | } 93 | 94 | // remove task from redis 95 | func (rd *redisDb) Delete(taskId string) error { 96 | rd.Client.LRem(rd.Context, rd.TaskListKey, 0, taskId) 97 | rd.Client.Del(rd.Context, fmt.Sprintf("%s%s", TASK_KEY_PREFIX, taskId)) 98 | 99 | return nil 100 | } 101 | 102 | // remove all tasks from redis 103 | func (rd *redisDb) RemoveAll() error { 104 | listResult := rd.Client.LRange(rd.Context, rd.TaskListKey, 0, -1) 105 | listArray, _ := listResult.Result() 106 | if listArray != nil && len(listArray) > 0 { 107 | for _, tkId := range listArray { 108 | rd.Client.Del(rd.Context, fmt.Sprintf("%s%s", TASK_KEY_PREFIX, tkId)) 109 | } 110 | } 111 | rd.Client.Del(rd.Context, rd.TaskListKey) 112 | return nil 113 | } 114 | 115 | func (rd *redisDb) SaveWheelTimePointer(index int) error { 116 | cmd := rd.Client.Set(rd.Context, TIME_POINTER_CACHE_KEY, index, 0) 117 | return cmd.Err() 118 | } 119 | 120 | func (rd *redisDb) GetWheelTimePointer() int { 121 | if result := rd.Client.Get(rd.Context, TIME_POINTER_CACHE_KEY); result.Err() == nil { 122 | index, _ := strconv.Atoi(result.Val()) 123 | return index 124 | } else { 125 | return 0 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /internal/app/message/processor_test.go: -------------------------------------------------------------------------------- 1 | package message 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "testing" 7 | 8 | "github.com/raymondmars/go-delayqueue/internal/app/core" 9 | "github.com/raymondmars/go-delayqueue/internal/app/notify" 10 | "github.com/stretchr/testify/assert" 11 | ) 12 | 13 | type testNotify struct{} 14 | 15 | func (tn *testNotify) DoDelayTask(contents string) error { 16 | log.Println(fmt.Sprintf("test -> Do task.....%s", contents)) 17 | return nil 18 | } 19 | 20 | func testFactory(taskMode notify.NotifyMode) notify.Executor { 21 | return &testNotify{} 22 | } 23 | 24 | type testDoNothingDb struct{} 25 | 26 | func (td *testDoNothingDb) Save(task *core.Task) error { 27 | return nil 28 | } 29 | 30 | func (td *testDoNothingDb) GetList() []*core.Task { 31 | return []*core.Task{} 32 | } 33 | 34 | func (td *testDoNothingDb) Delete(taskId string) error { 35 | return nil 36 | } 37 | 38 | func (td *testDoNothingDb) RemoveAll() error { 39 | return nil 40 | } 41 | 42 | func (td *testDoNothingDb) GetWheelTimePointer() int { 43 | return 0 44 | } 45 | 46 | func (td *testDoNothingDb) SaveWheelTimePointer(index int) error { 47 | return nil 48 | } 49 | 50 | func testQueue() *core.DelayQueue { 51 | presisDb := &testDoNothingDb{} 52 | return &core.DelayQueue{ 53 | Persistence: presisDb, 54 | TaskExecutor: testFactory, 55 | TaskQueryTable: make(core.SlotRecorder), 56 | } 57 | } 58 | 59 | func TestProcessor(t *testing.T) { 60 | dq := testQueue() 61 | processor := NewProcessor() 62 | 63 | // test not ready 64 | resp := processor.Receive(dq, []string{}) 65 | assert.Equal(t, Fail, resp.Status) 66 | assert.Equal(t, NOT_READY, resp.ErrorCode) 67 | 68 | dq.Start() 69 | 70 | // test auth and ping 71 | resp = processor.Receive(dq, []string{}) 72 | assert.Equal(t, Fail, resp.Status) 73 | assert.Equal(t, INVALID_MESSAGE, resp.ErrorCode) 74 | 75 | resp = processor.Receive(dq, []string{"hello", "1"}) 76 | assert.Equal(t, Fail, resp.Status) 77 | assert.Equal(t, AUTH_FAILED, resp.ErrorCode) 78 | 79 | resp = processor.Receive(dq, []string{messageAuthCode, "1"}) 80 | assert.Equal(t, Ok, resp.Status) 81 | assert.Equal(t, "pong", resp.Message) 82 | 83 | // test push task 84 | resp = processor.Receive(dq, []string{messageAuthCode, "2", "0", "1", "http://www.google.com", "test"}) 85 | assert.Equal(t, Fail, resp.Status) 86 | assert.Equal(t, INVALID_DELAY_TIME, resp.ErrorCode) 87 | 88 | resp = processor.Receive(dq, []string{messageAuthCode, "2", "100", "1", "http://www.google.com"}) 89 | assert.Equal(t, Fail, resp.Status) 90 | assert.Equal(t, INVALID_PUSH_MESSAGE, resp.ErrorCode) 91 | 92 | resp = processor.Receive(dq, []string{messageAuthCode, "2", "50", "1", "http://www.google.com", "test"}) 93 | assert.Equal(t, Ok, resp.Status) 94 | assert.Equal(t, 1, dq.WheelTaskQuantity(50%core.WHEEL_SIZE)) 95 | 96 | resp = processor.Receive(dq, []string{messageAuthCode, "2", "100", "2", "queue_name"}) 97 | assert.Equal(t, Fail, resp.Status) 98 | assert.Equal(t, INVALID_PUSH_MESSAGE, resp.ErrorCode) 99 | 100 | resp = processor.Receive(dq, []string{messageAuthCode, "2", "100", "2", "queue_name", "test"}) 101 | assert.Equal(t, Ok, resp.Status) 102 | assert.Equal(t, 1, dq.WheelTaskQuantity(100%core.WHEEL_SIZE)) 103 | 104 | resp = processor.Receive(dq, []string{messageAuthCode, "2", "100", "3", "queue_name", "test"}) 105 | assert.Equal(t, Fail, resp.Status) 106 | assert.Equal(t, INVALID_PUSH_MESSAGE, resp.ErrorCode) 107 | assert.Equal(t, "Invalid notify way.", resp.Message) 108 | 109 | // test update task from client 110 | resp = processor.Receive(dq, []string{messageAuthCode, "2", "50", "1", "http://www.google.com", "test1"}) 111 | assert.Equal(t, Ok, resp.Status) 112 | taskId := resp.Message 113 | taskInfo := dq.GetTask(taskId) 114 | assert.Equal(t, "http://www.google.com|test1", taskInfo.TaskData) 115 | assert.Equal(t, notify.HTTP, taskInfo.TaskMode) 116 | // send update message 117 | resp = processor.Receive(dq, []string{messageAuthCode, "3", taskId, "2", "queue_name", "test2"}) 118 | assert.Equal(t, Ok, resp.Status) 119 | taskInfo = dq.GetTask(taskId) 120 | assert.Equal(t, "queue_name|test2", taskInfo.TaskData) 121 | assert.Equal(t, notify.SubPub, taskInfo.TaskMode) 122 | 123 | // test delete task from client 124 | delaySeconds := 30 125 | // push a task to queue 126 | resp = processor.Receive(dq, []string{messageAuthCode, "2", fmt.Sprintf("%d", delaySeconds), "1", "http://www.google.com", "test1"}) 127 | assert.Equal(t, Ok, resp.Status) 128 | assert.Equal(t, 1, dq.WheelTaskQuantity(delaySeconds%core.WHEEL_SIZE)) 129 | 130 | taskId = resp.Message 131 | 132 | // send delete message 133 | resp = processor.Receive(dq, []string{messageAuthCode, "4", taskId}) 134 | assert.Equal(t, Ok, resp.Status) 135 | assert.Equal(t, 0, dq.WheelTaskQuantity(delaySeconds%core.WHEEL_SIZE)) 136 | } 137 | -------------------------------------------------------------------------------- /internal/app/message/processor.go: -------------------------------------------------------------------------------- 1 | package message 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | "strings" 7 | "time" 8 | 9 | log "github.com/sirupsen/logrus" 10 | 11 | "github.com/raymondmars/go-delayqueue/internal/app/core" 12 | "github.com/raymondmars/go-delayqueue/internal/app/notify" 13 | "github.com/raymondmars/go-delayqueue/internal/pkg/common" 14 | ) 15 | 16 | type ResponseStatusCode uint 17 | type ResponseErrCode uint 18 | 19 | const ( 20 | Ok ResponseStatusCode = iota + 1 21 | Fail 22 | ) 23 | 24 | const ( 25 | NOT_READY ResponseErrCode = 1000 26 | INVALID_MESSAGE ResponseErrCode = 1010 27 | AUTH_FAILED ResponseErrCode = 1012 28 | INVALID_DELAY_TIME ResponseErrCode = 1014 29 | INVALID_COMMAND ResponseErrCode = 1016 30 | INVALID_PUSH_MESSAGE ResponseErrCode = 1018 31 | UPDATE_FAILED ResponseErrCode = 1020 32 | DELETE_FAILED ResponseErrCode = 1022 33 | ) 34 | 35 | type Response struct { 36 | Status ResponseStatusCode 37 | ErrorCode ResponseErrCode 38 | Message string 39 | } 40 | 41 | func (r Response) String() string { 42 | if r.Status == Ok { 43 | return fmt.Sprintf("%d|%s", r.Status, r.Message) 44 | } else { 45 | return fmt.Sprintf("%d|%d|%s", r.Status, r.ErrorCode, r.Message) 46 | } 47 | } 48 | 49 | type Processor interface { 50 | Receive(queue *core.DelayQueue, contents []string) *Response 51 | } 52 | 53 | var messageAuthCode = common.GetEvnWithDefaultVal("MESSAGE_AUTH_CODE", "0_ONMARS_1") 54 | 55 | type processor struct { 56 | } 57 | 58 | func NewProcessor() Processor { 59 | return &processor{} 60 | } 61 | 62 | // receive message from client 63 | func (p *processor) Receive(queue *core.DelayQueue, contents []string) *Response { 64 | // defer conn.Close() 65 | if queue == nil || !queue.IsReady { 66 | return &Response{ 67 | Status: Fail, 68 | ErrorCode: NOT_READY, 69 | } 70 | } 71 | log.Info("Receive message:" + strings.Join(contents, " | ")) 72 | // message format is: 73 | // first line is auth code; 0 ----------| 74 | // second line is cmd name; 1 ----------| 75 | // third line is delay seconds or task id(for update, delete); 2 ----------| 76 | // fourth line is notify way 3 ----------| 77 | // fifth line http url if task mode is HTTP, or is queueName if task mode is PubSub; 4 ----------| 78 | // sixth line is message contents; 5 ----------| 79 | if len(contents) < 2 || len(contents) > 6 { 80 | return &Response{ 81 | Status: Fail, 82 | ErrorCode: INVALID_MESSAGE, 83 | } 84 | } 85 | 86 | if contents[0] != messageAuthCode { 87 | return &Response{ 88 | Status: Fail, 89 | ErrorCode: AUTH_FAILED, 90 | } 91 | } 92 | 93 | code, _ := strconv.Atoi(contents[1]) 94 | cmd := Command(code) 95 | if cmd == Test { 96 | return &Response{ 97 | Status: Ok, 98 | Message: "pong", 99 | } 100 | } 101 | 102 | switch cmd { 103 | case Push: 104 | if len(contents) != 6 { 105 | return &Response{ 106 | Status: Fail, 107 | ErrorCode: INVALID_PUSH_MESSAGE, 108 | } 109 | } 110 | delaySeconds, _ := strconv.Atoi(contents[2]) 111 | if delaySeconds <= 0 { 112 | return &Response{ 113 | Status: Fail, 114 | ErrorCode: INVALID_DELAY_TIME, 115 | } 116 | } 117 | wayCode, _ := strconv.Atoi(contents[3]) 118 | taskTarget := contents[4] 119 | taskData := contents[5] 120 | switch notify.NotifyMode(wayCode) { 121 | case notify.HTTP: 122 | return p.executePush(queue, taskTarget, taskData, delaySeconds, notify.HTTP) 123 | case notify.SubPub: 124 | return p.executePush(queue, taskTarget, taskData, delaySeconds, notify.SubPub) 125 | default: 126 | return &Response{ 127 | Status: Fail, 128 | ErrorCode: INVALID_PUSH_MESSAGE, 129 | Message: "Invalid notify way.", 130 | } 131 | } 132 | 133 | case Update: 134 | if len(contents) != 6 { 135 | return &Response{ 136 | Status: Fail, 137 | ErrorCode: INVALID_PUSH_MESSAGE, 138 | } 139 | } 140 | taskId := strings.TrimSpace(contents[2]) 141 | wayCode, _ := strconv.Atoi(contents[3]) 142 | taskTarget := contents[4] 143 | taskData := contents[5] 144 | err := queue.UpdateTask(taskId, notify.NotifyMode(wayCode), fmt.Sprintf("%s|%s", taskTarget, taskData)) 145 | if err != nil { 146 | return &Response{ 147 | Status: Fail, 148 | ErrorCode: UPDATE_FAILED, 149 | Message: err.Error(), 150 | } 151 | } else { 152 | return &Response{ 153 | Status: Ok, 154 | Message: taskId, 155 | } 156 | } 157 | case Delete: 158 | if len(contents) != 3 { 159 | return &Response{ 160 | Status: Fail, 161 | ErrorCode: INVALID_PUSH_MESSAGE, 162 | } 163 | } 164 | taskId := strings.TrimSpace(contents[2]) 165 | err := queue.DeleteTask(taskId) 166 | if err != nil { 167 | return &Response{ 168 | Status: Fail, 169 | ErrorCode: DELETE_FAILED, 170 | Message: err.Error(), 171 | } 172 | } else { 173 | return &Response{ 174 | Status: Ok, 175 | Message: taskId, 176 | } 177 | } 178 | default: 179 | return &Response{ 180 | Status: Fail, 181 | ErrorCode: INVALID_COMMAND, 182 | Message: "Invalid command.", 183 | } 184 | } 185 | } 186 | 187 | func (p *processor) executePush(queue *core.DelayQueue, target, data string, delaySeconds int, mode notify.NotifyMode) *Response { 188 | task, err := queue.Push(time.Duration(delaySeconds)*time.Second, mode, fmt.Sprintf("%s|%s", target, data)) 189 | if err != nil { 190 | return &Response{ 191 | Status: Fail, 192 | ErrorCode: INVALID_PUSH_MESSAGE, 193 | Message: err.Error(), 194 | } 195 | } 196 | return &Response{ 197 | Status: Ok, 198 | Message: task.Id, 199 | } 200 | } 201 | -------------------------------------------------------------------------------- /internal/app/core/delay_queue_test.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "strconv" 7 | "sync" 8 | "testing" 9 | "time" 10 | 11 | "github.com/raymondmars/go-delayqueue/internal/app/notify" 12 | "github.com/stretchr/testify/assert" 13 | ) 14 | 15 | type testNotify struct{} 16 | 17 | // implement Executor interface 18 | func (tn *testNotify) DoDelayTask(contents string) error { 19 | log.Println(fmt.Sprintf("Do task.....%s", contents)) 20 | return nil 21 | } 22 | 23 | // a factory method to build executor 24 | func testFactory(taskMode notify.NotifyMode) notify.Executor { 25 | return &testNotify{} 26 | } 27 | 28 | type testDoNothingDb struct{} 29 | 30 | func (td *testDoNothingDb) Save(task *Task) error { 31 | return nil 32 | } 33 | 34 | func (td *testDoNothingDb) GetList() []*Task { 35 | return []*Task{} 36 | } 37 | 38 | func (td *testDoNothingDb) Delete(taskId string) error { 39 | return nil 40 | } 41 | 42 | func (td *testDoNothingDb) RemoveAll() error { 43 | return nil 44 | } 45 | 46 | func (td *testDoNothingDb) GetWheelTimePointer() int { 47 | return 0 48 | } 49 | 50 | func (td *testDoNothingDb) SaveWheelTimePointer(index int) error { 51 | return nil 52 | } 53 | 54 | var dq *DelayQueue 55 | 56 | func testBeforeSetUp() { 57 | presisDb := &testDoNothingDb{} 58 | dq = &DelayQueue{ 59 | Persistence: presisDb, 60 | TaskExecutor: testFactory, 61 | TaskQueryTable: make(SlotRecorder), 62 | } 63 | } 64 | 65 | func testWithRedisBeforeSetUp() { 66 | dq = &DelayQueue{ 67 | Persistence: getRedisDb(), 68 | TaskExecutor: testFactory, 69 | TaskQueryTable: make(SlotRecorder), 70 | } 71 | dq.RemoveAllTasks() 72 | } 73 | 74 | func TestCanntPushTask(t *testing.T) { 75 | testBeforeSetUp() 76 | _, err := dq.Push(999*time.Millisecond, notify.HTTP, "") 77 | assert.Equal(t, err.Error(), "the delay time cannot be less than 1 second, current is: 999ms") 78 | } 79 | 80 | func TestPushTaskInCorrectPosition(t *testing.T) { 81 | testBeforeSetUp() 82 | var wg sync.WaitGroup 83 | for i := 1; i <= WHEEL_SIZE; i++ { 84 | wg.Add(1) 85 | go func(index int) { 86 | dq.Push(time.Duration(index)*time.Second, notify.HTTP, "") 87 | wg.Done() 88 | }(i) 89 | } 90 | wg.Wait() 91 | 92 | for i := 1; i <= WHEEL_SIZE; i++ { 93 | assert.Equal(t, 1, dq.WheelTaskQuantity(i%WHEEL_SIZE)) 94 | } 95 | } 96 | 97 | func TestConcurrentPush(t *testing.T) { 98 | testBeforeSetUp() 99 | targetSeconds := 50 100 | taskCounts := 10000 101 | var wg sync.WaitGroup 102 | for i := 0; i < taskCounts; i++ { 103 | wg.Add(1) 104 | go func() { 105 | dq.Push(time.Duration(targetSeconds)*time.Second, notify.SubPub, "") 106 | wg.Done() 107 | }() 108 | } 109 | wg.Wait() 110 | assert.Equal(t, taskCounts, dq.WheelTaskQuantity(targetSeconds%WHEEL_SIZE)) 111 | } 112 | 113 | func TestExecuteTask(t *testing.T) { 114 | testBeforeSetUp() 115 | dq.Start() 116 | targetSeconds := 2 117 | tk, _ := dq.Push(time.Duration(targetSeconds)*time.Second, notify.HTTP, "hello,world") 118 | assert.NotNil(t, tk) 119 | 120 | // wait to task be executed 121 | time.Sleep(time.Duration(targetSeconds+2) * time.Second) 122 | assert.Equal(t, 0, dq.WheelTaskQuantity(targetSeconds%WHEEL_SIZE)) 123 | assert.Nil(t, dq.GetTask(tk.Id)) 124 | } 125 | 126 | func TestGetTask(t *testing.T) { 127 | testBeforeSetUp() 128 | tk1, _ := dq.Push(10*time.Second, notify.HTTP, "hello1") 129 | tk2, _ := dq.Push(10*time.Second, notify.HTTP, "hello2") 130 | tk3, _ := dq.Push(20*time.Second, notify.SubPub, "hello3") 131 | 132 | assert.Equal(t, dq.GetTask(tk1.Id).TaskMode, notify.HTTP) 133 | assert.Equal(t, dq.GetTask(tk1.Id).TaskData, "hello1") 134 | 135 | assert.Equal(t, dq.GetTask(tk2.Id).TaskMode, notify.HTTP) 136 | assert.Equal(t, dq.GetTask(tk2.Id).TaskData, "hello2") 137 | 138 | assert.Equal(t, dq.GetTask(tk3.Id).TaskMode, notify.SubPub) 139 | assert.Equal(t, dq.GetTask(tk3.Id).TaskData, "hello3") 140 | } 141 | 142 | func TestConcurrentGetTask(t *testing.T) { 143 | testBeforeSetUp() 144 | targetSeconds := 50 145 | taskCounts := 10000 146 | var taskIds [10000]string 147 | var wg sync.WaitGroup 148 | total := 0 149 | for i := 0; i < taskCounts; i++ { 150 | tk, _ := dq.Push(time.Duration(targetSeconds)*time.Second, notify.HTTP, i) 151 | taskIds[i] = tk.Id 152 | total = total + i 153 | } 154 | 155 | innerTotal := 0 156 | for i := 0; i < taskCounts; i++ { 157 | wg.Add(1) 158 | go func(index int) { 159 | tk := dq.GetTask(taskIds[index]) 160 | if tk != nil { 161 | p, _ := strconv.Atoi(tk.TaskData) 162 | mutex.Lock() 163 | innerTotal = innerTotal + p 164 | mutex.Unlock() 165 | } 166 | wg.Done() 167 | }(i) 168 | } 169 | wg.Wait() 170 | assert.Equal(t, taskCounts, dq.WheelTaskQuantity(targetSeconds%WHEEL_SIZE)) 171 | assert.Equal(t, total, innerTotal) 172 | } 173 | 174 | func TestUpdateTask(t *testing.T) { 175 | testBeforeSetUp() 176 | tk1, _ := dq.Push(10*time.Second, notify.HTTP, "hello1") 177 | 178 | assert.Equal(t, dq.GetTask(tk1.Id).TaskMode, notify.HTTP) 179 | assert.Equal(t, dq.GetTask(tk1.Id).TaskData, "hello1") 180 | 181 | err := dq.UpdateTask(tk1.Id, notify.SubPub, "hello100") 182 | assert.Nil(t, err) 183 | 184 | assert.Equal(t, dq.GetTask(tk1.Id).TaskMode, notify.SubPub) 185 | assert.Equal(t, dq.GetTask(tk1.Id).TaskData, "hello100") 186 | } 187 | 188 | func TestDeleteTask(t *testing.T) { 189 | testBeforeSetUp() 190 | targetSeconds := 10 191 | tk1, _ := dq.Push(time.Duration(targetSeconds)*time.Second, notify.HTTP, "hello1") 192 | tk2, _ := dq.Push(time.Duration(targetSeconds)*time.Second, notify.SubPub, "hello2") 193 | tk3, _ := dq.Push(time.Duration(targetSeconds)*time.Second, notify.SubPub, "hello3") 194 | assert.Equal(t, 3, len(dq.TaskQueryTable)) 195 | assert.Equal(t, 3, dq.WheelTaskQuantity(targetSeconds%WHEEL_SIZE)) 196 | err := dq.DeleteTask(tk2.Id) 197 | assert.Nil(t, err) 198 | assert.Equal(t, 2, len(dq.TaskQueryTable)) 199 | assert.Equal(t, 2, dq.WheelTaskQuantity(targetSeconds%WHEEL_SIZE)) 200 | 201 | assert.Equal(t, dq.GetTask(tk1.Id).TaskMode, notify.HTTP) 202 | assert.Equal(t, dq.GetTask(tk1.Id).TaskData, "hello1") 203 | 204 | assert.Equal(t, dq.GetTask(tk3.Id).TaskMode, notify.SubPub) 205 | assert.Equal(t, dq.GetTask(tk3.Id).TaskData, "hello3") 206 | 207 | dq.DeleteTask(tk1.Id) 208 | dq.DeleteTask(tk3.Id) 209 | 210 | assert.Equal(t, 0, len(dq.TaskQueryTable)) 211 | assert.Equal(t, 0, dq.WheelTaskQuantity(targetSeconds%WHEEL_SIZE)) 212 | } 213 | 214 | func TestConcurrentDeleteTasks(t *testing.T) { 215 | testBeforeSetUp() 216 | targetSeconds := 50 217 | taskCounts := 10000 218 | taskIds := []string{} 219 | var lock sync.Mutex 220 | 221 | var wg sync.WaitGroup 222 | for i := 0; i < taskCounts; i++ { 223 | wg.Add(1) 224 | go func() { 225 | tk, _ := dq.Push(time.Duration(targetSeconds)*time.Second, notify.HTTP, "") 226 | lock.Lock() 227 | defer lock.Unlock() 228 | taskIds = append(taskIds, tk.Id) 229 | wg.Done() 230 | }() 231 | } 232 | wg.Wait() 233 | assert.Equal(t, taskCounts, len(dq.TaskQueryTable)) 234 | assert.Equal(t, taskCounts, len(taskIds)) 235 | assert.Equal(t, taskCounts, dq.WheelTaskQuantity(targetSeconds%WHEEL_SIZE)) 236 | 237 | for i := 0; i < taskCounts; i++ { 238 | wg.Add(1) 239 | go func(index int) { 240 | dq.DeleteTask(taskIds[index]) 241 | wg.Done() 242 | }(i) 243 | } 244 | wg.Wait() 245 | 246 | assert.Equal(t, 0, len(dq.TaskQueryTable)) 247 | assert.Equal(t, 0, dq.WheelTaskQuantity(targetSeconds%WHEEL_SIZE)) 248 | } 249 | 250 | func TestDelayQueueAndRedisIntegrate(t *testing.T) { 251 | testWithRedisBeforeSetUp() 252 | 253 | randomSlots := []int{60, 100, 560, 2450, 3500} 254 | eachSoltNodes := 100 255 | for _, seconds := range randomSlots { 256 | for i := 0; i < eachSoltNodes; i++ { 257 | dq.Push(time.Duration(seconds)*time.Second, notify.HTTP, i) 258 | } 259 | } 260 | assert.Equal(t, eachSoltNodes*len(randomSlots), len(dq.TaskQueryTable)) 261 | for _, seconds := range randomSlots { 262 | assert.Equal(t, eachSoltNodes, dq.WheelTaskQuantity(seconds%WHEEL_SIZE)) 263 | } 264 | //remove nodes 265 | dq.TaskQueryTable = make(SlotRecorder) 266 | for i := 0; i < len(dq.TimeWheel); i++ { 267 | dq.TimeWheel[i].NotifyTasks = nil 268 | } 269 | // Do not use range to fetch wheel, it will use copy value 270 | // for _, wheel := range dq.TimeWheel { 271 | // wheel.NotifyTasks = nil 272 | // } 273 | assert.Equal(t, 0, len(dq.TaskQueryTable)) 274 | for _, seconds := range randomSlots { 275 | assert.Equal(t, 0, dq.WheelTaskQuantity(seconds%WHEEL_SIZE)) 276 | } 277 | // load from cache 278 | dq.loadTasksFromDb() 279 | assert.Equal(t, eachSoltNodes*len(randomSlots), len(dq.TaskQueryTable)) 280 | for _, seconds := range randomSlots { 281 | assert.Equal(t, eachSoltNodes, dq.WheelTaskQuantity(seconds%WHEEL_SIZE)) 282 | } 283 | } 284 | 285 | func BenchmarkPushTask(b *testing.B) { 286 | testBeforeSetUp() 287 | targetSeconds := 50 288 | 289 | for i := 0; i < b.N; i++ { 290 | dq.Push(time.Duration(targetSeconds)*time.Second, notify.HTTP, i) 291 | } 292 | } 293 | -------------------------------------------------------------------------------- /internal/app/core/delay_queue.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "fmt" 7 | "log" 8 | "strconv" 9 | "sync" 10 | "time" 11 | 12 | "github.com/google/uuid" 13 | "github.com/raymondmars/go-delayqueue/internal/app/notify" 14 | "github.com/raymondmars/go-delayqueue/internal/pkg/common" 15 | ) 16 | 17 | const ( 18 | // The time length of the round is currently set to one hour, that is, 19 | // it takes 1 hour for each cycle of the time wheel; 20 | // the minimum granularity of each step on the default time wheel is 1 second. 21 | WHEEL_SIZE = 3600 22 | REFRESH_POINTER_DEFAULT_SECONDS = 5 23 | ) 24 | 25 | // factory method 26 | type BuildExecutor func(taskMode notify.NotifyMode) notify.Executor 27 | 28 | type SlotRecorder map[string]int 29 | 30 | type ActionEvent func() 31 | 32 | var onceNew sync.Once 33 | var onceStart sync.Once 34 | 35 | var mutex = &sync.RWMutex{} 36 | 37 | var delayQueueInstance *DelayQueue 38 | 39 | type wheel struct { 40 | // all tasks of the time wheel saved in linked table 41 | NotifyTasks *Task 42 | } 43 | 44 | type DelayQueue struct { 45 | // circular queue 46 | TimeWheel [WHEEL_SIZE]wheel 47 | CurrentIndex uint // time wheel current pointer 48 | Persistence 49 | // task executor 50 | TaskExecutor BuildExecutor 51 | 52 | TaskQueryTable SlotRecorder 53 | // ready flag 54 | IsReady bool 55 | } 56 | 57 | // singleton method use redis as persistence layer 58 | func GetDelayQueue(serviceBuilder BuildExecutor) *DelayQueue { 59 | onceNew.Do(func() { 60 | delayQueueInstance = &DelayQueue{ 61 | Persistence: getRedisDb(), 62 | TaskExecutor: serviceBuilder, 63 | TaskQueryTable: make(SlotRecorder), 64 | IsReady: false, 65 | } 66 | }) 67 | return delayQueueInstance 68 | } 69 | 70 | // singleton method use other persistence layer 71 | func GetDelayQueueWithPersis(serviceBuilder BuildExecutor, persistence Persistence) *DelayQueue { 72 | if persistence == nil { 73 | log.Fatalf("persistance is null") 74 | } 75 | onceNew.Do(func() { 76 | delayQueueInstance = &DelayQueue{ 77 | Persistence: persistence, 78 | TaskExecutor: serviceBuilder, 79 | TaskQueryTable: make(SlotRecorder), 80 | IsReady: false, 81 | } 82 | }) 83 | return delayQueueInstance 84 | } 85 | 86 | func (dq *DelayQueue) Start() { 87 | // ensure only excute one time even multi delay queue instances call it 88 | onceStart.Do(dq.init) 89 | } 90 | 91 | func (dq *DelayQueue) init() { 92 | log.Println("delay queue init...") 93 | // load task from cache 94 | dq.loadTasksFromDb() 95 | 96 | // update pointer 97 | dq.CurrentIndex = uint(dq.Persistence.GetWheelTimePointer()) 98 | 99 | // start time wheel 100 | go func() { 101 | for { 102 | select { 103 | case <-time.After(time.Second * 1): 104 | if dq.CurrentIndex >= WHEEL_SIZE { 105 | dq.CurrentIndex = dq.CurrentIndex % WHEEL_SIZE 106 | } 107 | taskLinkHead := dq.TimeWheel[dq.CurrentIndex].NotifyTasks 108 | headIndex := dq.CurrentIndex 109 | 110 | dq.CurrentIndex++ 111 | 112 | // fetch linked list 113 | prev := taskLinkHead 114 | p := taskLinkHead 115 | for p != nil { 116 | if p.CycleCount == 0 { 117 | taskId := p.Id 118 | // Open a new go routing for notifications, speed up each traversal, 119 | // and ensure that the time wheel will not be slowed down 120 | // If there is an exception in the task, try to let the specific business object handle it, 121 | // and the delay queue does not handle the specific business exception. 122 | // This can ensure the business simplicity of the delay queue and avoid problems that are difficult to maintain. 123 | // If there is a problem with a specific business and you need to be notified repeatedly, 124 | // you can add the task back to the queue. 125 | go dq.ExecuteTask(p.TaskMode, p.TaskData) 126 | // delete task 127 | // if the first node 128 | if prev == p { 129 | dq.TimeWheel[headIndex].NotifyTasks = p.Next 130 | prev = p.Next 131 | p = p.Next 132 | } else { 133 | // if it is not the first node 134 | prev.Next = p.Next 135 | p = p.Next 136 | } 137 | // remove the task from the persistent object 138 | dq.Persistence.Delete(taskId) 139 | // remove task from query table 140 | mutex.Lock() 141 | delete(dq.TaskQueryTable, taskId) 142 | mutex.Unlock() 143 | } else { 144 | p.CycleCount-- 145 | prev = p 146 | p = p.Next 147 | } 148 | } 149 | } 150 | } 151 | }() 152 | 153 | // async to update timewheel pointer 154 | go func() { 155 | // refresh pinter internal seconds 156 | refreshInternal, _ := strconv.Atoi(common.GetEvnWithDefaultVal("REFRESH_POINTER_INTERNAL", fmt.Sprintf("%d", REFRESH_POINTER_DEFAULT_SECONDS))) 157 | if refreshInternal < REFRESH_POINTER_DEFAULT_SECONDS { 158 | refreshInternal = REFRESH_POINTER_DEFAULT_SECONDS 159 | } 160 | for { 161 | select { 162 | case <-time.After(time.Second * time.Duration(refreshInternal)): 163 | err := dq.Persistence.SaveWheelTimePointer(int(dq.CurrentIndex)) 164 | if err != nil { 165 | log.Println(err) 166 | } 167 | } 168 | } 169 | 170 | }() 171 | 172 | dq.IsReady = true 173 | } 174 | 175 | func (dq *DelayQueue) loadTasksFromDb() { 176 | tasks := dq.Persistence.GetList() 177 | if tasks != nil && len(tasks) > 0 { 178 | for _, task := range tasks { 179 | delaySeconds := (task.CycleCount * WHEEL_SIZE) + task.WheelPosition 180 | if delaySeconds > 0 { 181 | tk, _ := dq.internalPush(time.Duration(delaySeconds)*time.Second, task.Id, task.TaskMode, task.TaskData, false) 182 | if tk != nil { 183 | dq.TaskQueryTable[task.Id] = task.WheelPosition 184 | } 185 | } 186 | } 187 | } 188 | } 189 | 190 | // Add a task to the delay queue 191 | func (dq *DelayQueue) Push(delaySeconds time.Duration, taskMode notify.NotifyMode, taskData interface{}) (task *Task, err error) { 192 | var pms string 193 | result, ok := taskData.(string) 194 | if !ok { 195 | tp, _ := json.Marshal(taskData) 196 | pms = string(tp) 197 | } else { 198 | pms = result 199 | } 200 | 201 | task, err = dq.internalPush(delaySeconds, "", taskMode, pms, true) 202 | if err == nil { 203 | mutex.Lock() 204 | dq.TaskQueryTable[task.Id] = task.WheelPosition 205 | mutex.Unlock() 206 | } 207 | 208 | return 209 | } 210 | 211 | func (dq *DelayQueue) internalPush(delaySeconds time.Duration, taskId string, taskMode notify.NotifyMode, taskData string, needPresis bool) (*Task, error) { 212 | if int(delaySeconds.Seconds()) == 0 { 213 | errorMsg := fmt.Sprintf("the delay time cannot be less than 1 second, current is: %v", delaySeconds) 214 | return nil, errors.New(errorMsg) 215 | } 216 | 217 | // Start timing from the current time pointer 218 | seconds := int(delaySeconds.Seconds()) 219 | calculateValue := int(dq.CurrentIndex) + seconds 220 | 221 | cycle := calculateValue / WHEEL_SIZE 222 | index := calculateValue % WHEEL_SIZE 223 | 224 | if taskId == "" { 225 | u := uuid.New() 226 | taskId = u.String() 227 | } 228 | task := &Task{ 229 | Id: taskId, 230 | CycleCount: cycle, 231 | WheelPosition: index, 232 | TaskMode: taskMode, 233 | TaskData: taskData, 234 | } 235 | 236 | if cycle > 0 && index <= int(dq.CurrentIndex) { 237 | cycle-- 238 | task.CycleCount = cycle 239 | } 240 | 241 | mutex.Lock() 242 | if dq.TimeWheel[index].NotifyTasks == nil { 243 | dq.TimeWheel[index].NotifyTasks = task 244 | } else { 245 | // Insert a new task into the head of the linked list. 246 | // Since there is no order relationship between tasks, 247 | // this implementation is the easiest 248 | head := dq.TimeWheel[index].NotifyTasks 249 | task.Next = head 250 | dq.TimeWheel[index].NotifyTasks = task 251 | } 252 | mutex.Unlock() 253 | 254 | if needPresis { 255 | dq.Persistence.Save(task) 256 | } 257 | 258 | return task, nil 259 | } 260 | 261 | // execute task 262 | func (dq *DelayQueue) ExecuteTask(taskMode notify.NotifyMode, taskData string) error { 263 | if dq.TaskExecutor != nil { 264 | executor := dq.TaskExecutor(taskMode) 265 | if executor != nil { 266 | log.Printf("Execute task: %d with params: %s\n", taskMode, taskData) 267 | 268 | return executor.DoDelayTask(taskData) 269 | } else { 270 | return errors.New("executor is nil") 271 | } 272 | } else { 273 | return errors.New("task build executor is nil") 274 | } 275 | 276 | } 277 | 278 | // Get the number of tasks on a time wheel 279 | func (dq *DelayQueue) WheelTaskQuantity(index int) int { 280 | tasks := dq.TimeWheel[index].NotifyTasks 281 | if tasks == nil { 282 | return 0 283 | } 284 | k := 0 285 | for p := tasks; p != nil; p = p.Next { 286 | k++ 287 | } 288 | 289 | return k 290 | } 291 | 292 | func (dq *DelayQueue) GetTask(taskId string) *Task { 293 | mutex.Lock() 294 | val, ok := dq.TaskQueryTable[taskId] 295 | mutex.Unlock() 296 | if !ok { 297 | return nil 298 | } else { 299 | tasks := dq.TimeWheel[val].NotifyTasks 300 | for p := tasks; p != nil; p = p.Next { 301 | if p.Id == taskId { 302 | return p 303 | } 304 | } 305 | return nil 306 | } 307 | } 308 | 309 | func (dq *DelayQueue) UpdateTask(taskId string, taskMode notify.NotifyMode, taskData string) error { 310 | task := dq.GetTask(taskId) 311 | if task == nil { 312 | return errors.New("task not found") 313 | } 314 | task.TaskMode = taskMode 315 | task.TaskData = taskData 316 | 317 | // update cache 318 | dq.Persistence.Save(task) 319 | 320 | return nil 321 | } 322 | 323 | func (dq *DelayQueue) DeleteTask(taskId string) error { 324 | mutex.Lock() 325 | defer mutex.Unlock() 326 | val, ok := dq.TaskQueryTable[taskId] 327 | if !ok { 328 | return errors.New("task not found") 329 | } else { 330 | p := dq.TimeWheel[val].NotifyTasks 331 | prev := p 332 | for p != nil { 333 | if p.Id == taskId { 334 | // if current node is root node 335 | if p == prev { 336 | dq.TimeWheel[val].NotifyTasks = p.Next 337 | } else { 338 | prev.Next = p.Next 339 | } 340 | // clear cache 341 | delete(dq.TaskQueryTable, taskId) 342 | dq.Persistence.Delete(taskId) 343 | p = nil 344 | prev = nil 345 | 346 | break 347 | } else { 348 | prev = p 349 | p = p.Next 350 | } 351 | } 352 | return nil 353 | } 354 | } 355 | 356 | func (dq *DelayQueue) RemoveAllTasks() error { 357 | dq.TaskQueryTable = make(SlotRecorder) 358 | for i := 0; i < len(dq.TimeWheel); i++ { 359 | dq.TimeWheel[i].NotifyTasks = nil 360 | } 361 | dq.Persistence.RemoveAll() 362 | return nil 363 | } 364 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE= 2 | github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 3 | github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= 4 | github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= 5 | github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= 6 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 7 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 8 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 9 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= 10 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= 11 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 12 | github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= 13 | github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= 14 | github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI= 15 | github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo= 16 | github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= 17 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 18 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= 19 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= 20 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= 21 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= 22 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= 23 | github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 24 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= 25 | github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= 26 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 27 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 28 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 29 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 30 | github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= 31 | github.com/google/uuid v1.1.2 h1:EVhdT+1Kseyi1/pUmXKaFxYsDNy9RQYkMWRH68J/W7Y= 32 | github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 33 | github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= 34 | github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= 35 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= 36 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 37 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 38 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 39 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 40 | github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= 41 | github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= 42 | github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= 43 | github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 44 | github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= 45 | github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= 46 | github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= 47 | github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= 48 | github.com/onsi/ginkgo/v2 v2.0.0/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c= 49 | github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= 50 | github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= 51 | github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= 52 | github.com/onsi/gomega v1.18.1 h1:M1GfJqGRrBrrGGsbxzV5dqM2U2ApXefZCQpkukxYRLE= 53 | github.com/onsi/gomega v1.18.1/go.mod h1:0q+aL8jAiMXy9hbwj2mr5GziHiwhAIQpFmmtT5hitRs= 54 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 55 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 56 | github.com/rabbitmq/amqp091-go v1.7.0 h1:V5CF5qPem5OGSnEo8BoSbsDGwejg6VUJsKEdneaoTUo= 57 | github.com/rabbitmq/amqp091-go v1.7.0/go.mod h1:wfClAtY0C7bOHxd3GjmF26jEHn+rR/0B3+YV+Vn9/NI= 58 | github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= 59 | github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= 60 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 61 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 62 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 63 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= 64 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 65 | github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= 66 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 67 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 68 | github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= 69 | go.uber.org/goleak v1.2.0 h1:xqgm/S+aQvhWFTtR0XK3Jvg7z8kGV8P4X14IzwN3Eqk= 70 | go.uber.org/goleak v1.2.0/go.mod h1:XJYK+MuIchqpmGmUSAzotztawfKvYLUIgg7guXrwVUo= 71 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 72 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 73 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 74 | golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 75 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 76 | golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 77 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 78 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 79 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 80 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 81 | golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 82 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 83 | golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= 84 | golang.org/x/net v0.0.0-20210428140749-89ef3d95e781 h1:DzZ89McO9/gWPsQXS/FVKAlG02ZjaQ6AlZRBimEYOd0= 85 | golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= 86 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 87 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 88 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 89 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 90 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 91 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 92 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 93 | golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 94 | golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 95 | golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 96 | golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 97 | golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 98 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 99 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 100 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 101 | golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 102 | golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 103 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 104 | golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 105 | golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e h1:fLOSk5Q00efkSvAm+4xcoXD+RRmLmmulPn5I3Y9F2EM= 106 | golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 107 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 108 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 109 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 110 | golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M= 111 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 112 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 113 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 114 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 115 | golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 116 | golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= 117 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 118 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 119 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 120 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 121 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 122 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 123 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= 124 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= 125 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= 126 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 127 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 128 | google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= 129 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 130 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= 131 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 132 | gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= 133 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= 134 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= 135 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 136 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 137 | gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 138 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 139 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 140 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 141 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 142 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 143 | --------------------------------------------------------------------------------