├── LICENSE ├── db.go ├── mutex.go ├── mutex_test.go └── readme.md /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013, Ryan Smith 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 15 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 16 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 17 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 18 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 19 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 20 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 21 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 22 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 23 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | -------------------------------------------------------------------------------- /db.go: -------------------------------------------------------------------------------- 1 | package ddbsync 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "github.com/bmizerany/aws4" 8 | "io/ioutil" 9 | "net/http" 10 | "os" 11 | "time" 12 | ) 13 | 14 | const ( 15 | opPutItem = "DynamoDB_20111205.PutItem" 16 | opGetItem = "DynamoDB_20111205.GetItem" 17 | opDeleteItem = "DynamoDB_20111205.DeleteItem" 18 | ) 19 | 20 | type item struct { 21 | Name string 22 | Created int64 23 | } 24 | 25 | type responseError struct { 26 | resp *http.Response 27 | } 28 | 29 | type s struct { 30 | S string 31 | } 32 | 33 | type n struct { 34 | N int64 `json:",string"` 35 | } 36 | 37 | type database struct { 38 | keys *aws4.Keys 39 | s *aws4.Service 40 | } 41 | 42 | var db = &database{ 43 | keys: &aws4.Keys{ 44 | AccessKey: os.Getenv("AWS_ACCESS_KEY"), 45 | SecretKey: os.Getenv("AWS_SECRET_KEY"), 46 | }, 47 | s: &aws4.Service{ 48 | Name: "dynamodb", 49 | Region: "us-east-1", 50 | }, 51 | } 52 | 53 | func (db *database) put(name string, created int64) error { 54 | type T struct { 55 | TableName string 56 | Item struct { 57 | Name s 58 | Created n 59 | } 60 | Expected struct { 61 | Name struct { 62 | Exists bool 63 | } 64 | } 65 | } 66 | 67 | t := new(T) 68 | t.TableName = "Locks" 69 | t.Item.Name.S = name 70 | t.Item.Created.N = created 71 | t.Expected.Name.Exists = false 72 | 73 | resp, err := db.do(opPutItem, t) 74 | if err != nil { 75 | return err 76 | } 77 | defer resp.Body.Close() 78 | 79 | if resp.StatusCode != 200 { 80 | b, err := ioutil.ReadAll(resp.Body) 81 | if err != nil { 82 | return err 83 | } 84 | return fmt.Errorf("updateMinute error: %d %q", resp.StatusCode, string(b)) 85 | } 86 | 87 | return nil 88 | } 89 | 90 | func (db *database) get(name string) (*item, error) { 91 | type T struct { 92 | TableName string 93 | ConsistentRead bool 94 | Key struct { 95 | HashKeyElement s 96 | } 97 | AttributesToGet []string 98 | } 99 | 100 | t := new(T) 101 | t.TableName = "Locks" 102 | t.ConsistentRead = true 103 | t.Key.HashKeyElement.S = name 104 | t.AttributesToGet = []string{"Name", "Created"} 105 | 106 | resp, err := db.do(opGetItem, t) 107 | if err != nil { 108 | return nil, err 109 | } 110 | defer resp.Body.Close() 111 | 112 | if resp.StatusCode != 200 { 113 | b, err := ioutil.ReadAll(resp.Body) 114 | if err != nil { 115 | return nil, err 116 | } 117 | return nil, fmt.Errorf("get error: %d %q", resp.StatusCode, string(b)) 118 | } 119 | 120 | type R struct { 121 | Item struct { 122 | Name s 123 | Created n 124 | } 125 | } 126 | r := new(R) 127 | if err := json.NewDecoder(resp.Body).Decode(r); err != nil { 128 | return nil, err 129 | } 130 | 131 | if r.Item.Name.S == "" { 132 | return nil, nil 133 | } 134 | return &item{r.Item.Name.S, r.Item.Created.N}, nil 135 | } 136 | 137 | func (db *database) delete(name string) error { 138 | type T struct { 139 | TableName string 140 | Key struct { 141 | HashKeyElement s 142 | } 143 | } 144 | 145 | t := new(T) 146 | t.TableName = "Locks" 147 | t.Key.HashKeyElement.S = name 148 | 149 | resp, err := db.do(opDeleteItem, t) 150 | if err != nil { 151 | return err 152 | } 153 | 154 | defer resp.Body.Close() 155 | 156 | if resp.StatusCode != 200 { 157 | b, err := ioutil.ReadAll(resp.Body) 158 | if err != nil { 159 | return err 160 | } 161 | return fmt.Errorf("delete error: %d %q", resp.StatusCode, string(b)) 162 | } 163 | return nil 164 | } 165 | 166 | func (db *database) do(op string, v interface{}) (*http.Response, error) { 167 | b := new(bytes.Buffer) 168 | if err := json.NewEncoder(b).Encode(v); err != nil { 169 | panic(err) 170 | } 171 | 172 | r, _ := http.NewRequest("POST", "https://dynamodb.us-east-1.amazonaws.com/", b) 173 | r.Header.Set("Date", time.Now().UTC().Format(http.TimeFormat)) 174 | r.Header.Set("X-Amz-Target", op) 175 | r.Header.Set("Content-Type", "application/x-amz-json-1.0") 176 | 177 | err := db.s.Sign(db.keys, r) 178 | if err != nil { 179 | return nil, err 180 | } 181 | 182 | return http.DefaultClient.Do(r) 183 | } 184 | -------------------------------------------------------------------------------- /mutex.go: -------------------------------------------------------------------------------- 1 | // Copyright 2012 Ryan Smith. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // Package ddbsync provides DynamoDB-backed synchronization primitives such 6 | // as mutual exclusion locks. This package is designed to behave like pkg/sync. 7 | 8 | package ddbsync 9 | 10 | import ( 11 | "fmt" 12 | "time" 13 | ) 14 | 15 | // A Mutex is a mutual exclusion lock. 16 | // Mutexes can be created as part of other structures. 17 | type Mutex struct { 18 | Name string 19 | Ttl int64 20 | } 21 | 22 | // Lock will write an item in a DynamoDB table if the item does not exist. 23 | // Before writing the lock, we will clear any locks that are expired. 24 | // Calling this function will block until a lock can be acquired. 25 | func (m *Mutex) Lock() { 26 | for { 27 | m.PruneExpired() 28 | err := db.put(m.Name, time.Now().Unix()) 29 | if err == nil { 30 | return 31 | } 32 | } 33 | } 34 | 35 | // Unlock will delete an item in a DynamoDB table. 36 | func (m *Mutex) Unlock() { 37 | for { 38 | err := db.delete(m.Name) 39 | if err == nil { 40 | return 41 | } 42 | } 43 | } 44 | 45 | // PruneExpired delete all locks that have lived past their TTL. 46 | // This is to prevent deadlock from processes that have taken locks 47 | // but never removed them after execution. This commonly happens when a 48 | // processor experiences network failure. 49 | func (m *Mutex) PruneExpired() { 50 | item, err := db.get(m.Name) 51 | if err != nil { 52 | fmt.Printf("error=%v", err) 53 | return 54 | } 55 | if item != nil { 56 | if item.Created < (time.Now().Unix() - m.Ttl) { 57 | m.Unlock() 58 | } 59 | } 60 | return 61 | } 62 | -------------------------------------------------------------------------------- /mutex_test.go: -------------------------------------------------------------------------------- 1 | package ddbsync 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestLockUnlock(t *testing.T) { 8 | m := Mutex{"mut-test", 4} 9 | m.Lock() 10 | // It should take us 4 seconds to acquire this lock. 11 | m.Lock() 12 | m.Unlock() 13 | } 14 | 15 | func TestUnlock(t *testing.T) { 16 | m := Mutex{"mut-test", 4} 17 | m.Lock() 18 | m.Unlock() 19 | } 20 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # ddbsync 2 | 3 | DynamoDB/sync 4 | 5 | This package is designed to emulate the behaviour of `pkg/sync` on top of Amazon's DynamoDB. If you need a distributed locking mechanism, consider using this package and DynamoDB before standing up paxos or Zookeeper. 6 | 7 | [GoPkgDoc](http://go.pkgdoc.org/github.com/ryandotsmith/ddbsync) 8 | 9 | ## Usage 10 | 11 | Create a DynamoDB table named *Locks*. 12 | 13 | ```bash 14 | $ export AWS_ACCESS_KEY=access 15 | $ export AWS_SECRET_KEY=secret 16 | ``` 17 | 18 | ```go 19 | // ./main.go 20 | 21 | package main 22 | 23 | import( 24 | "time" 25 | "github.com/ryandotsmith/ddbsync" 26 | ) 27 | 28 | func main() { 29 | m := new(ddbsync.Mutex) 30 | m.Name = "some-name" 31 | m.Ttl = 10 * time.Second 32 | m.Lock() 33 | defer m.Unlock() 34 | // do important work here 35 | return 36 | } 37 | ``` 38 | 39 | ```bash 40 | $ go get github.com/ryandotsmith/ddbsync 41 | $ go run main.go 42 | ``` 43 | 44 | ## Related 45 | 46 | [lock-smith](https://github.com/ryandotsmith/lock-smith) 47 | --------------------------------------------------------------------------------