├── .gitignore ├── README.md ├── example └── main.go └── dlock.go /.gitignore: -------------------------------------------------------------------------------- 1 | /example/example 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # dlock [![GoDoc](https://godoc.org/github.com/sameervitian/dlock?status.svg)](https://godoc.org/github.com/sameervitian/dlock) [![CircleCI](https://circleci.com/gh/sameervitian/dlock.svg?style=svg)](https://circleci.com/gh/sameervitian/dlock) 2 | 3 | Distributed lock implementation in Golang using consul 4 | 5 | 6 | 7 | ## Usage 8 | 9 | ##### Dlock Initialization 10 | 11 | ```go 12 | 13 | d, err = dlock.New(&dlock.Config{ConsulKey: "LockKV", LockRetryInterval: time.Second * 10}) 14 | if err != nil { 15 | log.Println("Error ", err) 16 | return 17 | } 18 | 19 | ``` 20 | 21 | ##### Attempt to Acquire Lock 22 | 23 | ```go 24 | 25 | acquireCh := make(chan bool) 26 | releaseCh := make(chan bool) 27 | 28 | for { // loop is to re-attempt for lock acquisition when the lock was initially acquired but auto released after some time 29 | 30 | log.Println("try to acquire lock") 31 | value := map[string]string{ 32 | "key1": "val1", 33 | // Optional keys 34 | // Any number of similar keys can be added with the limit of 512KB. as mentioned here - https://www.consul.io/docs/faq.html#q-what-is-the-per-key-value-size-limitation-for-consul-39-s-key-value-store- 35 | // key named `lockAcquisitionTime` is automatically added. This is the time at which lock is acquired. time is in RFC3339 format 36 | } 37 | go d.RetryLockAcquire(value, acquireCh, releaseCh) // It will keep on attempting for the lock. The re-attempt interval is configured through `LockRetryInterval`, which is set while dlock initialization. 38 | select { 39 | case <-acquireCh: 40 | log.Println("log acquired") 41 | } 42 | <-releaseCh // lock is released due to session invalidation 43 | log.Println("log released") 44 | } 45 | ``` 46 | 47 | `acquireCh` recieves msg when the lock is acquired, otherwise blocks and wait for lock acquisition and compete with others for the lock 48 | 49 | `releaseCh` recieves msg when the lock which was earlier acquired is released due to some reason(consul session invalidation etc) 50 | 51 | ##### Destroy Consul Session and Release lock 52 | 53 | ```go 54 | 55 | if err := d.DestroySession(); err != nil { // Should be called during clean-up. eg reloading the service. Can be done by catching SIGHUP signal 56 | //Destroy session will release the lock and give others a chance to acquire the lock 57 | log.Println(err) 58 | } 59 | 60 | ``` 61 | 62 | ## Authors 63 | 64 | * [Sameer Akhtar](https://github.com/sameervitian) 65 | * [Rishi Bhardwaj](https://github.com/rishitoko) 66 | -------------------------------------------------------------------------------- /example/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "log" 7 | "net/http" 8 | "os" 9 | "os/signal" 10 | "syscall" 11 | "time" 12 | 13 | "github.com/robfig/cron" 14 | "github.com/sameervitian/dlock" 15 | ) 16 | 17 | func main() { 18 | 19 | var port = flag.Int64("port", 9001, "port") 20 | flag.Parse() 21 | 22 | var d *dlock.Dlock 23 | var err error 24 | 25 | go func() { 26 | mcron := NewMCron() 27 | // dlock.SetLogger("/tmp/dlock.log") // If set, dlock logs will be be directed to the file path specified, else on Stdout 28 | d, err = dlock.New(&dlock.Config{ConsulKey: "LockKV", LockRetryInterval: time.Second * 10}) 29 | if err != nil { 30 | log.Println("Error ", err) 31 | return 32 | } 33 | acquireCh := make(chan bool) 34 | releaseCh := make(chan bool) 35 | 36 | for { // loop is to re-attempt for lock acquisition when the lock was initially acquired but auto released after some time 37 | log.Println("try to acquire lock") 38 | hostname, _ := os.Hostname() 39 | value := map[string]string{ 40 | "hostname": hostname, 41 | // Any number of similar keys can be added 42 | // key named `lockAcquisitionTime` is automatically added. This is the time at which lock is acquired. time is in RFC3339 format 43 | } 44 | go d.RetryLockAcquire(value, acquireCh, releaseCh) 45 | select { 46 | case <-acquireCh: 47 | mcron.Start() // Start the cron when lock is acquired 48 | } 49 | <-releaseCh 50 | mcron.Stop() // Stop the cron when lock is released 51 | } 52 | 53 | }() 54 | 55 | errCh := make(chan error) 56 | go func() { 57 | log.Println("http server running on port", *port) 58 | errCh <- http.ListenAndServe(fmt.Sprintf(":%d", *port), nil) 59 | }() 60 | 61 | term := make(chan os.Signal) 62 | signal.Notify(term, os.Interrupt, syscall.SIGTERM) 63 | select { 64 | case <-term: 65 | if err := d.DestroySession(); err != nil { 66 | log.Println(err) 67 | } 68 | time.Sleep(1 * time.Second) 69 | log.Println("Exiting gracefully...") 70 | case err := <-errCh: 71 | log.Println("Error starting web server, exiting gracefully:", err) 72 | } 73 | 74 | } 75 | 76 | // Cron logic 77 | 78 | type MCron struct { 79 | cron *cron.Cron 80 | } 81 | 82 | func NewMCron() *MCron { 83 | mcron := &MCron{} 84 | c := cron.New() 85 | c.AddFunc("*/3 * * * * *", func() { fmt.Println("Every 3 sec") }) 86 | mcron.cron = c 87 | return mcron 88 | } 89 | 90 | func (a *MCron) Start() { 91 | log.Println("cron started") 92 | a.cron.Start() 93 | } 94 | 95 | func (a *MCron) Stop() { 96 | log.Println("cron stopped") 97 | a.cron.Stop() 98 | } 99 | -------------------------------------------------------------------------------- /dlock.go: -------------------------------------------------------------------------------- 1 | package dlock 2 | 3 | import ( 4 | "encoding/json" 5 | "log" 6 | "os" 7 | "time" 8 | 9 | api "github.com/hashicorp/consul/api" 10 | ) 11 | 12 | const ( 13 | // DefaultLockRetryInterval is how long we wait after a failed lock acquisition 14 | DefaultLockRetryInterval = 30 * time.Second 15 | // DefautSessionTTL is ttl for the session created 16 | DefautSessionTTL = 5 * time.Minute 17 | ) 18 | 19 | // Dlock configured for lock acquisition 20 | type Dlock struct { 21 | ConsulClient *api.Client 22 | Key string 23 | SessionID string 24 | LockRetryInterval time.Duration 25 | SessionTTL time.Duration 26 | PermanentRelease bool 27 | } 28 | 29 | // Config is used to configure creation of client 30 | type Config struct { 31 | ConsulKey string // key on which lock to acquire 32 | LockRetryInterval time.Duration // interval at which attempt is done to acquire lock 33 | SessionTTL time.Duration // time after which consul session will expire and release the lock 34 | } 35 | 36 | var logger *log.Logger 37 | 38 | func init() { 39 | logger = log.New(os.Stdout, "dlock:", log.Ldate|log.Ltime|log.Lshortfile) 40 | } 41 | 42 | // New returns a new Dlock object 43 | func New(o *Config) (*Dlock, error) { 44 | var d Dlock 45 | consulClient, err := api.NewClient(api.DefaultConfig()) 46 | if err != nil { 47 | logger.Println("error on creating consul client", err) 48 | return &d, err 49 | } 50 | 51 | d.ConsulClient = consulClient 52 | d.Key = o.ConsulKey 53 | d.LockRetryInterval = DefaultLockRetryInterval 54 | d.SessionTTL = DefautSessionTTL 55 | 56 | if o.LockRetryInterval != 0 { 57 | d.LockRetryInterval = o.LockRetryInterval 58 | } 59 | if o.SessionTTL != 0 { 60 | d.SessionTTL = o.SessionTTL 61 | } 62 | 63 | return &d, nil 64 | } 65 | 66 | // RetryLockAcquire attempts to acquire the lock at `LockRetryInterval` 67 | // First consul session is created and then attempt is done to acquire lock on this session 68 | // Checks configured over Session is all the checks configured for the client itself 69 | // sends msg to chan `acquired` once lock is acquired 70 | // msg is sent to `released` chan when the lock is released due to consul session invalidation 71 | func (d *Dlock) RetryLockAcquire(value map[string]string, acquired chan<- bool, released chan<- bool) { 72 | if d.PermanentRelease { 73 | logger.Printf("lock is permanently released. last session id - %+s", d.SessionID) 74 | return 75 | } 76 | ticker := time.NewTicker(d.LockRetryInterval) 77 | for ; true; <-ticker.C { 78 | value["lockAcquisitionTime"] = time.Now().Format(time.RFC3339) 79 | lock, err := d.acquireLock(value, released) 80 | if err != nil { 81 | logger.Println("error on acquireLock :", err, "retry in -", d.LockRetryInterval) 82 | continue 83 | } 84 | if lock { 85 | logger.Printf("lock acquired with consul session - %s", d.SessionID) 86 | ticker.Stop() 87 | acquired <- true 88 | break 89 | } 90 | } 91 | } 92 | 93 | // DestroySession invalidates the consul session and indirectly release the acquired lock if any 94 | // Should be called in destructor function e.g clean-up, service reload 95 | // this will give others a chance to acquire lock 96 | func (d *Dlock) DestroySession() error { 97 | if d.SessionID == "" { 98 | logger.Printf("cannot destroy empty session") 99 | return nil 100 | } 101 | _, err := d.ConsulClient.Session().Destroy(d.SessionID, nil) 102 | if err != nil { 103 | return err 104 | } 105 | logger.Printf("destroyed consul session - %s", d.SessionID) 106 | d.PermanentRelease = true 107 | return nil 108 | } 109 | 110 | func (d *Dlock) createSession() (string, error) { 111 | return createSession(d.ConsulClient, d.Key, d.SessionTTL) 112 | } 113 | 114 | // SetLogger sets file path for dlock logs 115 | func SetLogger(logpath string) { 116 | f, err := os.OpenFile(logpath, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0644) 117 | if err != nil { 118 | log.Printf("error opening file: %v", err) 119 | return 120 | } 121 | logger = log.New(f, "dlock:", log.Ldate|log.Ltime|log.Lshortfile) 122 | } 123 | 124 | func (d *Dlock) recreateSession() error { 125 | sessionID, err := d.createSession() 126 | if err != nil { 127 | return err 128 | } 129 | d.SessionID = sessionID 130 | return nil 131 | } 132 | 133 | func (d *Dlock) acquireLock(value map[string]string, released chan<- bool) (bool, error) { 134 | if d.SessionID == "" { 135 | err := d.recreateSession() 136 | if err != nil { 137 | return false, err 138 | } 139 | } 140 | b, err := json.Marshal(value) 141 | if err != nil { 142 | logger.Println("error on value marshal", err) 143 | } 144 | lock, err := d.ConsulClient.LockOpts(&api.LockOptions{Key: d.Key, Value: b, Session: d.SessionID, LockWaitTime: 1 * time.Second, LockTryOnce: true}) 145 | if err != nil { 146 | return false, err 147 | } 148 | a, _, err := d.ConsulClient.Session().Info(d.SessionID, nil) 149 | if err == nil && a == nil { 150 | logger.Printf("consul session - %s is invalid now", d.SessionID) 151 | d.SessionID = "" 152 | return false, nil 153 | } 154 | if err != nil { 155 | return false, err 156 | } 157 | 158 | resp, err := lock.Lock(nil) 159 | if err != nil { 160 | return false, err 161 | } 162 | if resp != nil { 163 | doneCh := make(chan struct{}) 164 | go func() { d.ConsulClient.Session().RenewPeriodic(d.SessionTTL.String(), d.SessionID, nil, doneCh) }() 165 | go func() { 166 | <-resp 167 | logger.Printf("lock released with session - %s", d.SessionID) 168 | close(doneCh) 169 | released <- true 170 | }() 171 | return true, nil 172 | } 173 | 174 | return false, nil 175 | } 176 | 177 | func createSession(client *api.Client, consulKey string, ttl time.Duration) (string, error) { 178 | agentChecks, err := client.Agent().Checks() 179 | if err != nil { 180 | logger.Println("error on getting checks", err) 181 | return "", err 182 | } 183 | checks := []string{} 184 | checks = append(checks, "serfHealth") 185 | for _, j := range agentChecks { 186 | checks = append(checks, j.CheckID) 187 | } 188 | 189 | sessionID, _, err := client.Session().Create(&api.SessionEntry{Name: consulKey, Checks: checks, LockDelay: 0 * time.Second, TTL: ttl.String()}, nil) 190 | if err != nil { 191 | return "", err 192 | } 193 | logger.Println("created consul session -", sessionID) 194 | return sessionID, nil 195 | } 196 | --------------------------------------------------------------------------------