marloss 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.rubocop.yml +19 -0
- data/.travis.yml +5 -2
- data/CHANGELOG.md +13 -0
- data/README.md +7 -2
- data/Rakefile +1 -1
- data/lib/marloss.rb +9 -13
- data/lib/marloss/error.rb +7 -9
- data/lib/marloss/locker.rb +8 -4
- data/lib/marloss/store.rb +49 -20
- data/lib/marloss/version.rb +2 -4
- data/marloss.gemspec +16 -10
- metadata +46 -17
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ee90d354ebc9dcd4f9bdba03380d2c44616b31614c7e1b92273e533e800415ce
|
4
|
+
data.tar.gz: 6cd27c87bb5b65ea18450719ba6e527a8a40e955e40c705d2c8a968a73c30a64
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: ede87eeba38468521d6582e6586a307911adc6938e7e85c785ebdf228efc95c398ec3c69db30c64a28de856d576a618cd563d8ed53fb65a7aee93c500005e419
|
7
|
+
data.tar.gz: 5751077d681c9c0611e243d662df3b0d79982b0b759feb1b81481a5bf8fccefa72b5595806ef97dab5dc2f1937207fd3504286e284e67b7508034a6097afe72e
|
data/.rubocop.yml
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
Metrics/LineLength:
|
2
|
+
Max: 100
|
3
|
+
|
4
|
+
Style/StringLiterals:
|
5
|
+
EnforcedStyle: double_quotes
|
6
|
+
|
7
|
+
Layout/MultilineMethodCallIndentation:
|
8
|
+
EnforcedStyle: indented
|
9
|
+
|
10
|
+
Style/AccessModifierDeclarations:
|
11
|
+
EnforcedStyle: inline
|
12
|
+
|
13
|
+
Style/Documentation:
|
14
|
+
Enabled: false # disable for now
|
15
|
+
|
16
|
+
Metrics/BlockLength:
|
17
|
+
Exclude:
|
18
|
+
- "spec/**/*"
|
19
|
+
- "marloss.gemspec"
|
data/.travis.yml
CHANGED
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,16 @@
|
|
1
|
+
## 0.4.0 01/09/2018
|
2
|
+
|
3
|
+
IMPROVEMENTS:
|
4
|
+
|
5
|
+
* Custom ttl attribute name instead of hardcoding `Expires` [#12](https://github.com/eredi93/marloss/pull/12)
|
6
|
+
* Add possiblity to pass retries to `wait_until_lock_obtained` preventing spin lock [#11](https://github.com/eredi93/marloss/pull/11)
|
7
|
+
|
8
|
+
## 0.3.1 01/09/2018
|
9
|
+
|
10
|
+
IMPROVEMENTS:
|
11
|
+
|
12
|
+
* Make `create_table` wait for table [#8](https://github.com/eredi93/marloss/pull/8)
|
13
|
+
|
1
14
|
## 0.3.0 29/11/2017
|
2
15
|
|
3
16
|
IMPROVEMENTS:
|
data/README.md
CHANGED
@@ -64,6 +64,11 @@ Firstly, we need to initialize a lock store:
|
|
64
64
|
store = Marloss::Store.new("lock_table_name", "LockHashKeyName")
|
65
65
|
```
|
66
66
|
|
67
|
+
Create table if it does not exist:
|
68
|
+
```ruby
|
69
|
+
store.create_table
|
70
|
+
```
|
71
|
+
|
67
72
|
We can use this store to create a single lock
|
68
73
|
|
69
74
|
```ruby
|
@@ -76,7 +81,7 @@ locker.obtain_lock
|
|
76
81
|
locker.wait_until_lock_obtained
|
77
82
|
|
78
83
|
# refresh the lock once
|
79
|
-
locker.
|
84
|
+
locker.refresh_lock
|
80
85
|
|
81
86
|
# delete the lock
|
82
87
|
locker.release_lock
|
@@ -100,4 +105,4 @@ Marloss.logger = Logger.new("my_app.log")
|
|
100
105
|
|
101
106
|
### Contributing
|
102
107
|
|
103
|
-
This repository is [open to contributions](
|
108
|
+
This repository is [open to contributions](CONTRIBUTING.md).
|
data/Rakefile
CHANGED
data/lib/marloss.rb
CHANGED
@@ -1,5 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
-
|
2
|
+
|
3
3
|
require "aws-sdk-dynamodb"
|
4
4
|
|
5
5
|
require "marloss/version"
|
@@ -8,7 +8,6 @@ require "marloss/store"
|
|
8
8
|
require "marloss/locker"
|
9
9
|
|
10
10
|
module Marloss
|
11
|
-
|
12
11
|
def self.logger
|
13
12
|
@logger ||= ::Logger.new(STDOUT)
|
14
13
|
end
|
@@ -19,10 +18,10 @@ module Marloss
|
|
19
18
|
|
20
19
|
def self.included(base)
|
21
20
|
base.define_singleton_method(:marloss_options) do |opts|
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
raise(MissingParameterError, "DynamoDB
|
21
|
+
%i[table hash_key].each do |key|
|
22
|
+
next unless opts[key].nil?
|
23
|
+
|
24
|
+
raise(MissingParameterError, "DynamoDB #{key.to_s.capitalize.tr('_', ' ')} not set")
|
26
25
|
end
|
27
26
|
|
28
27
|
define_method(:marloss_options_hash) { opts }
|
@@ -34,13 +33,12 @@ module Marloss
|
|
34
33
|
end
|
35
34
|
|
36
35
|
module InstanceMethods
|
37
|
-
|
38
36
|
def marloss_store
|
39
|
-
@marloss_store ||=begin
|
37
|
+
@marloss_store ||= begin
|
40
38
|
table = marloss_options_hash[:table]
|
41
39
|
hash_key = marloss_options_hash[:hash_key]
|
42
|
-
options = marloss_options_hash.reject do |k,
|
43
|
-
|
40
|
+
options = marloss_options_hash.reject do |k, _|
|
41
|
+
%i[table hash_key].include?(k)
|
44
42
|
end
|
45
43
|
|
46
44
|
Store.new(table, hash_key, options)
|
@@ -57,10 +55,8 @@ module Marloss
|
|
57
55
|
locker.wait_until_lock_obtained(opts)
|
58
56
|
|
59
57
|
yield(locker)
|
60
|
-
|
58
|
+
ensure
|
61
59
|
locker.release_lock
|
62
60
|
end
|
63
|
-
|
64
61
|
end
|
65
|
-
|
66
62
|
end
|
data/lib/marloss/error.rb
CHANGED
@@ -1,17 +1,15 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
-
|
2
|
+
|
3
3
|
module Marloss
|
4
|
+
class Error < StandardError; end
|
4
5
|
|
5
|
-
class
|
6
|
-
end
|
6
|
+
class CreateTableError < Error; end
|
7
7
|
|
8
|
-
class
|
9
|
-
end
|
8
|
+
class SetTableTtlError < Error; end
|
10
9
|
|
11
|
-
class
|
12
|
-
end
|
10
|
+
class LockNotObtainedError < Error; end
|
13
11
|
|
14
|
-
class
|
15
|
-
end
|
12
|
+
class LockNotRefreshedError < Error; end
|
16
13
|
|
14
|
+
class MissingParameterError < Error; end
|
17
15
|
end
|
data/lib/marloss/locker.rb
CHANGED
@@ -1,8 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
-
|
2
|
+
|
3
3
|
module Marloss
|
4
4
|
class Locker
|
5
|
-
|
6
5
|
attr_reader :store, :name
|
7
6
|
|
8
7
|
def initialize(store, name)
|
@@ -22,12 +21,17 @@ module Marloss
|
|
22
21
|
store.delete_lock(name)
|
23
22
|
end
|
24
23
|
|
25
|
-
def wait_until_lock_obtained(sleep_seconds: 3)
|
24
|
+
def wait_until_lock_obtained(sleep_seconds: 3, retries: nil)
|
26
25
|
store.create_lock(name)
|
27
26
|
rescue LockNotObtainedError
|
28
27
|
sleep(sleep_seconds)
|
28
|
+
|
29
|
+
unless retries.nil?
|
30
|
+
retries -= 1
|
31
|
+
raise if retries.zero?
|
32
|
+
end
|
33
|
+
|
29
34
|
retry
|
30
35
|
end
|
31
|
-
|
32
36
|
end
|
33
37
|
end
|
data/lib/marloss/store.rb
CHANGED
@@ -1,49 +1,81 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
-
#
|
3
|
-
module Marloss
|
4
|
-
class Store
|
5
2
|
|
6
|
-
|
3
|
+
module Marloss
|
4
|
+
class Store # rubocop:disable Metrics/ClassLength
|
5
|
+
attr_reader :client, :table, :hash_key, :expires_key, :ttl
|
7
6
|
|
8
|
-
def initialize(table, hash_key, ttl: 30, client_options: {})
|
7
|
+
def initialize(table, hash_key, expires_key: "Expires", ttl: 30, client_options: {})
|
9
8
|
@client = Aws::DynamoDB::Client.new(client_options)
|
10
9
|
@table = table
|
11
10
|
@hash_key = hash_key
|
11
|
+
@expires_key = expires_key
|
12
12
|
@ttl = ttl
|
13
13
|
end
|
14
14
|
|
15
15
|
def create_table
|
16
|
+
create_ddb_table
|
17
|
+
wait_until_ddb_table_exists
|
18
|
+
set_ddb_table_ttl
|
19
|
+
end
|
20
|
+
|
21
|
+
private def create_ddb_table # rubocop:disable Metrics/MethodLength
|
16
22
|
client.create_table(
|
17
23
|
attribute_definitions: [
|
18
24
|
{
|
19
25
|
attribute_name: hash_key,
|
20
|
-
attribute_type: "S"
|
26
|
+
attribute_type: "S"
|
21
27
|
}
|
22
28
|
],
|
23
29
|
key_schema: [
|
24
30
|
{
|
25
31
|
attribute_name: hash_key,
|
26
|
-
key_type: "HASH"
|
32
|
+
key_type: "HASH"
|
27
33
|
}
|
28
34
|
],
|
29
35
|
provisioned_throughput: {
|
30
36
|
read_capacity_units: 5,
|
31
|
-
write_capacity_units: 5
|
37
|
+
write_capacity_units: 5
|
32
38
|
},
|
33
39
|
table_name: table
|
34
40
|
)
|
35
41
|
|
36
42
|
Marloss.logger.info("DynamoDB table created successfully")
|
43
|
+
rescue Aws::DynamoDB::Errors::ResourceInUseException => e
|
44
|
+
case e.message
|
45
|
+
when "Table already exists: #{table}"
|
46
|
+
Marloss.logger.warn("DynamoDB table #{table} already exists")
|
47
|
+
else
|
48
|
+
raise(CreateTableError, e.message)
|
49
|
+
end
|
50
|
+
end
|
37
51
|
|
52
|
+
private def wait_until_ddb_table_exists
|
53
|
+
client.wait_until(:table_exists, table_name: table) do |w|
|
54
|
+
w.max_attempts = 10
|
55
|
+
w.delay = 1
|
56
|
+
end
|
57
|
+
rescue Aws::Waiters::Errors::WaiterFailed => e
|
58
|
+
Marloss.logger.error("Failed waiting for initialization of table #{table}")
|
59
|
+
raise(CreateTableError, e.message)
|
60
|
+
end
|
61
|
+
|
62
|
+
private def set_ddb_table_ttl # rubocop:disable Metrics/MethodLength
|
38
63
|
client.update_time_to_live(
|
39
64
|
table_name: table,
|
40
65
|
time_to_live_specification: {
|
41
66
|
enabled: true,
|
42
|
-
attribute_name:
|
67
|
+
attribute_name: expires_key
|
43
68
|
}
|
44
69
|
)
|
45
70
|
|
46
71
|
Marloss.logger.info("DynamoDB table TTL configured successfully")
|
72
|
+
rescue Aws::DynamoDB::Errors::ValidationException => e
|
73
|
+
case e.message
|
74
|
+
when "TimeToLive is already enabled"
|
75
|
+
Marloss.logger.warn("TTL attribute is already configured for table #{table}")
|
76
|
+
else
|
77
|
+
raise(SetTableTtlError, e.message)
|
78
|
+
end
|
47
79
|
end
|
48
80
|
|
49
81
|
def delete_table
|
@@ -52,53 +84,51 @@ module Marloss
|
|
52
84
|
Marloss.logger.info("DynamoDB table deleted successfully")
|
53
85
|
end
|
54
86
|
|
55
|
-
def create_lock(name)
|
87
|
+
def create_lock(name) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
|
56
88
|
client.put_item(
|
57
89
|
table_name: table,
|
58
90
|
item: {
|
59
91
|
hash_key => name,
|
60
92
|
"ProcessID" => process_id,
|
61
|
-
|
93
|
+
expires_key => (Time.now + ttl).to_i
|
62
94
|
},
|
63
95
|
expression_attribute_names: {
|
64
|
-
"#E" =>
|
96
|
+
"#E" => expires_key,
|
65
97
|
"#P" => "ProcessID"
|
66
98
|
},
|
67
99
|
expression_attribute_values: {
|
68
100
|
":now" => Time.now.to_i,
|
69
|
-
":process_id" => process_id
|
101
|
+
":process_id" => process_id
|
70
102
|
},
|
71
103
|
condition_expression: "attribute_not_exists(#{hash_key}) OR #E < :now OR #P = :process_id"
|
72
104
|
)
|
73
105
|
|
74
106
|
Marloss.logger.info("Lock for #{name} created successfully, will expire in #{ttl} seconds")
|
75
107
|
rescue Aws::DynamoDB::Errors::ConditionalCheckFailedException => e
|
76
|
-
|
77
108
|
Marloss.logger.error("Failed to create lock for #{name}")
|
78
109
|
|
79
110
|
raise(LockNotObtainedError, e.message)
|
80
111
|
end
|
81
112
|
|
82
|
-
def refresh_lock(name)
|
113
|
+
def refresh_lock(name) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
|
83
114
|
client.update_item(
|
84
115
|
table_name: table,
|
85
116
|
key: { hash_key => name },
|
86
117
|
expression_attribute_names: {
|
87
|
-
"#E" =>
|
118
|
+
"#E" => expires_key,
|
88
119
|
"#P" => "ProcessID"
|
89
120
|
},
|
90
121
|
expression_attribute_values: {
|
91
122
|
":expires" => (Time.now + ttl).to_i,
|
92
123
|
":now" => Time.now.to_i,
|
93
|
-
":process_id" => process_id
|
124
|
+
":process_id" => process_id
|
94
125
|
},
|
95
|
-
update_expression: "SET #E = :expires",
|
126
|
+
update_expression: "SET #E = :expires",
|
96
127
|
condition_expression: "attribute_exists(#{hash_key}) AND (#E < :now OR #P = :process_id)"
|
97
128
|
)
|
98
129
|
|
99
130
|
Marloss.logger.info("Lock for #{name} refreshed successfully, will expire in #{ttl} seconds")
|
100
131
|
rescue Aws::DynamoDB::Errors::ConditionalCheckFailedException => e
|
101
|
-
|
102
132
|
Marloss.logger.error("Failed to refresh lock for #{name}")
|
103
133
|
|
104
134
|
raise(LockNotRefreshedError, e.message)
|
@@ -116,6 +146,5 @@ module Marloss
|
|
116
146
|
|
117
147
|
"#{hostname}:#{pid}"
|
118
148
|
end
|
119
|
-
|
120
149
|
end
|
121
150
|
end
|
data/lib/marloss/version.rb
CHANGED
data/marloss.gemspec
CHANGED
@@ -1,25 +1,31 @@
|
|
1
|
-
#
|
2
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
lib = File.expand_path("lib", __dir__)
|
3
4
|
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
5
|
require "marloss/version"
|
5
6
|
|
7
|
+
files = `git ls-files -z`.split("\x0")
|
8
|
+
.reject { |f| f.match(%r{^(test|spec|features)/}) }
|
9
|
+
|
6
10
|
Gem::Specification.new do |spec|
|
7
11
|
spec.name = "marloss"
|
8
12
|
spec.version = Marloss::VERSION
|
9
13
|
spec.authors = ["Jacopo Scrinzi"]
|
10
14
|
spec.email = "scrinzi.jcopo@gmail.com"
|
11
15
|
|
12
|
-
spec.summary =
|
13
|
-
spec.description =
|
16
|
+
spec.summary = "AWS DynamoDB based Locking"
|
17
|
+
spec.description = "Distributed locking using AWS DynamoDB"
|
14
18
|
spec.homepage = "https://github.com/eredi93/marloss"
|
15
19
|
spec.license = "MIT"
|
16
20
|
|
17
|
-
spec.files =
|
18
|
-
spec.require_paths = %w
|
21
|
+
spec.files = files
|
22
|
+
spec.require_paths = %w[lib]
|
19
23
|
|
20
|
-
spec.add_dependency "aws-sdk-dynamodb", "~> 1.
|
24
|
+
spec.add_dependency "aws-sdk-dynamodb", "~> 1.11"
|
21
25
|
|
22
|
-
spec.add_development_dependency "bundler"
|
23
|
-
spec.add_development_dependency "
|
24
|
-
spec.add_development_dependency "
|
26
|
+
spec.add_development_dependency "bundler", "~> 1"
|
27
|
+
spec.add_development_dependency "gem-release", "~> 2.0"
|
28
|
+
spec.add_development_dependency "rake", "~> 12.3"
|
29
|
+
spec.add_development_dependency "rspec", "~> 3.8"
|
30
|
+
spec.add_development_dependency "rubocop", "= 0.58"
|
25
31
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: marloss
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.4.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Jacopo Scrinzi
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2018-10-07 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: aws-sdk-dynamodb
|
@@ -16,56 +16,84 @@ dependencies:
|
|
16
16
|
requirements:
|
17
17
|
- - "~>"
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version: '1.
|
19
|
+
version: '1.11'
|
20
20
|
type: :runtime
|
21
21
|
prerelease: false
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
23
23
|
requirements:
|
24
24
|
- - "~>"
|
25
25
|
- !ruby/object:Gem::Version
|
26
|
-
version: '1.
|
26
|
+
version: '1.11'
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
28
|
name: bundler
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
30
30
|
requirements:
|
31
|
-
- - "
|
31
|
+
- - "~>"
|
32
32
|
- !ruby/object:Gem::Version
|
33
|
-
version: '
|
33
|
+
version: '1'
|
34
34
|
type: :development
|
35
35
|
prerelease: false
|
36
36
|
version_requirements: !ruby/object:Gem::Requirement
|
37
37
|
requirements:
|
38
|
-
- - "
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '1'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: gem-release
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '2.0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
39
53
|
- !ruby/object:Gem::Version
|
40
|
-
version: '0'
|
54
|
+
version: '2.0'
|
41
55
|
- !ruby/object:Gem::Dependency
|
42
56
|
name: rake
|
43
57
|
requirement: !ruby/object:Gem::Requirement
|
44
58
|
requirements:
|
45
|
-
- - "
|
59
|
+
- - "~>"
|
46
60
|
- !ruby/object:Gem::Version
|
47
|
-
version: '
|
61
|
+
version: '12.3'
|
48
62
|
type: :development
|
49
63
|
prerelease: false
|
50
64
|
version_requirements: !ruby/object:Gem::Requirement
|
51
65
|
requirements:
|
52
|
-
- - "
|
66
|
+
- - "~>"
|
53
67
|
- !ruby/object:Gem::Version
|
54
|
-
version: '
|
68
|
+
version: '12.3'
|
55
69
|
- !ruby/object:Gem::Dependency
|
56
70
|
name: rspec
|
57
71
|
requirement: !ruby/object:Gem::Requirement
|
58
72
|
requirements:
|
59
|
-
- - "
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '3.8'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '3.8'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: rubocop
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - '='
|
60
88
|
- !ruby/object:Gem::Version
|
61
|
-
version: '0'
|
89
|
+
version: '0.58'
|
62
90
|
type: :development
|
63
91
|
prerelease: false
|
64
92
|
version_requirements: !ruby/object:Gem::Requirement
|
65
93
|
requirements:
|
66
|
-
- -
|
94
|
+
- - '='
|
67
95
|
- !ruby/object:Gem::Version
|
68
|
-
version: '0'
|
96
|
+
version: '0.58'
|
69
97
|
description: Distributed locking using AWS DynamoDB
|
70
98
|
email: scrinzi.jcopo@gmail.com
|
71
99
|
executables: []
|
@@ -73,6 +101,7 @@ extensions: []
|
|
73
101
|
extra_rdoc_files: []
|
74
102
|
files:
|
75
103
|
- ".gitignore"
|
104
|
+
- ".rubocop.yml"
|
76
105
|
- ".travis.yml"
|
77
106
|
- CHANGELOG.md
|
78
107
|
- CONTRIBUTING.md
|
@@ -106,7 +135,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
106
135
|
version: '0'
|
107
136
|
requirements: []
|
108
137
|
rubyforge_project:
|
109
|
-
rubygems_version: 2.7.
|
138
|
+
rubygems_version: 2.7.6
|
110
139
|
signing_key:
|
111
140
|
specification_version: 4
|
112
141
|
summary: AWS DynamoDB based Locking
|