├── .github └── workflows │ └── go.yml ├── .gitignore ├── K8sLease.md ├── LICENSE ├── README.md ├── example ├── applicationCache │ └── main.go ├── awsDynamoDb │ ├── docker-compose.yml │ └── main.go ├── basicLocal │ └── main.go ├── postgres │ ├── docker-compose.yml │ └── main.go └── redis │ └── basic │ └── main.go ├── go.mod ├── go.sum ├── goInterval.go ├── goInterval_lock.go ├── goInterval_test.go └── material ├── gointerlock.png └── gointerlock_bg.png /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: Go 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | 11 | container-job: 12 | 13 | runs-on: ubuntu-latest 14 | 15 | services: 16 | 17 | redis: 18 | # Docker Hub image 19 | image: redis 20 | # Set health checks to wait until redis has started 21 | options: >- 22 | --health-cmd "redis-cli ping" 23 | --health-interval 10s 24 | --health-timeout 5s 25 | --health-retries 5 26 | ports: 27 | - 6379:6379 28 | steps: 29 | - uses: actions/checkout@v2 30 | 31 | - name: Set up Go 32 | uses: actions/setup-go@v2 33 | with: 34 | go-version: 1.17 35 | 36 | - name: Build 37 | run: go build -v ./... 38 | 39 | - name: Test 40 | run: go test ./... -RedisHost localhost:6379 41 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /K8sLease.md: -------------------------------------------------------------------------------- 1 | 2 | # Kubernetes Lease 3 | 4 | Kubernetes provides a built-in leader election mechanism using the **Lease API**. 5 | 6 | The **Lease** object ensures that only one replica holds the lease at any given time, allowing it to perform the task while others wait for the next opportunity. 7 | 8 | Here’s an example of how to use Kubernetes’ built-in leader election using the **Lease** resource in your application. 9 | 10 | 11 | ```go 12 | 13 | package main 14 | 15 | import ( 16 | "context" 17 | "fmt" 18 | "os" 19 | "time" 20 | 21 | "k8s.io/client-go/kubernetes" 22 | "k8s.io/client-go/rest" 23 | "k8s.io/client-go/tools/clientcmd" 24 | "k8s.io/client-go/tools/leaderelection" 25 | "k8s.io/client-go/tools/leaderelection/resourcelock" 26 | "k8s.io/client-go/util/retry" 27 | ) 28 | 29 | func main() { 30 | 31 | config, err := rest.InClusterConfig() 32 | if err != nil { 33 | // Fallback to use kubeconfig if not running inside a cluster 34 | config, err = clientcmd.BuildConfigFromFlags("", clientcmd.RecommendedHomeFile) 35 | if err != nil { 36 | fmt.Printf("Failed to create Kubernetes client config: %v", err) 37 | os.Exit(1) 38 | } 39 | } 40 | 41 | clientSet, err := kubernetes.NewForConfig(config) 42 | if err != nil { 43 | fmt.Printf("Failed to create Kubernetes clientSet: %v", err) 44 | os.Exit(1) 45 | } 46 | 47 | // Define the lease lock for leader election 48 | leaseLock := &resourcelock.LeaseLock{ 49 | LeaseMeta: metav1.ObjectMeta{ 50 | Name: "example-lease", 51 | Namespace: "default", 52 | }, 53 | Client: clientSet.CoordinationV1(), 54 | LockConfig: resourcelock.ResourceLockConfig{ 55 | Identity: os.Getenv("POD_NAME"), 56 | }, 57 | } 58 | 59 | ctx, cancel := context.WithCancel(context.Background()) 60 | defer cancel() 61 | 62 | // Set up leader election 63 | leaderelection.RunOrDie(ctx, leaderelection.LeaderElectionConfig{ 64 | Lock: leaseLock, 65 | // The duration that the leader holds the lock. Other pods can only acquire the lock if the current leader does not renew it within this time. 66 | LeaseDuration: 15 * time.Second, 67 | // The time by which the leader must renew its lock. 68 | RenewDeadline: 10 * time.Second, 69 | // The interval at which the leader tries to acquire or renew the lock. 70 | RetryPeriod: 2 * time.Second, 71 | Callbacks: leaderelection.LeaderCallbacks{ 72 | OnStartedLeading: func(c context.Context) { 73 | // When the pod becomes the leader 74 | fmt.Println("I am the leader, performing the task...") 75 | executeTask(c) 76 | }, 77 | OnStoppedLeading: func() { 78 | // when leadership is lost 79 | fmt.Println("I am not the leader anymore, stopping tasks...") 80 | }, 81 | }, 82 | }) 83 | } 84 | 85 | // only the leader should execute this 86 | func executeTask(ctx context.Context) { 87 | ticker := time.NewTicker(2 * time.Second) 88 | defer ticker.Stop() 89 | 90 | for { 91 | select { 92 | case <-ctx.Done(): 93 | // Leadership is lost or context is canceled 94 | fmt.Println("Context canceled, stopping task...") 95 | return 96 | case <-ticker.C: 97 | // Execute task every tick 98 | fmt.Println("Leader executing task...") 99 | } 100 | } 101 | } 102 | 103 | 104 | ``` 105 | 106 | 107 | ### RBAC Permissions 108 | 109 | ```yaml 110 | apiVersion: rbac.authorization.k8s.io/v1 111 | kind: Role 112 | metadata: 113 | namespace: default 114 | name: leader-election-role 115 | rules: 116 | - apiGroups: ["coordination.k8s.io"] 117 | resources: ["leases"] 118 | verbs: ["get", "watch", "list", "delete", "update", "create"] 119 | --- 120 | apiVersion: rbac.authorization.k8s.io/v1 121 | kind: RoleBinding 122 | metadata: 123 | name: leader-election-binding 124 | namespace: default 125 | roleRef: 126 | apiGroup: rbac.authorization.k8s.io 127 | kind: Role 128 | name: leader-election-role 129 | subjects: 130 | - kind: ServiceAccount 131 | name: default 132 | namespace: default 133 | ``` 134 | 135 | 136 | ### Ensure that the POD_NAME environment variable is injected into each pod 137 | ```yaml 138 | env: 139 | - name: POD_NAME 140 | valueFrom: 141 | fieldRef: 142 | fieldPath: metadata.name 143 | ``` 144 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Jay Ehsaniara 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # goInterLock 2 | ![Go Interval Lock](material/gointerlock_bg.png) 3 | 4 | _known as: ⏰ Interval (Cron / Job / Task / Scheduler) Go Centralized Lock ⏱️_ 5 | 6 | ## Go Interval job/task , with centralized Lock for Distributed Systems 7 | 8 | `goInterLock` is go job/task scheduler with centralized locking mechanism. In distributed system locking is preventing task been executed in every instant that has the scheduler, 9 | 10 | > **_Note:_** Kubernetes has the Lease object, which you can use to implement the `Leader election`. [Here is a working example of using kubernetes Lease API](K8sLease.md) 11 | 12 | **For example:** if your application has a task of calling some external APIs or doing some DB querying every 10 minutes, the lock prevents the process been run in every instance of that application, and you ended up running that task multiple time every 10 minutes. 13 | 14 | Quick Start 15 | 16 | ```shell 17 | go get github.com/ehsaniara/gointerlock 18 | ``` 19 | 20 | Supported Lock 21 | - [Redis](#redis) 22 | - [AWS DynamoDB](#aws-dynamodb) 23 | - Postgres DB 24 | 25 | # Local Scheduler (Single App) 26 | 27 | (Interval every 2 seconds) 28 | 29 | ```go 30 | var job = gointerlock.GoInterval{ 31 | Interval: 2 * time.Second, 32 | Arg: myJob, 33 | } 34 | err := job.Run(ctx) 35 | if err != nil { 36 | log.Fatalf("Error: %s", err) 37 | } 38 | ``` 39 | 40 | ### Examples 41 | [**Basic Local Task:**](example/basicLocal/main.go) Simple Task Interval (Single App). 42 | 43 | [**Application Cache:**](./example/applicationCache/main.go) An example of periodically cached value update on http server. 44 | 45 | ------ 46 | # Distributed Mode (Scaled Up) 47 | 48 | ## Redis (Recommended) 49 | 50 | ### Existing Redis Connection 51 | you should already configure your Redis connection and pass it into the `GoInterLock`. Also make sure you are giving the 52 | unique name per job 53 | 54 | Step 1: configure redis connection `redisConnection.Rdb` from the existing application and pass it into the Job. for example: 55 | ```go 56 | var redisConnector = redis.NewClient(&redis.Options{ 57 | Addr: "localhost:6379", 58 | Password: "myRedisPassword", 59 | DB: 0, 60 | }) 61 | ``` 62 | Step 2: Pass the redis connection into the `GoInterval` 63 | 64 | ```go 65 | var job = gointerlock.GoInterval{ 66 | Interval: 2 * time.Second, 67 | Arg: myJob, 68 | Name: "MyTestJob", 69 | RedisConnector: redisConnector, 70 | } 71 | err := jobTicker.Run(ctx) 72 | if err != nil { 73 | log.Fatalf("Error: %s", err) 74 | } 75 | ``` 76 | 77 | in both examples `myJob` is your function, for example: 78 | 79 | ```go 80 | func myJob() { 81 | fmt.Println(time.Now(), " - called") 82 | } 83 | ``` 84 | > Currently `GoInterLock` does not support any argument for the job function 85 | 86 | ### Built in Redis Connector 87 | 88 | another way is to use an existing redis connection: 89 | 90 | ```go 91 | var job = gointerlock.GoInterval{ 92 | Name: "MyTestJob", 93 | Interval: 2 * time.Second, 94 | Arg: myJob, 95 | RedisHost: "localhost:6379", 96 | RedisPassword: "myRedisPassword", //if no pass leave it as "" 97 | } 98 | err := job.Run(context.Background()) 99 | if err != nil { 100 | log.Fatalf("Error: %s", err) 101 | } 102 | ``` 103 | 104 | ##### GoInterLock is using [go-redis](https://github.com/go-redis/redis) for Redis Connection. 105 | 106 | 107 | 108 | ### Examples 109 | 110 | [**Basic Distributed Task:**](example/redis/basic/main.go) Simple Task Interval with Redis Lock. 111 | 112 | ----- 113 | 114 | ## AWS DynamoDb 115 | 116 | ### Basic Config (Local Environment) 117 | This ia sample of local DynamoDb (Docker) for your local test. 118 | ```go 119 | var job = gointerlock.GoInterval{ 120 | Name: "MyTestJob", 121 | Interval: 2 * time.Second, 122 | Arg: myJob, 123 | LockVendor: gointerlock.AwsDynamoDbLock, 124 | AwsDynamoDbRegion: "us-east-1", 125 | AwsDynamoDbEndpoint: "http://127.0.0.1:8000", 126 | AwsDynamoDbSecretAccessKey: "dummy", 127 | AwsDynamoDbAccessKeyID: "dummy", 128 | } 129 | err := job.Run(cnx) 130 | if err != nil { 131 | log.Fatalf("Error: %s", err) 132 | } 133 | ``` 134 | task: 135 | ```go 136 | func myJob() { 137 | fmt.Println(time.Now(), " - called") 138 | } 139 | ``` 140 | > You can get the docker-compose file from [AWS DynamoDB Docker compose](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/DynamoDBLocal.DownloadingAndRunning.html) , also avalble from: [docker-compose.yml](./example/awsDynamoDb/docker-compose.yml). 141 | 142 | ### Using AWS Profile 143 | 144 | `goInterLock` will get credentials from the AWS profile 145 | 146 | ```go 147 | var job = gointerlock.GoInterval{ 148 | Name: "MyTestJob", 149 | Interval: 2 * time.Second, 150 | Arg: myJob, 151 | } 152 | err := job.Run(cnx) 153 | if err != nil { 154 | log.Fatalf("Error: %s", err) 155 | } 156 | ``` 157 | ### Examples 158 | 159 | [**Basic Distributed Task:**](example/awsDynamoDb/main.go) Simple Task Interval with DynamoDb Lock. 160 | 161 | 162 | 163 | # :four_leaf_clover: Centralized lock for the Distributed Systems (No Timer/Scheduler) 164 | 165 | ```go 166 | 167 | import ( 168 | "context" 169 | "fmt" 170 | "time" 171 | 172 | "github.com/go-redis/redis/v8" 173 | ) 174 | 175 | type DistributedLock interface { 176 | Lock(ctx context.Context, lockName string, maxLockingDuration time.Duration) bool 177 | UnLock(ctx context.Context, lockName string) 178 | } 179 | 180 | func NewDistributedLock(rdb *redis.Client) DistributedLock { 181 | return &distributedLock{ 182 | rdb: rdb, 183 | } 184 | } 185 | 186 | type distributedLock struct { 187 | rdb *redis.Client 188 | } 189 | 190 | // Lock return TRUE when successfully locked, return FALSE if it's already been locked by others 191 | func (d distributedLock) Lock(ctx context.Context, lockName string, maxLockingDuration time.Duration) bool { 192 | key := fmt.Sprintf("lock_%s", lockName) 193 | //check if it's already locked 194 | iter := d.rdb.Scan(ctx, 0, key, 0).Iterator() 195 | for iter.Next(ctx) { 196 | //exit if lock exist 197 | return false 198 | } 199 | //then lock it then 200 | d.rdb.Set(ctx, key, []byte("true"), maxLockingDuration) 201 | return true 202 | } 203 | 204 | func (d distributedLock) UnLock(ctx context.Context, lockName string) { 205 | key := fmt.Sprintf("lock_%s", lockName) 206 | //remove the lock 207 | d.rdb.Del(ctx, key) 208 | } 209 | 210 | ``` 211 | -------------------------------------------------------------------------------- /example/applicationCache/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "github.com/ehsaniara/gointerlock" 7 | "log" 8 | "net/http" 9 | "time" 10 | ) 11 | 12 | func main() { 13 | cnx := context.Background() 14 | 15 | var expensiveQueryResult = fmt.Sprintf("Last result at: %v", time.Now()) 16 | 17 | //introduced the task 18 | go func() { 19 | //setting up the scheduler parameter 20 | var job = gointerlock.GoInterval{ 21 | LockVendor: gointerlock.SingleApp, //optional 22 | Interval: 5 * time.Second, 23 | Arg: func() { 24 | expensiveQueryResult = fmt.Sprintf("Last result at: %v", time.Now()) 25 | }, 26 | } 27 | 28 | // start the scheduler 29 | _ = job.Run(cnx) 30 | }() 31 | 32 | // http Server, access http://localhost:8080 from your browser 33 | http.HandleFunc("/", func(writer http.ResponseWriter, request *http.Request) { 34 | _, _ = fmt.Fprintln(writer, expensiveQueryResult) 35 | }) 36 | fmt.Println("Server started at port 8080") 37 | log.Fatal(http.ListenAndServe(":8080", nil)) 38 | } 39 | -------------------------------------------------------------------------------- /example/awsDynamoDb/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.8' 2 | services: 3 | dynamodb-local: 4 | command: "-jar DynamoDBLocal.jar -sharedDb -dbPath ./data" 5 | image: "amazon/dynamodb-local:latest" 6 | container_name: dynamodb-local 7 | ports: 8 | - "8000:8000" 9 | volumes: 10 | - "./docker/dynamodb:/home/dynamodblocal/data" 11 | working_dir: /home/dynamodblocal -------------------------------------------------------------------------------- /example/awsDynamoDb/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "github.com/ehsaniara/gointerlock" 7 | "log" 8 | "time" 9 | ) 10 | 11 | func myJob() { 12 | fmt.Println(time.Now(), " - called") 13 | } 14 | 15 | func main() { 16 | cnx := context.Background() 17 | 18 | //test cron 19 | go func() { 20 | 21 | var job = gointerlock.GoInterval{ 22 | Name: "MyTestJob", 23 | Interval: 2 * time.Second, 24 | Arg: myJob, 25 | LockVendor: gointerlock.AwsDynamoDbLock, 26 | AwsDynamoDbRegion: "us-east-1", 27 | AwsDynamoDbEndpoint: "http://127.0.0.1:8000", 28 | AwsDynamoDbSecretAccessKey: "dummy", 29 | AwsDynamoDbAccessKeyID: "dummy", 30 | } 31 | err := job.Run(cnx) 32 | if err != nil { 33 | log.Fatalf("Error: %s", err) 34 | } 35 | }() 36 | 37 | //example: just run it for 10 second before application exits 38 | time.Sleep(10 * time.Second) 39 | } 40 | -------------------------------------------------------------------------------- /example/basicLocal/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "github.com/ehsaniara/gointerlock" 7 | "log" 8 | "time" 9 | ) 10 | 11 | func myJob() { 12 | fmt.Println(time.Now(), " - called") 13 | } 14 | 15 | func main() { 16 | cnx := context.Background() 17 | 18 | var job = gointerlock.GoInterval{ 19 | LockVendor: gointerlock.SingleApp, //optional 20 | Interval: 2 * time.Second, 21 | Arg: myJob, 22 | } 23 | 24 | //test cron 25 | go func() { 26 | err := job.Run(cnx) 27 | if err != nil { 28 | log.Fatalf("Error: %s", err) 29 | } 30 | }() 31 | 32 | //example: just run it for 10 second before application exits 33 | time.Sleep(10 * time.Second) 34 | } 35 | -------------------------------------------------------------------------------- /example/postgres/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.8' 2 | services: 3 | postgres: 4 | image: "postgres:10.20-alpine" 5 | environment: 6 | POSTGRES_USER: guest 7 | POSTGRES_PASSWORD: guest 8 | POSTGRES_DB: locks 9 | ports: 10 | - "5432:5432" -------------------------------------------------------------------------------- /example/postgres/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log" 7 | "time" 8 | 9 | "github.com/ehsaniara/gointerlock" 10 | ) 11 | 12 | func myJob(s string) { 13 | fmt.Printf("%s - called %s\n", time.Now().String(), s) 14 | } 15 | 16 | func main() { 17 | cnx := context.Background() 18 | 19 | //test cron 20 | go func() { 21 | 22 | var job = gointerlock.GoInterval{ 23 | Name: "MyTestJob", 24 | Interval: 2 * time.Second, 25 | Arg: func() { 26 | myJob("job1") 27 | }, 28 | LockVendor: gointerlock.PostgresLock, 29 | PostgresConnStr: "postgresql://guest:guest@localhost:5432/locks?sslmode=disable", 30 | } 31 | err := job.Run(cnx) 32 | if err != nil { 33 | log.Fatalf("Error: %s", err) 34 | } 35 | }() 36 | 37 | //test cron 38 | go func() { 39 | 40 | var job = gointerlock.GoInterval{ 41 | Name: "MyTestJob", 42 | Interval: 3 * time.Second, 43 | Arg: func() { 44 | myJob("job2") 45 | }, 46 | LockVendor: gointerlock.PostgresLock, 47 | PostgresConnStr: "postgresql://guest:guest@localhost:5432/locks?sslmode=disable", 48 | } 49 | err := job.Run(cnx) 50 | if err != nil { 51 | log.Fatalf("Error: %s", err) 52 | } 53 | }() 54 | 55 | //example: just run it for 10 second before application exits 56 | time.Sleep(20 * time.Second) 57 | } 58 | -------------------------------------------------------------------------------- /example/redis/basic/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log" 7 | "time" 8 | 9 | "github.com/ehsaniara/gointerlock" 10 | ) 11 | 12 | func myJob() { 13 | fmt.Println(time.Now(), " - called") 14 | } 15 | 16 | func main() { 17 | cnx := context.Background() 18 | 19 | var job = gointerlock.GoInterval{ 20 | LockVendor: gointerlock.RedisLock, 21 | Name: "MyTestJob", 22 | Interval: 2 * time.Second, 23 | Arg: myJob, 24 | RedisHost: "localhost:6379", 25 | RedisPassword: "secret", 26 | } 27 | 28 | //test cron 29 | go func() { 30 | err := job.Run(cnx) 31 | if err != nil { 32 | log.Fatalf("Error: %s", err) 33 | } 34 | }() 35 | 36 | //example: just run it for 10 second before application exits 37 | time.Sleep(10 * time.Second) 38 | } 39 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/ehsaniara/gointerlock 2 | 3 | go 1.17 4 | 5 | require ( 6 | github.com/aws/aws-sdk-go v1.44.22 7 | github.com/go-redis/redis/v8 v8.11.5 8 | github.com/lib/pq v1.10.6 9 | ) 10 | 11 | require ( 12 | github.com/cespare/xxhash/v2 v2.1.2 // indirect 13 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect 14 | github.com/jmespath/go-jmespath v0.4.0 // indirect 15 | ) 16 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/aws/aws-sdk-go v1.40.16 h1:Tgg7i9ee2j6ir2EfejPDJBB3PyfUM4dPlvmMLtvJVfo= 2 | github.com/aws/aws-sdk-go v1.40.16/go.mod h1:585smgzpB/KqRA+K3y/NL/oYRqQvpNJYvLm+LY1U59Q= 3 | github.com/aws/aws-sdk-go v1.44.22 h1:StP+vxaFzl445mSML6KzgiTcqpA+eVwbO5fMNvhVN7c= 4 | github.com/aws/aws-sdk-go v1.44.22/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX4oIKwKHZo= 5 | github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY= 6 | github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 7 | github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE= 8 | github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 9 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 10 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= 11 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= 12 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 13 | github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= 14 | github.com/go-redis/redis/v8 v8.11.1 h1:Aqf/1y2eVfE9zrySM++/efzwv3mkLH7n/T96//gbo94= 15 | github.com/go-redis/redis/v8 v8.11.1/go.mod h1:DLomh7y2e3ggQXQLd1YgmvIfecPJoFl7WU5SOQ/r06M= 16 | github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI= 17 | github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo= 18 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 19 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= 20 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= 21 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= 22 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= 23 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= 24 | github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 25 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 26 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 27 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 28 | github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 29 | github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= 30 | github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= 31 | github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= 32 | github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= 33 | github.com/lib/pq v1.10.6 h1:jbk+ZieJ0D7EVGJYpL9QTz7/YW6UHbmdnZWYyK5cdBs= 34 | github.com/lib/pq v1.10.6/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= 35 | github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= 36 | github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 37 | github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= 38 | github.com/onsi/ginkgo v1.15.0/go.mod h1:hF8qUzuuC8DJGygJH3726JnCZX4MYbRB8yFfISqnKUg= 39 | github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= 40 | github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= 41 | github.com/onsi/gomega v1.10.5/go.mod h1:gza4q3jKQJijlu05nKWRCW/GavJumGt8aNRxWg7mt48= 42 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 43 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 44 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 45 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 46 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 47 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 48 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 49 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 50 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 51 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 52 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 53 | golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 54 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 55 | golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 56 | golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 57 | golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= 58 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 59 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 60 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 61 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 62 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 63 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 64 | golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 65 | golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 66 | golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 67 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 68 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 69 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 70 | golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 71 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 72 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 73 | golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 74 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 75 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 76 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 77 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 78 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 79 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 80 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 81 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 82 | golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 83 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 84 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 85 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 86 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 87 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 88 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 89 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= 90 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= 91 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= 92 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 93 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 94 | gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= 95 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= 96 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 97 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 98 | gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 99 | -------------------------------------------------------------------------------- /goInterval.go: -------------------------------------------------------------------------------- 1 | package gointerlock 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "log" 7 | "time" 8 | 9 | "github.com/go-redis/redis/v8" 10 | ) 11 | 12 | var locker Lock 13 | 14 | type LockVendor int32 15 | 16 | const ( 17 | SingleApp LockVendor = 0 // no distributed lock 18 | RedisLock LockVendor = 1 19 | AwsDynamoDbLock LockVendor = 2 20 | PostgresLock LockVendor = 3 21 | ) 22 | 23 | type GoInterval struct { 24 | 25 | //Name: is a unique job/task name, this is needed for distribution lock, this value enables the distribution mode. for local uses you don't need to set this value 26 | Name string 27 | 28 | // Arg: the func that need to be call in every period 29 | Arg func() 30 | 31 | // Interval: Timer Interval 32 | Interval time.Duration 33 | 34 | LockVendor LockVendor 35 | 36 | //redis connection--------- 37 | 38 | // RedisConnector : in case your app has redis connection configured already 39 | RedisConnector *redis.Client 40 | 41 | // RedisHost Redis Host the default value "localhost:6379" 42 | RedisHost string 43 | 44 | // RedisPassword: Redis Password (AUTH), It can be blank if Redis has no authentication req 45 | RedisPassword string 46 | 47 | // 0 , It's from 0 to 15 (Not for redis cluster) 48 | RedisDB string 49 | 50 | // DynamoDb 51 | 52 | //leave empty to get from ~/.aws/credentials, (if AwsDynamoDbEndpoint not provided) 53 | AwsDynamoDbRegion string 54 | 55 | //leave empty to get from ~/.aws/credentials 56 | AwsDynamoDbEndpoint string 57 | 58 | //leave empty to get from ~/.aws/credentials, StaticCredentials (if AwsDynamoDbEndpoint not provided) 59 | AwsDynamoDbAccessKeyID string 60 | 61 | //leave empty to get from ~/.aws/credentials, StaticCredentials (if AwsDynamoDbEndpoint not provided) 62 | AwsDynamoDbSecretAccessKey string 63 | 64 | //leave empty to get from ~/.aws/credentials, StaticCredentials (if AwsDynamoDbEndpoint not provided) 65 | AwsDynamoDbSessionToken string 66 | 67 | // Postgres 68 | 69 | PostgresConnStr string 70 | 71 | // internal use, it should not get modified 72 | timer *time.Timer 73 | } 74 | 75 | // Run to start the interval timer 76 | func (t *GoInterval) Run(ctx context.Context) error { 77 | 78 | if ctx == nil { 79 | ctx = context.Background() 80 | } 81 | 82 | if t.Interval == 0 { 83 | return errors.New("`Time Interval is missing!`") 84 | } 85 | 86 | if t.Arg == nil { 87 | return errors.New("`What this timer should to run?`") 88 | } 89 | 90 | //To check if it's a distributed system, support older version v1.0.3 91 | if t.Name != "" { 92 | if t.LockVendor == 0 { 93 | //default one, to support pre. Versions 94 | t.LockVendor = RedisLock 95 | } 96 | } 97 | 98 | switch t.LockVendor { 99 | case RedisLock: 100 | r := &RedisLocker{ 101 | redisConnector: t.RedisConnector, 102 | Name: t.Name, 103 | RedisHost: t.RedisHost, 104 | RedisPassword: t.RedisPassword, 105 | RedisDB: t.RedisDB, 106 | } 107 | err := r.SetClient() 108 | if err != nil { 109 | return err 110 | } 111 | 112 | locker = r 113 | case AwsDynamoDbLock: 114 | d := &DynamoDbLocker{ 115 | AwsDynamoDbRegion: t.AwsDynamoDbRegion, 116 | AwsDynamoDbEndpoint: t.AwsDynamoDbEndpoint, 117 | AwsDynamoDbAccessKeyID: t.AwsDynamoDbAccessKeyID, 118 | AwsDynamoDbSecretAccessKey: t.AwsDynamoDbSecretAccessKey, 119 | AwsDynamoDbSessionToken: t.AwsDynamoDbSessionToken, 120 | } 121 | err := d.SetClient() 122 | if err != nil { 123 | return err 124 | } 125 | 126 | locker = d 127 | case PostgresLock: 128 | p := &PostgresLocker{ 129 | Name: t.Name, 130 | PostgresConnStr: t.PostgresConnStr, 131 | } 132 | err := p.SetClient() 133 | if err != nil { 134 | return err 135 | } 136 | 137 | locker = p 138 | } 139 | 140 | t.updateTimer() 141 | 142 | for { 143 | select { 144 | case <-ctx.Done(): 145 | log.Printf("Job %s terminated!", t.Name) 146 | return nil 147 | default: 148 | 149 | <-t.timer.C 150 | 151 | //lock 152 | lock, err := t.isNotLockThenLock(ctx) 153 | if err != nil { 154 | log.Fatalf("err: %v", err) 155 | return nil 156 | } 157 | if lock { 158 | // run the task 159 | t.Arg() 160 | 161 | t.UnLock(ctx) 162 | } 163 | t.updateTimer() 164 | } 165 | } 166 | } 167 | 168 | func (t *GoInterval) isNotLockThenLock(ctx context.Context) (bool, error) { 169 | //lock 170 | if t.LockVendor == SingleApp { 171 | return true, nil 172 | } 173 | locked, err := locker.Lock(ctx, t.Name, t.Interval) 174 | 175 | if err != nil { 176 | log.Fatalf("err:%v", err) 177 | return false, err 178 | } 179 | return locked, nil 180 | } 181 | 182 | func (t *GoInterval) UnLock(ctx context.Context) { 183 | //unlock 184 | 185 | if t.LockVendor == SingleApp { 186 | return 187 | } 188 | 189 | err := locker.UnLock(ctx, t.Name) 190 | if err != nil { 191 | return 192 | } 193 | } 194 | 195 | func (t *GoInterval) updateTimer() { 196 | next := time.Now() 197 | if !next.After(time.Now()) { 198 | next = next.Add(t.Interval) 199 | } 200 | diff := next.Sub(time.Now()) 201 | if t.timer == nil { 202 | t.timer = time.NewTimer(diff) 203 | } else { 204 | t.timer.Reset(diff) 205 | } 206 | } 207 | -------------------------------------------------------------------------------- /goInterval_lock.go: -------------------------------------------------------------------------------- 1 | package gointerlock 2 | 3 | import ( 4 | "context" 5 | "database/sql" 6 | "errors" 7 | "fmt" 8 | "log" 9 | "time" 10 | 11 | "github.com/aws/aws-sdk-go/aws" 12 | "github.com/aws/aws-sdk-go/aws/credentials" 13 | "github.com/aws/aws-sdk-go/aws/session" 14 | "github.com/aws/aws-sdk-go/service/dynamodb" 15 | "github.com/aws/aws-sdk-go/service/dynamodb/dynamodbattribute" 16 | "github.com/aws/aws-sdk-go/service/dynamodb/expression" 17 | "github.com/go-redis/redis/v8" 18 | "github.com/lib/pq" 19 | ) 20 | 21 | const Prefix = "GoInterLock" 22 | 23 | const ( 24 | UniqueViolationErr = pq.ErrorCode("23505") 25 | ) 26 | 27 | type Lock interface { 28 | Lock(ctx context.Context, key string, interval time.Duration) (success bool, err error) 29 | UnLock(ctx context.Context, key string) (err error) 30 | SetClient() error 31 | } 32 | 33 | type RedisLocker struct { 34 | redisConnector *redis.Client 35 | 36 | // Name: is a unique job/task name, this is needed for distribution lock, this value enables the distribution mode. for local uses you don't need to set this value 37 | Name string 38 | 39 | // RedisHost Redis Host the default value "localhost:6379" 40 | RedisHost string 41 | 42 | // RedisPassword: Redis Password (AUTH), It can be blank if Redis has no authentication req 43 | RedisPassword string 44 | 45 | // 0 , It's from 0 to 15 (Not for redis cluster) 46 | RedisDB string 47 | } 48 | 49 | func (r *RedisLocker) SetClient() error { 50 | 51 | //already has connection 52 | if r.redisConnector != nil { 53 | return nil 54 | } 55 | 56 | log.Printf("Job %s started in distributed mode!", r.Name) 57 | 58 | //if Redis host missed, use the default one 59 | if r.RedisHost == "" { 60 | r.RedisHost = "localhost:6379" 61 | } 62 | 63 | r.redisConnector = redis.NewClient(&redis.Options{ 64 | Addr: r.RedisHost, 65 | Password: r.RedisPassword, // no password set 66 | DB: 0, // use default DB 67 | }) 68 | 69 | log.Printf("Job %s started in distributed mode by provided redis connection", r.Name) 70 | return nil 71 | } 72 | 73 | func (r *RedisLocker) Lock(ctx context.Context, key string, lockTtl time.Duration) (success bool, err error) { 74 | 75 | if r.redisConnector != nil { 76 | 77 | if key == "" { 78 | return false, errors.New("`Distributed Jobs should have a unique name!`") 79 | } 80 | 81 | res, err := r.redisConnector.SetNX(ctx, fmt.Sprintf("%s_%s", Prefix, key), time.Now().String(), lockTtl).Result() 82 | if err != nil { 83 | return false, err 84 | } 85 | return res, nil 86 | } 87 | 88 | return false, errors.New("`No Redis Connection found`") 89 | } 90 | 91 | func (r *RedisLocker) UnLock(ctx context.Context, key string) error { 92 | if r.redisConnector != nil { 93 | return r.redisConnector.Del(ctx, fmt.Sprintf("%s_%s", Prefix, key)).Err() 94 | } else { 95 | return nil 96 | } 97 | } 98 | 99 | type DynamoDbLocker struct { 100 | dynamoClient *dynamodb.DynamoDB 101 | 102 | //leave empty to get from ~/.aws/credentials, (if AwsDynamoDbEndpoint not provided) 103 | AwsDynamoDbRegion string 104 | 105 | //leave empty to get from ~/.aws/credentials 106 | AwsDynamoDbEndpoint string 107 | 108 | //leave empty to get from ~/.aws/credentials, StaticCredentials (if AwsDynamoDbEndpoint not provided) 109 | AwsDynamoDbAccessKeyID string 110 | 111 | //leave empty to get from ~/.aws/credentials, StaticCredentials (if AwsDynamoDbEndpoint not provided) 112 | AwsDynamoDbSecretAccessKey string 113 | 114 | //leave empty to get from ~/.aws/credentials, StaticCredentials (if AwsDynamoDbEndpoint not provided) 115 | AwsDynamoDbSessionToken string 116 | } 117 | 118 | func (d *DynamoDbLocker) SetClient() error { 119 | 120 | // override the AWS profile credentials 121 | if aws.String(d.AwsDynamoDbEndpoint) == nil { 122 | // Initialize a session that the SDK will use to load 123 | // credentials from the shared credentials file ~/.aws/credentials 124 | // and region from the shared configuration file ~/.aws/config. 125 | sess := session.Must(session.NewSessionWithOptions(session.Options{ 126 | SharedConfigState: session.SharedConfigEnable, 127 | })) 128 | // Create DynamoDB client 129 | d.dynamoClient = dynamodb.New(sess) 130 | } else { 131 | 132 | if aws.String(d.AwsDynamoDbRegion) == nil { 133 | return errors.New("`AwsDynamoDbRegion is missing (AWS Region)`") 134 | } 135 | 136 | //setting StaticCredentials 137 | awsConfig := &aws.Config{ 138 | Credentials: credentials.NewStaticCredentials(d.AwsDynamoDbAccessKeyID, d.AwsDynamoDbSecretAccessKey, d.AwsDynamoDbSessionToken), 139 | Region: aws.String(d.AwsDynamoDbRegion), 140 | Endpoint: aws.String(d.AwsDynamoDbEndpoint), 141 | } 142 | sess, err := session.NewSession(awsConfig) 143 | if err != nil { 144 | return err 145 | } 146 | // Create DynamoDB client 147 | d.dynamoClient = dynamodb.New(sess) 148 | } 149 | 150 | //sess, err := session.NewSession(&aws.Config{ 151 | // Region: aws.String("us-west-2"), 152 | // Credentials: credentials.NewStaticCredentials(conf.AWS_ACCESS_KEY_ID, conf.AWS_SECRET_ACCESS_KEY, ""), 153 | //}) 154 | 155 | if d.dynamoClient == nil { 156 | return errors.New("`DynamoDb Connection Failed!`") 157 | } 158 | 159 | //check if table exist, if not create one 160 | tableInput := &dynamodb.CreateTableInput{ 161 | AttributeDefinitions: []*dynamodb.AttributeDefinition{ 162 | { 163 | AttributeName: aws.String("id"), 164 | AttributeType: aws.String("S"), 165 | }, 166 | }, 167 | KeySchema: []*dynamodb.KeySchemaElement{ 168 | { 169 | AttributeName: aws.String("id"), 170 | KeyType: aws.String("HASH"), 171 | }, 172 | }, 173 | ProvisionedThroughput: &dynamodb.ProvisionedThroughput{ 174 | ReadCapacityUnits: aws.Int64(10), 175 | WriteCapacityUnits: aws.Int64(10), 176 | }, 177 | //TimeToLiveDescription: &dynamodb.TimeToLiveDescription{ 178 | // AttributeName: aws.String("ttl"), 179 | // TimeToLiveStatus: aws.String("enable"), 180 | //}, 181 | TableName: aws.String(Prefix), 182 | } 183 | 184 | _, err := d.dynamoClient.CreateTable(tableInput) 185 | if err != nil { 186 | log.Printf("Got error calling CreateTable: %s", err) 187 | } else { 188 | fmt.Println("Created the table", Prefix) 189 | } 190 | 191 | return nil 192 | } 193 | 194 | func (d *DynamoDbLocker) Lock(ctx context.Context, key string, lockTtl time.Duration) (success bool, err error) { 195 | 196 | if d.dynamoClient == nil { 197 | return false, errors.New("`No Redis Connection found`") 198 | } 199 | 200 | expr, _ := expression.NewBuilder().WithFilter( 201 | //filter by id 202 | expression.Name("id").Equal(expression.Value(key)), 203 | ).Build() 204 | 205 | // Build the query input parameters 206 | params := &dynamodb.ScanInput{ 207 | ExpressionAttributeNames: expr.Names(), 208 | ExpressionAttributeValues: expr.Values(), 209 | FilterExpression: expr.Filter(), 210 | TableName: aws.String(Prefix), 211 | } 212 | 213 | // Make the DynamoDB Query API call 214 | result, _ := d.dynamoClient.ScanWithContext(ctx, params) 215 | 216 | if len(result.Items) > 0 { 217 | return false, nil 218 | } 219 | 220 | _, errPut := d.dynamoClient.PutItemWithContext(ctx, &dynamodb.PutItemInput{ 221 | Item: DynamoDbUnlockMarshal(key), 222 | TableName: aws.String(Prefix), 223 | }) 224 | 225 | if errPut != nil { 226 | return false, errPut 227 | } 228 | 229 | return true, nil 230 | 231 | } 232 | 233 | func (d *DynamoDbLocker) UnLock(ctx context.Context, key string) error { 234 | if d.dynamoClient != nil { 235 | _, _ = d.dynamoClient.DeleteItemWithContext(ctx, &dynamodb.DeleteItemInput{ 236 | Key: DynamoDbUnlockMarshal(key), 237 | TableName: aws.String(Prefix), 238 | }) 239 | } 240 | return nil 241 | } 242 | 243 | func DynamoDbUnlockMarshal(key string) map[string]*dynamodb.AttributeValue { 244 | lockObj, _ := dynamodbattribute.MarshalMap(struct { 245 | Id string `json:"id"` 246 | }{ 247 | Id: key, 248 | }) 249 | return lockObj 250 | } 251 | 252 | type PostgresLocker struct { 253 | postgresConnector *sql.DB 254 | 255 | // Name: is a unique job/task name, this is needed for distribution lock, this value enables the distribution mode. for local uses you don't need to set this value 256 | Name string 257 | 258 | // PostgresHost - Postgres Host the default value "localhost:5672" 259 | PostgresHost string 260 | 261 | // PostgresPassword: Redis Password (AUTH), It can be blank if Redis has no authentication req 262 | PostgresPassword string 263 | 264 | // 0 , It's from 0 to 15 (Not for redis cluster) 265 | PostgresDB string 266 | 267 | PostgresConnStr string 268 | } 269 | 270 | func (r *PostgresLocker) SetClient() error { 271 | 272 | //already has connection 273 | if r.postgresConnector != nil { 274 | return nil 275 | } 276 | 277 | log.Printf("Job %s started in distributed mode!", r.Name) 278 | 279 | //if Postgres host missed, use the default one 280 | if r.PostgresHost == "" { 281 | r.PostgresHost = "localhost:5432" 282 | } 283 | 284 | db, err := sql.Open("postgres", r.PostgresConnStr) 285 | if err != nil { 286 | log.Fatal(err) 287 | } 288 | 289 | r.postgresConnector = db 290 | 291 | err = r.setupTable(db) 292 | if err != nil { 293 | log.Fatal(err) 294 | } 295 | 296 | log.Printf("Job %s started in distributed mode by provided postgres connection", r.Name) 297 | return nil 298 | } 299 | 300 | func (r *PostgresLocker) setupTable(db *sql.DB) error { 301 | query := `CREATE TABLE IF NOT EXISTS locks ( 302 | id text NOT NULL, 303 | created_at timestamp NOT NULL, 304 | ttl integer, 305 | PRIMARY KEY (id) 306 | )` 307 | res, err := r.postgresConnector.ExecContext(context.Background(), query) 308 | if err != nil { 309 | return err 310 | } 311 | 312 | fmt.Print(res) 313 | return nil 314 | } 315 | 316 | func (r *PostgresLocker) Lock(ctx context.Context, key string, lockTtl time.Duration) (success bool, err error) { 317 | 318 | if r.postgresConnector != nil { 319 | 320 | if key == "" { 321 | return false, errors.New("`Distributed Jobs should have a unique name!`") 322 | } 323 | 324 | res, err := r.postgresConnector.ExecContext(ctx, "INSERT into locks values ($1,$2,$3)", fmt.Sprintf("%s_%s", Prefix, key), time.Now(), lockTtl.Seconds()) 325 | if err != nil { 326 | if IsErrorCode(err, UniqueViolationErr) { 327 | return false, nil 328 | } 329 | return false, err 330 | } 331 | affected, err := res.RowsAffected() 332 | if err != nil { 333 | return false, errors.New("`Couldn't Acquire Lock`") 334 | } 335 | return affected >= 1, nil 336 | } 337 | 338 | return false, errors.New("`No Postgres Connection found`") 339 | } 340 | 341 | func (r *PostgresLocker) UnLock(ctx context.Context, key string) error { 342 | if r.postgresConnector != nil { 343 | res, err := r.postgresConnector.ExecContext(ctx, "DELETE FROM locks WHERE id = $1", fmt.Sprintf("%s_%s", Prefix, key)) 344 | if err != nil { 345 | return err 346 | } 347 | _, err = res.RowsAffected() 348 | if err != nil { 349 | return errors.New("`Couldn't Remove Lock`") 350 | } 351 | return nil 352 | } else { 353 | return nil 354 | } 355 | } 356 | 357 | func IsErrorCode(err error, errcode pq.ErrorCode) bool { 358 | if pgerr, ok := err.(*pq.Error); ok { 359 | return pgerr.Code == errcode 360 | } 361 | return false 362 | } 363 | -------------------------------------------------------------------------------- /goInterval_test.go: -------------------------------------------------------------------------------- 1 | package gointerlock_test 2 | 3 | import ( 4 | "context" 5 | "flag" 6 | "fmt" 7 | "github.com/ehsaniara/gointerlock" 8 | "github.com/go-redis/redis/v8" 9 | "log" 10 | "testing" 11 | "time" 12 | ) 13 | 14 | var ( 15 | ctx = context.Background() 16 | rdb *redis.Client 17 | ) 18 | var ( 19 | RedisHost string 20 | RedisPass string 21 | ) 22 | 23 | func init() { 24 | flag.StringVar(&RedisPass, "RedisPass", "", "Redis Password") 25 | flag.StringVar(&RedisHost, "RedisHost", "localhost:6379", "Redis Host") 26 | } 27 | 28 | func init() { 29 | rdb = redis.NewClient(&redis.Options{ 30 | Addr: ":6379", 31 | DialTimeout: 10 * time.Second, 32 | ReadTimeout: 30 * time.Second, 33 | WriteTimeout: 30 * time.Second, 34 | PoolSize: 10, 35 | PoolTimeout: 30 * time.Second, 36 | }) 37 | } 38 | 39 | func ExampleNewClient() { 40 | 41 | rdb := redis.NewClient(&redis.Options{ 42 | Addr: RedisHost, // use default Addr 43 | Password: RedisPass, // no password set 44 | DB: 0, // use default DB 45 | }) 46 | 47 | pong, err := rdb.Ping(ctx).Result() 48 | fmt.Println(pong, err) 49 | // Output: PONG 50 | } 51 | 52 | func TestGoInterval_Run(t *testing.T) { 53 | var counter = 0 54 | var job1 = gointerlock.GoInterval{ 55 | Interval: 100 * time.Millisecond, 56 | Arg: func() { 57 | counter++ 58 | }, 59 | } 60 | go func() { 61 | _ = job1.Run(context.Background()) 62 | }() 63 | time.Sleep(2 * time.Second) 64 | 65 | log.Printf("counter %d", counter) 66 | if counter != 19 { 67 | t.Error("counter should be 19") 68 | } 69 | 70 | } 71 | 72 | func TestGoInterval_LockCheck(t *testing.T) { 73 | var counter = 0 74 | cnx := context.Background() 75 | var job2 = gointerlock.GoInterval{ 76 | Name: "job2", 77 | RedisHost: RedisHost, 78 | RedisPassword: RedisPass, 79 | Interval: 1 * time.Second, 80 | Arg: func() { 81 | counter++ 82 | }, 83 | } 84 | time.Sleep(1 * time.Second) 85 | //instance 1 replication 86 | go func() { 87 | _ = job2.Run(cnx) 88 | }() 89 | //instance 2 replication 90 | go func() { 91 | _ = job2.Run(cnx) 92 | }() 93 | //instance 3 replication 94 | go func() { 95 | _ = job2.Run(cnx) 96 | }() 97 | time.Sleep(2 * time.Second) 98 | 99 | log.Printf("counter %d", counter) 100 | if counter != 1 { 101 | t.Error("counter should be 1") 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /material/gointerlock.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ehsaniara/gointerlock/01e16f56f5ea064c1a29e8969dd74bf0f96273fd/material/gointerlock.png -------------------------------------------------------------------------------- /material/gointerlock_bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ehsaniara/gointerlock/01e16f56f5ea064c1a29e8969dd74bf0f96273fd/material/gointerlock_bg.png --------------------------------------------------------------------------------