├── .gitignore ├── v2 ├── go.mod ├── kettle_test.go ├── redis.go ├── go.sum └── kettle.go ├── .circleci_backup └── config.yml ├── .github └── workflows │ └── main.yml ├── go.mod ├── kettle_test.go ├── redis.go ├── LICENSE ├── examples ├── v2 │ └── simple │ │ └── main.go └── simple │ └── main.go ├── README.md ├── go.sum └── kettle.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, build with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | examples/simple/simple 15 | examples/v2/simple/simple 16 | -------------------------------------------------------------------------------- /v2/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/flowerinthenight/kettle/v2 2 | 3 | go 1.21 4 | 5 | require ( 6 | github.com/go-redsync/redsync v1.4.2 7 | github.com/gofrs/uuid/v5 v5.3.0 8 | github.com/gomodule/redigo v2.0.0+incompatible 9 | ) 10 | 11 | require ( 12 | github.com/hashicorp/errwrap v1.1.0 // indirect 13 | github.com/hashicorp/go-multierror v1.1.1 // indirect 14 | ) 15 | -------------------------------------------------------------------------------- /.circleci_backup/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | jobs: 3 | build: 4 | docker: 5 | - image: circleci/golang:1.14.3 6 | working_directory: /go/src/github.com/flowerinthenight/kettle/ 7 | steps: 8 | - checkout 9 | - run: 10 | name: build 11 | shell: /bin/bash 12 | command: | 13 | cd examples/simple/ 14 | go build -v 15 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: main 3 | 4 | on: 5 | push: 6 | branches: [ master ] 7 | tags: ['*'] 8 | pull_request: 9 | branches: [ master ] 10 | 11 | jobs: 12 | build: 13 | name: Build 14 | if: "!contains(github.event.commits[0].message, 'ci skip')" 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v4 18 | with: 19 | fetch-depth: 0 20 | 21 | - name: Setup Go 22 | uses: actions/setup-go@v4 23 | with: 24 | go-version: '1.23' 25 | 26 | - name: Run tests 27 | run: | 28 | go test -v ./... 29 | 30 | - name: Build sample 31 | run: | 32 | cd examples/v2/simple/ 33 | go build -v 34 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/flowerinthenight/kettle 2 | 3 | go 1.21 4 | 5 | require ( 6 | github.com/fatih/color v1.15.0 7 | github.com/flowerinthenight/kettle/v2 v2.4.3 8 | github.com/go-redsync/redsync v1.4.2 9 | github.com/gofrs/uuid/v5 v5.0.0 10 | github.com/gomodule/redigo v2.0.0+incompatible 11 | github.com/pkg/errors v0.9.1 12 | ) 13 | 14 | require ( 15 | github.com/hashicorp/errwrap v1.1.0 // indirect 16 | github.com/hashicorp/go-multierror v1.1.1 // indirect 17 | github.com/kr/pretty v0.3.1 // indirect 18 | github.com/mattn/go-colorable v0.1.13 // indirect 19 | github.com/mattn/go-isatty v0.0.18 // indirect 20 | github.com/rogpeppe/go-internal v1.10.0 // indirect 21 | github.com/satori/go.uuid v1.2.0 // indirect 22 | golang.org/x/sys v0.7.0 // indirect 23 | ) 24 | -------------------------------------------------------------------------------- /kettle_test.go: -------------------------------------------------------------------------------- 1 | package kettle 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | "time" 7 | ) 8 | 9 | func TestGen(t *testing.T) { 10 | if host := os.Getenv("REDIS_HOST"); host == "" { 11 | t.Log("no redis host:port") 12 | return 13 | } 14 | 15 | k, err := New(WithName("kettle_v0"), WithVerbose(true), WithTickTime(5)) 16 | if err != nil { 17 | t.Fatal(err) 18 | } 19 | 20 | in := StartInput{ 21 | Master: func(v interface{}) error { 22 | kt := v.(*Kettle) 23 | t.Log("from master, name:", kt.Name()) 24 | return nil 25 | }, 26 | MasterCtx: k, 27 | Quit: make(chan error, 1), 28 | Done: make(chan error, 1), 29 | } 30 | 31 | err = k.Start(&in) 32 | if err != nil { 33 | t.Fatal(err) 34 | } 35 | 36 | time.Sleep(time.Second * 3) 37 | in.Quit <- nil // terminate 38 | <-in.Done // wait 39 | } 40 | -------------------------------------------------------------------------------- /v2/kettle_test.go: -------------------------------------------------------------------------------- 1 | package kettle 2 | 3 | import ( 4 | "context" 5 | "os" 6 | "testing" 7 | "time" 8 | ) 9 | 10 | func TestGen(t *testing.T) { 11 | if host := os.Getenv("REDIS_HOST"); host == "" { 12 | t.Log("no redis host:port") 13 | return 14 | } 15 | 16 | k, err := New(WithName("kettle_vx"), WithVerbose(true)) 17 | if err != nil { 18 | t.Fatal(err) 19 | } 20 | 21 | ctx, cancel := context.WithCancel(context.TODO()) 22 | done := make(chan error, 1) 23 | in := StartInput{ 24 | Master: func(v interface{}) error { 25 | kt := v.(*Kettle) 26 | t.Log("from master, name:", kt.Name()) 27 | return nil 28 | }, 29 | MasterCtx: k, 30 | } 31 | 32 | err = k.Start(ctx, &in, done) 33 | if err != nil { 34 | t.Fatal(err) 35 | } 36 | 37 | time.Sleep(time.Second * 5) 38 | cancel() // terminate 39 | <-done // wait 40 | } 41 | -------------------------------------------------------------------------------- /v2/redis.go: -------------------------------------------------------------------------------- 1 | package kettle 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "strconv" 7 | "time" 8 | 9 | "github.com/gomodule/redigo/redis" 10 | ) 11 | 12 | func NewRedisPool() (*redis.Pool, error) { 13 | addr := os.Getenv("REDIS_HOST") 14 | if addr == "" { 15 | return nil, fmt.Errorf("REDIS_HOST not set (host:port)") 16 | } 17 | 18 | var dialOpts []redis.DialOption 19 | password := os.Getenv("REDIS_PASSWORD") 20 | if password != "" { 21 | dialOpts = append(dialOpts, redis.DialPassword(password)) 22 | } 23 | 24 | tm := os.Getenv("REDIS_TIMEOUT_SECONDS") 25 | if tm != "" { 26 | tmsec, err := strconv.Atoi(tm) 27 | if err != nil { 28 | return nil, fmt.Errorf("REDIS_TIMEOUT_SECONDS convert failed: %w", err) 29 | } else { 30 | dialOpts = append(dialOpts, redis.DialConnectTimeout(time.Duration(tmsec)*time.Second)) 31 | } 32 | } 33 | 34 | rp := &redis.Pool{ 35 | MaxIdle: 3, 36 | MaxActive: 4, 37 | Wait: true, 38 | IdleTimeout: 240 * time.Second, 39 | Dial: func() (redis.Conn, error) { return redis.Dial("tcp", addr, dialOpts...) }, 40 | } 41 | 42 | return rp, nil 43 | } 44 | -------------------------------------------------------------------------------- /redis.go: -------------------------------------------------------------------------------- 1 | package kettle 2 | 3 | import ( 4 | "os" 5 | "strconv" 6 | "time" 7 | 8 | "github.com/gomodule/redigo/redis" 9 | "github.com/pkg/errors" 10 | ) 11 | 12 | func NewRedisPool() (*redis.Pool, error) { 13 | addr := os.Getenv("REDIS_HOST") 14 | if addr == "" { 15 | return nil, errors.Errorf("REDIS_HOST env variable must be set (e.g host:port, redis://password@host:port)") 16 | } 17 | 18 | var dialOpts []redis.DialOption 19 | password := os.Getenv("REDIS_PASSWORD") 20 | if password != "" { 21 | dialOpts = append(dialOpts, redis.DialPassword(password)) 22 | } 23 | 24 | tm := os.Getenv("REDIS_TIMEOUT_SECONDS") 25 | if tm != "" { 26 | tmsec, err := strconv.Atoi(tm) 27 | if err != nil { 28 | return nil, err 29 | } else { 30 | dialOpts = append(dialOpts, redis.DialConnectTimeout(time.Duration(tmsec)*time.Second)) 31 | } 32 | } 33 | 34 | rp := &redis.Pool{ 35 | MaxIdle: 3, 36 | MaxActive: 4, 37 | Wait: true, 38 | IdleTimeout: 240 * time.Second, 39 | Dial: func() (redis.Conn, error) { return redis.Dial("tcp", addr, dialOpts...) }, 40 | } 41 | 42 | return rp, nil 43 | } 44 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 flowerinthenight 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 | -------------------------------------------------------------------------------- /v2/go.sum: -------------------------------------------------------------------------------- 1 | github.com/go-redsync/redsync v1.4.2 h1:KADEZ2rlaHMZWnlkthQCxfGP+8ZWwJLiSjOYN3mntKA= 2 | github.com/go-redsync/redsync v1.4.2/go.mod h1:my8/M5YL986u2jBMtZTLkBIgBsKNNSixJWzWwISH6Uw= 3 | github.com/gofrs/uuid/v5 v5.3.0 h1:m0mUMr+oVYUdxpMLgSYCZiXe7PuVPnI94+OMeVBNedk= 4 | github.com/gofrs/uuid/v5 v5.3.0/go.mod h1:CDOjlDMVAtN56jqyRUZh58JT31Tiw7/oQyEXZV+9bD8= 5 | github.com/gomodule/redigo v2.0.0+incompatible h1:K/R+8tc58AaqLkqG2Ol3Qk+DR/TlNuhuh457pBFPtt0= 6 | github.com/gomodule/redigo v2.0.0+incompatible/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4= 7 | github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= 8 | github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= 9 | github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= 10 | github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= 11 | github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= 12 | github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= 13 | github.com/stvp/tempredis v0.0.0-20181119212430-b82af8480203 h1:QVqDTf3h2WHt08YuiTGPZLls0Wq99X9bWd0Q5ZSBesM= 14 | github.com/stvp/tempredis v0.0.0-20181119212430-b82af8480203/go.mod h1:oqN97ltKNihBbwlX8dLpwxCl3+HnXKV/R0e+sRLd9C8= 15 | -------------------------------------------------------------------------------- /examples/v2/simple/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "log" 6 | "time" 7 | 8 | "github.com/flowerinthenight/kettle/v2" 9 | ) 10 | 11 | type app struct { 12 | K *kettle.Kettle 13 | Name string 14 | } 15 | 16 | func (a *app) DoMaster(v interface{}) error { 17 | k := v.(*kettle.Kettle) 18 | log.Printf("[%v] HELLO FROM MASTER", k.Name()) 19 | return nil 20 | } 21 | 22 | func (a app) DoWork() error { 23 | log.Printf("[%v] hello from worker, master=%v", a.Name, a.K.IsMaster()) 24 | return nil 25 | } 26 | 27 | func main() { 28 | // Our app object abstraction. 29 | name := "kettle-simple-example" 30 | a := &app{Name: name} 31 | k, err := kettle.New(kettle.WithName(name), kettle.WithVerbose(true)) 32 | if err != nil { 33 | log.Fatal(err) 34 | } 35 | 36 | a.K = k // store reference to kettle 37 | quit, cancel := context.WithCancel(context.TODO()) 38 | done := make(chan error, 1) 39 | in := kettle.StartInput{ 40 | Master: a.DoMaster, // called when we are master 41 | MasterCtx: k, // context value that is passed to `Master` as parameter 42 | } 43 | 44 | err = k.Start(quit, &in, done) // start kettle 45 | if err != nil { 46 | log.Fatal(err) 47 | } 48 | 49 | // Proceed with our usual (simulated) worker job. 50 | for i := 0; i < 20; i++ { 51 | a.DoWork() 52 | time.Sleep(time.Second * 2) 53 | } 54 | 55 | cancel() // terminate 56 | <-done // wait 57 | } 58 | -------------------------------------------------------------------------------- /examples/simple/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "time" 6 | 7 | "github.com/flowerinthenight/kettle" 8 | ) 9 | 10 | type app struct { 11 | K *kettle.Kettle 12 | Name string 13 | } 14 | 15 | func (a *app) DoMaster(v interface{}) error { 16 | k := v.(*kettle.Kettle) 17 | log.Printf("[%v] HELLO FROM MASTER", k.Name()) 18 | return nil 19 | } 20 | 21 | func (a app) DoWork() error { 22 | log.Printf("[%v] hello from worker, master=%v", a.Name, a.K.IsMaster()) 23 | return nil 24 | } 25 | 26 | func main() { 27 | // Our app object abstraction. 28 | name := "kettle-simple-example" 29 | a := &app{Name: name} 30 | 31 | k, err := kettle.New( 32 | kettle.WithName(name), 33 | kettle.WithVerbose(true), 34 | ) 35 | 36 | if err != nil { 37 | log.Fatal(err) 38 | } 39 | 40 | // Store reference to kettle. 41 | a.K = k 42 | 43 | in := kettle.StartInput{ 44 | Master: a.DoMaster, // called when we are master 45 | MasterCtx: k, // context value that is passed to `Master` as parameter 46 | Quit: make(chan error), // tell kettle to exit 47 | Done: make(chan error), // kettle is done 48 | } 49 | 50 | err = k.Start(&in) // start kettle 51 | if err != nil { 52 | log.Fatal(err) 53 | } 54 | 55 | // Proceed with our usual (simulated) worker job. 56 | for i := 0; i < 20; i++ { 57 | a.DoWork() 58 | time.Sleep(time.Second * 2) 59 | } 60 | 61 | in.Quit <- nil // terminate 62 | <-in.Done // wait 63 | } 64 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![main](https://github.com/flowerinthenight/kettle/actions/workflows/main.yml/badge.svg)](https://github.com/flowerinthenight/kettle/actions/workflows/main.yml) 2 | [![Go Reference](https://pkg.go.dev/badge/github.com/flowerinthenight/kettle.svg)](https://pkg.go.dev/github.com/flowerinthenight/kettle) 3 | 4 | ## Overview 5 | `kettle` is a simple library that abstracts the use of distributed locking to elect a master among group of workers at a specified time interval. The elected master will then call the "master" function. This library uses [Redis](https://redis.io/) as the default [distributed locker](https://redis.io/topics/distlock). 6 | 7 | ## How it works 8 | All workers that share the same name will attempt to grab a Redis lock to become the master. A provided master function will be executed by the node that successfully grabbed the lock. A single node works as well, in which case, that node will run both as master and a worker. 9 | 10 | The main changes in v2.x.x is the use of context for termination and an optional 'done' channel for notification. It looks something like this: 11 | 12 | ```go 13 | name := "kettle-example" 14 | k, _ := kettle.New(kettle.WithName(name), kettle.WithVerbose(true)) 15 | in := kettle.StartInput{ 16 | // Our master callback function. 17 | Master: func(v interface{}) error { 18 | kt := v.(*kettle.Kettle) 19 | log.Println("from master, name:", kt.Name()) 20 | return nil 21 | }, 22 | MasterCtx: k, // arbitrary data that is passed to master function 23 | } 24 | 25 | ctx, cancel := context.WithCancel(context.TODO()) 26 | done := make(chan error, 1) 27 | err = k.Start(ctx, &in, done) 28 | _ = err 29 | 30 | // Simulate work 31 | time.Sleep(time.Second * 5) 32 | cancel() // terminate 33 | <-done // wait 34 | ``` 35 | 36 | 37 | For version 0.x.x, it looks something like this: 38 | 39 | ```go 40 | name := "kettle-example" 41 | k, _ := kettle.New(kettle.WithName(name), kettle.WithVerbose(true)) 42 | in := kettle.StartInput{ 43 | // Our master callback function. 44 | Master: func(v interface{}) error { 45 | kt := v.(*kettle.Kettle) 46 | log.Println("from master, name:", kt.Name()) 47 | return nil 48 | }, 49 | MasterCtx: k, // arbitrary data that is passed to master function 50 | Quit: make(chan error), 51 | Done: make(chan error), 52 | } 53 | 54 | err = k.Start(&in) 55 | _ = err 56 | 57 | // Simulate work 58 | time.Sleep(time.Second * 5) 59 | in.Quit <- nil // terminate 60 | <-in.Done // wait 61 | ``` 62 | 63 | ## Environment variables 64 | ```bash 65 | # Required 66 | REDIS_HOST=1.2.3.4:6379 67 | 68 | # Optional 69 | REDIS_PASSWORD=*** 70 | REDIS_TIMEOUT_SECONDS=5 71 | ``` 72 | 73 | ## Example 74 | A simple example is provided [here](https://github.com/flowerinthenight/kettle/blob/master/examples/v2/simple/main.go) for reference. Try running it simultaneously on multiple nodes. For the version 0.x.x example, check it out [here](https://github.com/flowerinthenight/kettle/blob/master/examples/simple/main.go). 75 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 2 | github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs= 3 | github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw= 4 | github.com/flowerinthenight/kettle/v2 v2.4.3 h1:cMgSOhrSR5gBKwwTWtdFMS85XlmaQNwOTgYv0DrK508= 5 | github.com/flowerinthenight/kettle/v2 v2.4.3/go.mod h1:AwhqP6AxODHojBEhVaqDMYBk7g2ITtESmfOze7mUzQ0= 6 | github.com/go-redsync/redsync v1.4.2 h1:KADEZ2rlaHMZWnlkthQCxfGP+8ZWwJLiSjOYN3mntKA= 7 | github.com/go-redsync/redsync v1.4.2/go.mod h1:my8/M5YL986u2jBMtZTLkBIgBsKNNSixJWzWwISH6Uw= 8 | github.com/gofrs/uuid/v5 v5.0.0 h1:p544++a97kEL+svbcFbCQVM9KFu0Yo25UoISXGNNH9M= 9 | github.com/gofrs/uuid/v5 v5.0.0/go.mod h1:CDOjlDMVAtN56jqyRUZh58JT31Tiw7/oQyEXZV+9bD8= 10 | github.com/gomodule/redigo v2.0.0+incompatible h1:K/R+8tc58AaqLkqG2Ol3Qk+DR/TlNuhuh457pBFPtt0= 11 | github.com/gomodule/redigo v2.0.0+incompatible/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4= 12 | github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= 13 | github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= 14 | github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= 15 | github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= 16 | github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= 17 | github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= 18 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 19 | github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 20 | github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= 21 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 22 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 23 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 24 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 25 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 26 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 27 | github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= 28 | github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= 29 | github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 30 | github.com/mattn/go-isatty v0.0.18 h1:DOKFKCQ7FNG2L1rbrmstDN4QVRdS89Nkh85u68Uwp98= 31 | github.com/mattn/go-isatty v0.0.18/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 32 | github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= 33 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 34 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 35 | github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= 36 | github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= 37 | github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= 38 | github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= 39 | github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= 40 | github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww= 41 | github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= 42 | github.com/stvp/tempredis v0.0.0-20181119212430-b82af8480203 h1:QVqDTf3h2WHt08YuiTGPZLls0Wq99X9bWd0Q5ZSBesM= 43 | github.com/stvp/tempredis v0.0.0-20181119212430-b82af8480203/go.mod h1:oqN97ltKNihBbwlX8dLpwxCl3+HnXKV/R0e+sRLd9C8= 44 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 45 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 46 | golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU= 47 | golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 48 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 49 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 50 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 51 | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= 52 | -------------------------------------------------------------------------------- /v2/kettle.go: -------------------------------------------------------------------------------- 1 | package kettle 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log" 7 | "os" 8 | "sync/atomic" 9 | "time" 10 | 11 | "github.com/go-redsync/redsync" 12 | "github.com/gofrs/uuid/v5" 13 | "github.com/gomodule/redigo/redis" 14 | ) 15 | 16 | // DistLocker abstracts a distributed locker. 17 | type DistLocker interface { 18 | Lock() error 19 | Unlock() (bool, error) 20 | } 21 | 22 | // KettleOption configures Kettle. 23 | type KettleOption interface { 24 | Apply(*Kettle) 25 | } 26 | 27 | type withName string 28 | 29 | // Apply applies a name to a Kettle instance. 30 | func (w withName) Apply(o *Kettle) { o.name = string(w) } 31 | 32 | // WithName configures Kettle instance's name. 33 | func WithName(v string) KettleOption { return withName(v) } 34 | 35 | type withNodeName string 36 | 37 | // Apply applies a name to a Kettle instance. 38 | func (w withNodeName) Apply(o *Kettle) { o.nodeName = string(w) } 39 | 40 | // WithName configures Kettle instance's name. 41 | func WithNodeName(v string) KettleOption { return withNodeName(v) } 42 | 43 | type withVerbose bool 44 | 45 | // Apply applies a verbosity value to a Kettle instance. 46 | func (w withVerbose) Apply(o *Kettle) { o.verbose = bool(w) } 47 | 48 | // WithVerbose configures a Kettle instance's log verbosity. 49 | func WithVerbose(v bool) KettleOption { return withVerbose(v) } 50 | 51 | type withDistLocker struct{ dl DistLocker } 52 | 53 | // Apply applies a distributed locker to a Kettle instance. 54 | func (w withDistLocker) Apply(o *Kettle) { o.lock = w.dl } 55 | 56 | // WithDistLocker configures a Kettle instance's DistLocker. 57 | func WithDistLocker(v DistLocker) KettleOption { return withDistLocker{v} } 58 | 59 | type withTickTime int64 60 | 61 | // Apply applies a tick time interval value to a Kettle instance. 62 | func (w withTickTime) Apply(o *Kettle) { o.tickTime = int64(w) } 63 | 64 | // WithTickTime configures a Kettle instance's tick timer in seconds. 65 | func WithTickTime(v int64) KettleOption { return withTickTime(v) } 66 | 67 | type withLogger struct{ l *log.Logger } 68 | 69 | // Apply applies a logger object to a Kettle instance. 70 | func (w withLogger) Apply(o *Kettle) { o.logger = w.l } 71 | 72 | // WithLogger sets the logger option. 73 | func WithLogger(v *log.Logger) KettleOption { return withLogger{v} } 74 | 75 | // Kettle provides functions that abstract the master election of a group of workers 76 | // at a given interval time. 77 | type Kettle struct { 78 | name string 79 | verbose bool 80 | pool *redis.Pool 81 | lock DistLocker 82 | master int32 // 1 if we are master, otherwise, 0 83 | nodeName string // should be unique per node 84 | startInput *StartInput // copy of StartInput 85 | masterQuit chan error // signal master set to quit 86 | masterDone chan error // master termination done 87 | tickTime int64 88 | logger *log.Logger 89 | } 90 | 91 | // Name returns the instance's name. 92 | func (k Kettle) Name() string { return k.name } 93 | 94 | // Name returns the node's unique name. 95 | func (k Kettle) NodeName() string { return k.nodeName } 96 | 97 | // IsVerbose returns the verbosity setting. 98 | func (k Kettle) IsVerbose() bool { return k.verbose } 99 | 100 | // IsMaster returns master status. 101 | func (k Kettle) IsMaster() bool { return k.isMaster() } 102 | 103 | // Pool returns the configured Redis connection pool. 104 | func (k Kettle) Pool() *redis.Pool { return k.pool } 105 | 106 | func (k Kettle) isMaster() bool { return atomic.LoadInt32(&k.master) == 1 } 107 | 108 | func (k *Kettle) setMaster() { 109 | if err := k.lock.Lock(); err != nil { 110 | atomic.StoreInt32(&k.master, 0) 111 | return 112 | } 113 | 114 | atomic.StoreInt32(&k.master, 1) 115 | if k.verbose { 116 | k.logger.Printf("[%v] %v set to master", k.name, k.nodeName) 117 | } 118 | } 119 | 120 | func (k *Kettle) doMaster() { 121 | masterTicker := time.NewTicker(time.Second * time.Duration(k.tickTime)) 122 | 123 | f := func() { 124 | // Attempt to be master here. 125 | k.setMaster() 126 | 127 | // Only if we are master. 128 | if k.isMaster() { 129 | if k.startInput.Master != nil { 130 | k.startInput.Master(k.startInput.MasterCtx) 131 | } 132 | } 133 | } 134 | 135 | f() // first invoke before tick 136 | 137 | go func() { 138 | for { 139 | select { 140 | case <-masterTicker.C: 141 | f() // succeeding ticks 142 | case <-k.masterQuit: 143 | k.masterDone <- nil 144 | return 145 | } 146 | } 147 | }() 148 | } 149 | 150 | // StartInput configures the Start function. 151 | type StartInput struct { 152 | Master func(ctx interface{}) error // function to call every time we are master 153 | MasterCtx interface{} // callback function parameter 154 | } 155 | 156 | // Start starts Kettle's main function. The ctx parameter is mainly used for termination 157 | // with an optional done channel for us to notify when we are done, if any. 158 | func (k *Kettle) Start(ctx context.Context, in *StartInput, done ...chan error) error { 159 | if in == nil { 160 | return fmt.Errorf("input cannot be nil") 161 | } 162 | 163 | k.startInput = in 164 | if k.nodeName == "" { 165 | hostname, _ := os.Hostname() 166 | id, _ := uuid.NewV4() 167 | hostname = hostname + fmt.Sprintf("__%v", id) 168 | k.nodeName = hostname 169 | } 170 | 171 | k.masterQuit = make(chan error, 1) 172 | k.masterDone = make(chan error, 1) 173 | 174 | go func() { 175 | <-ctx.Done() 176 | k.masterQuit <- nil 177 | <-k.masterDone 178 | if len(done) > 0 { 179 | done[0] <- nil 180 | } 181 | }() 182 | 183 | go k.doMaster() 184 | return nil 185 | } 186 | 187 | // New returns an instance of Kettle. 188 | func New(opts ...KettleOption) (*Kettle, error) { 189 | k := &Kettle{name: "kettle", tickTime: 30} 190 | for _, opt := range opts { 191 | opt.Apply(k) 192 | } 193 | 194 | if k.logger == nil { 195 | k.logger = log.New(os.Stdout, "[kettle] ", 0) 196 | } 197 | 198 | if k.lock == nil { 199 | pool, err := NewRedisPool() 200 | if err != nil { 201 | return nil, fmt.Errorf("NewRedisPool failed: %w", err) 202 | } 203 | 204 | k.pool = pool 205 | pools := []redsync.Pool{pool} 206 | rs := redsync.New(pools) 207 | k.lock = rs.NewMutex( 208 | fmt.Sprintf("%v-distlocker", k.name), 209 | redsync.SetExpiry(time.Second*time.Duration(k.tickTime-1)), 210 | redsync.SetTries(1), 211 | ) 212 | } 213 | 214 | return k, nil 215 | } 216 | -------------------------------------------------------------------------------- /kettle.go: -------------------------------------------------------------------------------- 1 | package kettle 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "os" 7 | "sync/atomic" 8 | "time" 9 | 10 | "github.com/fatih/color" 11 | "github.com/go-redsync/redsync" 12 | "github.com/gofrs/uuid/v5" 13 | "github.com/gomodule/redigo/redis" 14 | "github.com/pkg/errors" 15 | ) 16 | 17 | var ( 18 | red = color.New(color.FgRed).SprintFunc() 19 | green = color.New(color.FgGreen).SprintFunc() 20 | ) 21 | 22 | // DistLocker abstracts a distributed locker. 23 | type DistLocker interface { 24 | Lock() error 25 | Unlock() (bool, error) 26 | } 27 | 28 | // KettleOption configures Kettle. 29 | type KettleOption interface { 30 | Apply(*Kettle) 31 | } 32 | 33 | type withName string 34 | 35 | // Apply applies a name to a Kettle instance. 36 | func (w withName) Apply(o *Kettle) { o.name = string(w) } 37 | 38 | // WithName configures Kettle instance's name. 39 | func WithName(v string) KettleOption { return withName(v) } 40 | 41 | type withVerbose bool 42 | 43 | // Apply applies a verbosity value to a Kettle instance. 44 | func (w withVerbose) Apply(o *Kettle) { o.verbose = bool(w) } 45 | 46 | // WithVerbose configures a Kettle instance's log verbosity. 47 | func WithVerbose(v bool) KettleOption { return withVerbose(v) } 48 | 49 | type withDistLocker struct{ dl DistLocker } 50 | 51 | // Apply applies a distributed locker to a Kettle instance. 52 | func (w withDistLocker) Apply(o *Kettle) { o.lock = w.dl } 53 | 54 | // WithDistLocker configures a Kettle instance's DistLocker. 55 | func WithDistLocker(v DistLocker) KettleOption { return withDistLocker{v} } 56 | 57 | type withTickTime int64 58 | 59 | // Apply applies a tick time interval value to a Kettle instance. 60 | func (w withTickTime) Apply(o *Kettle) { o.tickTime = int64(w) } 61 | 62 | // WithTickTime configures a Kettle instance's tick timer in seconds. 63 | func WithTickTime(v int64) KettleOption { return withTickTime(v) } 64 | 65 | // Kettle provides functions that abstract the master election of a group of workers 66 | // at a given interval time. 67 | type Kettle struct { 68 | name string 69 | verbose bool 70 | pool *redis.Pool 71 | lock DistLocker 72 | master int32 // 1 if we are master, otherwise, 0 73 | hostname string 74 | startInput *StartInput // copy of StartInput 75 | masterQuit chan error // signal master set to quit 76 | masterDone chan error // master termination done 77 | tickTime int64 78 | } 79 | 80 | // Name returns the instance's name. 81 | func (k Kettle) Name() string { return k.name } 82 | 83 | // IsVerbose returns the verbosity setting. 84 | func (k Kettle) IsVerbose() bool { return k.verbose } 85 | 86 | // IsMaster returns master status. 87 | func (k Kettle) IsMaster() bool { return k.isMaster() } 88 | 89 | // Pool returns the configured Redis connection pool. 90 | func (k Kettle) Pool() *redis.Pool { return k.pool } 91 | 92 | func (k Kettle) info(v ...interface{}) { 93 | if !k.verbose { 94 | return 95 | } 96 | 97 | m := fmt.Sprintln(v...) 98 | log.Printf("%s %s", green("[info]"), m) 99 | } 100 | 101 | func (k Kettle) infof(format string, v ...interface{}) { 102 | if !k.verbose { 103 | return 104 | } 105 | 106 | m := fmt.Sprintf(format, v...) 107 | log.Printf("%s %s", green("[info]"), m) 108 | } 109 | 110 | func (k Kettle) error(v ...interface{}) { 111 | if !k.verbose { 112 | return 113 | } 114 | 115 | m := fmt.Sprintln(v...) 116 | log.Printf("%s %s", red("[error]"), m) 117 | } 118 | 119 | func (k Kettle) errorf(format string, v ...interface{}) { 120 | if !k.verbose { 121 | return 122 | } 123 | 124 | m := fmt.Sprintf(format, v...) 125 | log.Printf("%s %s", red("[error]"), m) 126 | } 127 | 128 | func (k Kettle) fatal(v ...interface{}) { 129 | k.error(v...) 130 | os.Exit(1) 131 | } 132 | 133 | func (k Kettle) fatalf(format string, v ...interface{}) { 134 | k.errorf(format, v...) 135 | os.Exit(1) 136 | } 137 | 138 | func (k Kettle) isMaster() bool { 139 | if atomic.LoadInt32(&k.master) == 1 { 140 | return true 141 | } else { 142 | return false 143 | } 144 | } 145 | 146 | func (k *Kettle) setMaster() { 147 | if err := k.lock.Lock(); err != nil { 148 | atomic.StoreInt32(&k.master, 0) 149 | return 150 | } 151 | 152 | k.infof("[%v] %v set to master", k.name, k.hostname) 153 | atomic.StoreInt32(&k.master, 1) 154 | } 155 | 156 | func (k *Kettle) doMaster() { 157 | masterTicker := time.NewTicker(time.Second * time.Duration(k.tickTime)) 158 | 159 | f := func() { 160 | // Attempt to be master here. 161 | k.setMaster() 162 | 163 | // Only if we are master. 164 | if k.isMaster() { 165 | if k.startInput.Master != nil { 166 | k.startInput.Master(k.startInput.MasterCtx) 167 | } 168 | } 169 | } 170 | 171 | f() // first invoke before tick 172 | 173 | go func() { 174 | for { 175 | select { 176 | case <-masterTicker.C: 177 | f() // succeeding ticks 178 | case <-k.masterQuit: 179 | k.masterDone <- nil 180 | return 181 | } 182 | } 183 | }() 184 | } 185 | 186 | // StartInput configures the Start function. 187 | type StartInput struct { 188 | Master func(ctx interface{}) error // function to call every time we are master 189 | MasterCtx interface{} // callback function parameter 190 | Quit chan error // signal for us to terminate 191 | Done chan error // report that we are done 192 | } 193 | 194 | // Start starts Kettle's main function. 195 | func (k *Kettle) Start(in *StartInput) error { 196 | if in == nil { 197 | return errors.Errorf("input cannot be nil") 198 | } 199 | 200 | k.startInput = in 201 | hostname, _ := os.Hostname() 202 | id, _ := uuid.NewV4() 203 | hostname = hostname + fmt.Sprintf("__%v", id) 204 | k.hostname = hostname 205 | 206 | k.masterQuit = make(chan error, 1) 207 | k.masterDone = make(chan error, 1) 208 | 209 | go func() { 210 | <-in.Quit 211 | k.infof("[%v] requested to terminate", k.name) 212 | 213 | // Attempt to gracefully terminate master. 214 | k.masterQuit <- nil 215 | <-k.masterDone 216 | 217 | k.infof("[%v] terminate complete", k.name) 218 | in.Done <- nil 219 | }() 220 | 221 | go k.doMaster() 222 | 223 | return nil 224 | } 225 | 226 | // New returns an instance of Kettle. 227 | func New(opts ...KettleOption) (*Kettle, error) { 228 | k := &Kettle{ 229 | name: "kettle", 230 | tickTime: 30, 231 | } 232 | 233 | for _, opt := range opts { 234 | opt.Apply(k) 235 | } 236 | 237 | if k.lock == nil { 238 | pool, err := NewRedisPool() 239 | if err != nil { 240 | return nil, err 241 | } 242 | 243 | k.pool = pool 244 | pools := []redsync.Pool{pool} 245 | rs := redsync.New(pools) 246 | k.lock = rs.NewMutex( 247 | fmt.Sprintf("%v-distlocker", k.name), 248 | redsync.SetExpiry(time.Second*time.Duration(k.tickTime)), 249 | ) 250 | } 251 | 252 | return k, nil 253 | } 254 | --------------------------------------------------------------------------------