lock-smith 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,20 @@
1
+ module Locksmith
2
+ module Config
3
+ extend self
4
+
5
+ def env(key)
6
+ ENV[key]
7
+ end
8
+
9
+ def env!(key)
10
+ env(key) || raise("missing #{key}")
11
+ end
12
+
13
+ def env?(key)
14
+ !env(key).nil?
15
+ end
16
+
17
+ def aws_id; env!("AWS_ID"); end
18
+ def aws_secret; env!("AWS_SECRET"); end
19
+ end
20
+ end
@@ -0,0 +1,81 @@
1
+ require 'thread'
2
+ require 'locksmith/log'
3
+
4
+ module Locksmith
5
+ module Dynamodb
6
+ extend self
7
+ TTL = 60
8
+ MAX_LOCK_ATTEMPTS = 3
9
+ LOCK_TIMEOUT = 30
10
+ LOCK_TABLE = "Locks"
11
+
12
+ @dynamo_lock = Mutex.new
13
+ @table_lock = Mutex.new
14
+
15
+ def lock(name)
16
+ lock = fetch_lock(name)
17
+ last_rev = lock[:Locked] || 0
18
+ new_rev = Time.now.to_i
19
+ attempts = 0
20
+ while attempts < MAX_LOCK_ATTEMPTS
21
+ begin
22
+ Timeout::timeout(LOCK_TIMEOUT) do
23
+ release_lock(name, last_rev) if last_rev < (Time.now.to_i - TTL)
24
+ write_lock(name, 0, new_rev)
25
+ log(at: "lock-acquired", lock: name, rev: new_rev)
26
+ result = yield
27
+ release_lock(name, new_rev)
28
+ log(at: "lock-released", lock: name, rev: new_rev)
29
+ return result
30
+ end
31
+ rescue AWS::DynamoDB::Errors::ConditionalCheckFailedException
32
+ attempts += 1
33
+ rescue Timeout::Error
34
+ attempts += 1
35
+ release_lock(name, new_rev)
36
+ log(at: "timeout-lock-released", lock: name, rev: new_rev)
37
+ end
38
+ end
39
+ end
40
+
41
+ def write_lock(name, rev, new_rev)
42
+ locks.put({Name: name, Locked: new_rev},
43
+ :if => {:Locked => rev})
44
+ end
45
+
46
+ def release_lock(name, rev)
47
+ locks.put({Name: app_name, Locked: 0},
48
+ :if => {:Locked => rev})
49
+ end
50
+
51
+ def fetch_lock(name)
52
+ locks[name].attributes
53
+ end
54
+
55
+ def locks
56
+ table(LOCK_TABLE)
57
+ end
58
+
59
+ def table(name)
60
+ @table_lock.synchronize {tables[name].items}
61
+ end
62
+
63
+ def tables
64
+ @tables ||= dynamo.tables.
65
+ map {|t| t.load_schema}.
66
+ reduce({}) {|h, t| h[t.name] = t; h}
67
+ end
68
+
69
+ def dynamo
70
+ @dynamo_lock.synchronize do
71
+ @db ||= AWS::DynamoDB.new(access_key_id: Config.aws_id,
72
+ secret_access_key: Config.aws_secret)
73
+ end
74
+ end
75
+
76
+ def log(data, &blk)
77
+ Log.log({ns: "app-lock"}.merge(data), &blk)
78
+ end
79
+
80
+ end
81
+ end
@@ -0,0 +1,21 @@
1
+ module Locksmith
2
+ module Log
3
+ extend self
4
+
5
+ def log(data)
6
+ result = nil
7
+ data = {lib: "locksmith"}.merge(data)
8
+ if block_given?
9
+ start = Time.now
10
+ result = yield
11
+ data.merge(elapsed: Time.now - start)
12
+ end
13
+ data.reduce(out=String.new) do |s, tup|
14
+ s << [tup.first, tup.last].join("=") << " "
15
+ end
16
+ puts(out) if ENV["DEBUG"]
17
+ return result
18
+ end
19
+
20
+ end
21
+ end
@@ -0,0 +1,50 @@
1
+ require 'zlib'
2
+ module Locksmith
3
+ module Postresql
4
+ BACKOFF = 0.5
5
+
6
+ def lock(name)
7
+ i = Zlib.crc32(name)
8
+ result = nil
9
+ begin
10
+ sleep(BACKOFF) until write_lock(i)
11
+ if block_given?
12
+ result = yield
13
+ end
14
+ return result
15
+ ensure
16
+ unlock(i)
17
+ end
18
+ end
19
+
20
+ def write_lock(i)
21
+ r = conn.exec("select pg_try_advisory_lock($1)", [i])
22
+ r[0]["pg_try_advisory_lock"] == "t"
23
+ end
24
+
25
+ def release_lock(i)
26
+ conn.exec("select pg_advisory_unlock($1)", [i])
27
+ end
28
+
29
+ def conn
30
+ @con ||= PG::Connection.open(
31
+ dburl.host,
32
+ dburl.port || 5432,
33
+ nil, '', #opts, tty
34
+ dburl.path.gsub("/",""), # database name
35
+ dburl.user,
36
+ dburl.password
37
+ )
38
+ end
39
+
40
+ def dburl
41
+ URI.parse(ENV["DATABASE_URL"])
42
+ end
43
+
44
+ def log(data, &blk)
45
+ Log.log({ns: "postgresql-lock"}.merge(data), &blk)
46
+ end
47
+
48
+ end
49
+ end
50
+
@@ -0,0 +1,66 @@
1
+ # Locksmith
2
+
3
+ A library of locking algorithms for a variety of data stores. Supported Data Stores:
4
+
5
+ * DynamoDB
6
+ * PostgreSQL
7
+ * TODO: Memcached
8
+ * TODO: Redis
9
+ * TODO: Doozerd
10
+ * TODO: Zookeeper
11
+
12
+ ## Usage
13
+
14
+ There is only 1 public method:
15
+
16
+ ```
17
+ lock(name, &blk)
18
+ ```
19
+
20
+ ### DynamoDB
21
+
22
+ Create a DynamoDB table named **Locks** with a hash key **Name**.
23
+
24
+ ```ruby
25
+ ENV["AWS_ID"] = "id"
26
+ ENV["AWS_SECRET"] = "secret"
27
+
28
+ require 'aws/dynamo_db'
29
+ require 'locksmith/dynamodb'
30
+ Locksmith::Dynamodb.lock("my-resource") do
31
+ puts("locked my-resource with DynamoDB")
32
+ end
33
+ ```
34
+
35
+ ### PostgreSQL
36
+
37
+ Locksmith will use `pg_try_advisory_lock` to lock, no need for table creation.
38
+
39
+ ```ruby
40
+ ENV["DATABASE_URL"] = "postgresql://user:pass@localhost/database_name"
41
+
42
+ require 'pg'
43
+ require 'locksmith/postgresql'
44
+ Locksmith::Postgresql.lock("my-resource") do
45
+ puts("locked my-resource with PostgreSQL")
46
+ end
47
+ ```
48
+
49
+ ## Why Locksmith
50
+
51
+ Locking code is tricky. Ideally, I would write it once, verify in production for
52
+ a year then never think about it again.
53
+
54
+ ## Hacking on Locksmith
55
+
56
+ There are still some Data Stores to implement, follow the pattern for PostgreSQLand DynamoDB and submit a pull request.
57
+
58
+ ## License
59
+
60
+ Copyright (C) 2012 Ryan Smith
61
+
62
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
63
+
64
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
65
+
66
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
metadata ADDED
@@ -0,0 +1,51 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: lock-smith
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Ryan Smith (♠ ace hacker)
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-09-12 00:00:00.000000000 Z
13
+ dependencies: []
14
+ description: Locking toolkit for a variety of data stores.
15
+ email: ryan@heroku.com
16
+ executables: []
17
+ extensions: []
18
+ extra_rdoc_files: []
19
+ files:
20
+ - lib/locksmith/config.rb
21
+ - lib/locksmith/dynamodb.rb
22
+ - lib/locksmith/log.rb
23
+ - lib/locksmith/postgresql.rb
24
+ - readme.md
25
+ homepage: http://github.com/ryandotsmith/lock-smith
26
+ licenses:
27
+ - MIT
28
+ post_install_message:
29
+ rdoc_options: []
30
+ require_paths:
31
+ - lib
32
+ required_ruby_version: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ! '>='
36
+ - !ruby/object:Gem::Version
37
+ version: '0'
38
+ required_rubygems_version: !ruby/object:Gem::Requirement
39
+ none: false
40
+ requirements:
41
+ - - ! '>='
42
+ - !ruby/object:Gem::Version
43
+ version: '0'
44
+ requirements: []
45
+ rubyforge_project:
46
+ rubygems_version: 1.8.23
47
+ signing_key:
48
+ specification_version: 3
49
+ summary: Locking is hard. Write it once.
50
+ test_files: []
51
+ has_rdoc: