redlock 0.1.8 → 0.2.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 +5 -13
- data/Gemfile.lock +28 -25
- data/README.md +14 -2
- data/lib/redlock/client.rb +18 -30
- data/lib/redlock/version.rb +1 -1
- data/spec/client_spec.rb +33 -5
- metadata +22 -22
checksums.yaml
CHANGED
@@ -1,15 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
|
5
|
-
data.tar.gz: !binary |-
|
6
|
-
ZDliZjZlMmM3OGNmZGMyYzhlNWI3YjBlMmUyNGRmNGY0Yjk0N2FkYw==
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 3820d9812a87989c395ff8b4856a4cea7801d84b
|
4
|
+
data.tar.gz: 7cc006346948cf1132be55de820949f37cb9f07d
|
7
5
|
SHA512:
|
8
|
-
metadata.gz:
|
9
|
-
|
10
|
-
YzVhMGJjMjBiMTM4NDYzY2ZkNWNiYzRmODk5YWFlZTJlMzczMTZlMzI4ZmRh
|
11
|
-
YTNkZDNhNjgxNzcxOTlhNGE2YTdjNzgyZTgwNTY0YWY4OTUwMjc=
|
12
|
-
data.tar.gz: !binary |-
|
13
|
-
NDllNDgxMmQ0ODk1NTdlNTNkMjczN2EyYjQyMjE2MWU5YmJmMDVlMmJkY2Q1
|
14
|
-
YTZjNTE5NjFjNTI3Mjc3NzJiZDBlMDIwYzUwM2NlMGM1YjE0NDVkNDIzODFm
|
15
|
-
MjVhNjU3OTI4NzY0YzY3OTE1ZDhiNGRhNmRlMzM5OGE3YjczMTI=
|
6
|
+
metadata.gz: ba94bdcbdd4f659a6564baad82a3f022ea64f541789579803f33bfd0981ee6a3df53b6887a05791f3794202c85748ae85bdd357d1703d690464e6f0844b3e899
|
7
|
+
data.tar.gz: c36ec9b01716486eeacdd9a82f27cbaa8d6b3165da1e17e11196ac4d700dfe82acf439e6b027b8852f5f33241af08927fea6c2d2f379ffca8392b1ac69a69c1d
|
data/Gemfile.lock
CHANGED
@@ -1,45 +1,45 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
redlock (0.
|
4
|
+
redlock (0.2.0)
|
5
5
|
redis (~> 3, >= 3.0.0)
|
6
6
|
|
7
7
|
GEM
|
8
8
|
remote: https://rubygems.org/
|
9
9
|
specs:
|
10
|
-
coveralls (0.8.
|
11
|
-
json (
|
12
|
-
simplecov (~> 0.
|
10
|
+
coveralls (0.8.19)
|
11
|
+
json (>= 1.8, < 3)
|
12
|
+
simplecov (~> 0.12.0)
|
13
13
|
term-ansicolor (~> 1.3)
|
14
14
|
thor (~> 0.19.1)
|
15
|
-
tins (~> 1.6
|
16
|
-
diff-lcs (1.
|
15
|
+
tins (~> 1.6)
|
16
|
+
diff-lcs (1.3)
|
17
17
|
docile (1.1.5)
|
18
|
-
json (
|
19
|
-
rake (11.
|
20
|
-
redis (3.3.
|
21
|
-
rspec (3.
|
22
|
-
rspec-core (~> 3.
|
23
|
-
rspec-expectations (~> 3.
|
24
|
-
rspec-mocks (~> 3.
|
25
|
-
rspec-core (3.
|
26
|
-
rspec-support (~> 3.
|
27
|
-
rspec-expectations (3.
|
18
|
+
json (2.0.3)
|
19
|
+
rake (11.3.0)
|
20
|
+
redis (3.3.3)
|
21
|
+
rspec (3.5.0)
|
22
|
+
rspec-core (~> 3.5.0)
|
23
|
+
rspec-expectations (~> 3.5.0)
|
24
|
+
rspec-mocks (~> 3.5.0)
|
25
|
+
rspec-core (3.5.4)
|
26
|
+
rspec-support (~> 3.5.0)
|
27
|
+
rspec-expectations (3.5.0)
|
28
28
|
diff-lcs (>= 1.2.0, < 2.0)
|
29
|
-
rspec-support (~> 3.
|
30
|
-
rspec-mocks (3.
|
29
|
+
rspec-support (~> 3.5.0)
|
30
|
+
rspec-mocks (3.5.0)
|
31
31
|
diff-lcs (>= 1.2.0, < 2.0)
|
32
|
-
rspec-support (~> 3.
|
33
|
-
rspec-support (3.
|
34
|
-
simplecov (0.
|
32
|
+
rspec-support (~> 3.5.0)
|
33
|
+
rspec-support (3.5.0)
|
34
|
+
simplecov (0.12.0)
|
35
35
|
docile (~> 1.1.0)
|
36
|
-
json (
|
36
|
+
json (>= 1.8, < 3)
|
37
37
|
simplecov-html (~> 0.10.0)
|
38
38
|
simplecov-html (0.10.0)
|
39
|
-
term-ansicolor (1.
|
39
|
+
term-ansicolor (1.4.0)
|
40
40
|
tins (~> 1.0)
|
41
|
-
thor (0.19.
|
42
|
-
tins (1.
|
41
|
+
thor (0.19.4)
|
42
|
+
tins (1.13.0)
|
43
43
|
|
44
44
|
PLATFORMS
|
45
45
|
ruby
|
@@ -49,3 +49,6 @@ DEPENDENCIES
|
|
49
49
|
rake (~> 11.1, >= 11.1.2)
|
50
50
|
redlock!
|
51
51
|
rspec (~> 3, >= 3.0.0)
|
52
|
+
|
53
|
+
BUNDLED WITH
|
54
|
+
1.12.5
|
data/README.md
CHANGED
@@ -8,8 +8,6 @@
|
|
8
8
|
[](http://inch-ci.org/github/leandromoreira/redlock-rb)
|
9
9
|
[](https://gitter.im/leandromoreira/redlock-rb?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
|
10
10
|
|
11
|
-
[](https://codeship.com/projects/901ff180-c1ad-0132-1a88-3eb2295b72b3/status?branch=master)
|
12
|
-
|
13
11
|
|
14
12
|
# Redlock - A ruby distributed lock using redis.
|
15
13
|
|
@@ -123,6 +121,20 @@ rescue Redlock::LockError
|
|
123
121
|
end
|
124
122
|
```
|
125
123
|
|
124
|
+
It's possible to customize the retry logic providing the following options:
|
125
|
+
|
126
|
+
```ruby
|
127
|
+
lock_manager = Redlock::Client.new(
|
128
|
+
servers, {
|
129
|
+
retry_count: 3,
|
130
|
+
retry_delay: 200, # milliseconds
|
131
|
+
retry_jitter: 50, # milliseconds
|
132
|
+
retry_timeout: 0.1 # seconds
|
133
|
+
})
|
134
|
+
```
|
135
|
+
|
136
|
+
For more information you can check [documentation](https://github.com/leandromoreira/redlock-rb/blob/master/lib/redlock/client.rb#L13-L20)
|
137
|
+
|
126
138
|
|
127
139
|
## Run tests
|
128
140
|
|
data/lib/redlock/client.rb
CHANGED
@@ -7,14 +7,16 @@ module Redlock
|
|
7
7
|
DEFAULT_REDIS_TIMEOUT = 0.1
|
8
8
|
DEFAULT_RETRY_COUNT = 3
|
9
9
|
DEFAULT_RETRY_DELAY = 200
|
10
|
+
DEFAULT_RETRY_JITTER = 50
|
10
11
|
CLOCK_DRIFT_FACTOR = 0.01
|
11
12
|
|
12
13
|
# Create a distributed lock manager implementing redlock algorithm.
|
13
14
|
# Params:
|
14
15
|
# +servers+:: The array of redis connection URLs or Redis connection instances. Or a mix of both.
|
15
|
-
# +options+:: You can override the default value for `retry_count` and `
|
16
|
+
# +options+:: You can override the default value for `retry_count`, `retry_delay` and `retry_gitter`.
|
16
17
|
# * `retry_count` being how many times it'll try to lock a resource (default: 3)
|
17
18
|
# * `retry_delay` being how many ms to sleep before try to lock again (default: 200)
|
19
|
+
# * `retry_jitter` being how many ms to jitter retry delay (default: 50)
|
18
20
|
# * `redis_timeout` being how the Redis timeout will be set in seconds (default: 0.1)
|
19
21
|
def initialize(servers = DEFAULT_REDIS_URLS, options = {})
|
20
22
|
redis_timeout = options[:redis_timeout] || DEFAULT_REDIS_TIMEOUT
|
@@ -28,13 +30,16 @@ module Redlock
|
|
28
30
|
@quorum = servers.length / 2 + 1
|
29
31
|
@retry_count = options[:retry_count] || DEFAULT_RETRY_COUNT
|
30
32
|
@retry_delay = options[:retry_delay] || DEFAULT_RETRY_DELAY
|
33
|
+
@retry_jitter = options[:retry_jitter] || DEFAULT_RETRY_JITTER
|
31
34
|
end
|
32
35
|
|
33
36
|
# Locks a resource for a given time.
|
34
37
|
# Params:
|
35
38
|
# +resource+:: the resource (or key) string to be locked.
|
36
39
|
# +ttl+:: The time-to-live in ms for the lock.
|
37
|
-
# +
|
40
|
+
# +options+:: Hash of optional parameters
|
41
|
+
# * +extend+: A lock ("lock_info") to extend.
|
42
|
+
# * +extend_only_if_life+: If +extend+ is given, only acquire lock if currently held
|
38
43
|
# +block+:: an optional block to be executed; after its execution, the lock (if successfully
|
39
44
|
# acquired) is automatically unlocked.
|
40
45
|
def lock(resource, ttl, options = {}, &block)
|
@@ -86,40 +91,24 @@ module Redlock
|
|
86
91
|
# also https://github.com/sbertrang/redis-distlock/issues/2 which proposes the value-checking
|
87
92
|
# and @maltoe for https://github.com/leandromoreira/redlock-rb/pull/20#discussion_r38903633
|
88
93
|
LOCK_SCRIPT = <<-eos
|
89
|
-
if redis.call("exists", KEYS[1]) == 0 or redis.call("get", KEYS[1]) == ARGV[1] then
|
94
|
+
if (redis.call("exists", KEYS[1]) == 0 and ARGV[3] == "yes") or redis.call("get", KEYS[1]) == ARGV[1] then
|
90
95
|
return redis.call("set", KEYS[1], ARGV[1], "PX", ARGV[2])
|
91
96
|
end
|
92
97
|
eos
|
93
98
|
|
94
|
-
EXTEND_LIFE_SCRIPT = <<-eos
|
95
|
-
if redis.call("get", KEYS[1]) == ARGV[1] then
|
96
|
-
redis.call("expire", KEYS[1], ARGV[2])
|
97
|
-
return 0
|
98
|
-
else
|
99
|
-
return 1
|
100
|
-
end
|
101
|
-
eos
|
102
|
-
|
103
99
|
def initialize(connection)
|
104
100
|
if connection.respond_to?(:client)
|
105
101
|
@redis = connection
|
106
102
|
else
|
107
|
-
@redis
|
103
|
+
@redis = Redis.new(connection)
|
108
104
|
end
|
109
105
|
|
110
106
|
load_scripts
|
111
107
|
end
|
112
108
|
|
113
|
-
def lock(resource, val, ttl)
|
109
|
+
def lock(resource, val, ttl, allow_new_lock)
|
114
110
|
recover_from_script_flush do
|
115
|
-
@redis.evalsha @lock_script_sha, keys: [resource], argv: [val, ttl]
|
116
|
-
end
|
117
|
-
end
|
118
|
-
|
119
|
-
def extend(resource, val, ttl)
|
120
|
-
recover_from_script_flush do
|
121
|
-
rc = @redis.evalsha @extend_life_script_sha, keys: [resource], argv: [val, ttl]
|
122
|
-
rc == 0
|
111
|
+
@redis.evalsha @lock_script_sha, keys: [resource], argv: [val, ttl, allow_new_lock]
|
123
112
|
end
|
124
113
|
end
|
125
114
|
|
@@ -136,7 +125,6 @@ module Redlock
|
|
136
125
|
def load_scripts
|
137
126
|
@unlock_script_sha = @redis.script(:load, UNLOCK_SCRIPT)
|
138
127
|
@lock_script_sha = @redis.script(:load, LOCK_SCRIPT)
|
139
|
-
@extend_life_script_sha = @redis.script(:load, EXTEND_LIFE_SCRIPT)
|
140
128
|
end
|
141
129
|
|
142
130
|
def recover_from_script_flush
|
@@ -161,23 +149,23 @@ module Redlock
|
|
161
149
|
def try_lock_instances(resource, ttl, options)
|
162
150
|
tries = options[:extend] ? 1 : @retry_count
|
163
151
|
|
164
|
-
tries.times do
|
152
|
+
tries.times do |attempt_number|
|
153
|
+
# Wait a random delay before retrying.
|
154
|
+
sleep((@retry_delay + rand(@retry_jitter)).to_f / 1000) if attempt_number > 0
|
155
|
+
|
165
156
|
lock_info = lock_instances(resource, ttl, options)
|
166
157
|
return lock_info if lock_info
|
167
|
-
|
168
|
-
# Wait a random delay before retrying
|
169
|
-
sleep(rand(@retry_delay).to_f / 1000)
|
170
158
|
end
|
171
159
|
|
172
160
|
false
|
173
161
|
end
|
174
162
|
|
175
163
|
def lock_instances(resource, ttl, options)
|
176
|
-
value
|
177
|
-
|
164
|
+
value = options.fetch(:extend, { value: SecureRandom.uuid })[:value]
|
165
|
+
allow_new_lock = (options[:extend_life] || options[:extend_only_if_life]) ? 'no' : 'yes'
|
178
166
|
|
179
167
|
locked, time_elapsed = timed do
|
180
|
-
@servers.select { |s| s.
|
168
|
+
@servers.select { |s| s.lock resource, value, ttl, allow_new_lock }.size
|
181
169
|
end
|
182
170
|
|
183
171
|
validity = ttl - time_elapsed - drift(ttl)
|
data/lib/redlock/version.rb
CHANGED
data/spec/client_spec.rb
CHANGED
@@ -3,7 +3,8 @@ require 'securerandom'
|
|
3
3
|
|
4
4
|
RSpec.describe Redlock::Client do
|
5
5
|
# It is recommended to have at least 3 servers in production
|
6
|
-
let(:
|
6
|
+
let(:lock_manager_opts) { { retry_count: 3 } }
|
7
|
+
let(:lock_manager) { Redlock::Client.new(Redlock::Client::DEFAULT_REDIS_URLS, lock_manager_opts) }
|
7
8
|
let(:resource_key) { SecureRandom.hex(3) }
|
8
9
|
let(:ttl) { 1000 }
|
9
10
|
|
@@ -43,16 +44,16 @@ RSpec.describe Redlock::Client do
|
|
43
44
|
expect(@lock_info[:value]).to eq(my_lock_info[:value])
|
44
45
|
end
|
45
46
|
|
46
|
-
context 'when
|
47
|
+
context 'when extend_only_if_life flag is given' do
|
47
48
|
it 'does not extend a non-existent lock' do
|
48
|
-
@lock_info = lock_manager.lock(resource_key, ttl, extend: {value: 'hello world'},
|
49
|
+
@lock_info = lock_manager.lock(resource_key, ttl, extend: {value: 'hello world'}, extend_only_if_life: true)
|
49
50
|
expect(@lock_info).to eq(false)
|
50
51
|
end
|
51
52
|
end
|
52
53
|
|
53
|
-
context 'when
|
54
|
+
context 'when extend_only_if_life flag is not given' do
|
54
55
|
it "sets the given value when trying to extend a non-existent lock" do
|
55
|
-
@lock_info = lock_manager.lock(resource_key, ttl, extend: {value: 'hello world'},
|
56
|
+
@lock_info = lock_manager.lock(resource_key, ttl, extend: {value: 'hello world'}, extend_only_if_life: false)
|
56
57
|
expect(@lock_info).to be_lock_info_for(resource_key)
|
57
58
|
expect(@lock_info[:value]).to eq('hello world') # really we should test what's in redis
|
58
59
|
end
|
@@ -80,6 +81,33 @@ RSpec.describe Redlock::Client do
|
|
80
81
|
lock_info = lock_manager.lock(resource_key, ttl, extend: yet_another_lock_info)
|
81
82
|
expect(lock_info).to eql(false)
|
82
83
|
end
|
84
|
+
|
85
|
+
it 'retries up to \'retry_count\' times' do
|
86
|
+
expect(lock_manager).to receive(:lock_instances).exactly(
|
87
|
+
lock_manager_opts[:retry_count]).times.and_return(false)
|
88
|
+
lock_manager.lock(resource_key, ttl)
|
89
|
+
end
|
90
|
+
|
91
|
+
it 'sleeps in between retries' do
|
92
|
+
expect(lock_manager).to receive(:sleep).exactly(lock_manager_opts[:retry_count] - 1).times
|
93
|
+
lock_manager.lock(resource_key, ttl)
|
94
|
+
end
|
95
|
+
|
96
|
+
it 'sleeps at least the specified retry_delay in milliseconds' do
|
97
|
+
expected_minimum = described_class::DEFAULT_RETRY_DELAY
|
98
|
+
expect(lock_manager).to receive(:sleep) do |sleep|
|
99
|
+
expect(sleep).to satisfy { |value| value >= expected_minimum / 1000.to_f }
|
100
|
+
end.at_least(:once)
|
101
|
+
lock_manager.lock(resource_key, ttl)
|
102
|
+
end
|
103
|
+
|
104
|
+
it 'sleeps a maximum of retry_delay + retry_jitter in milliseconds' do
|
105
|
+
expected_maximum = described_class::DEFAULT_RETRY_DELAY + described_class::DEFAULT_RETRY_JITTER
|
106
|
+
expect(lock_manager).to receive(:sleep) do |sleep|
|
107
|
+
expect(sleep).to satisfy { |value| value < expected_maximum / 1000.to_f }
|
108
|
+
end.at_least(:once)
|
109
|
+
lock_manager.lock(resource_key, ttl)
|
110
|
+
end
|
83
111
|
end
|
84
112
|
|
85
113
|
context 'when script cache has been flushed' do
|
metadata
CHANGED
@@ -1,87 +1,87 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: redlock
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Leandro Moreira
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2017-02-02 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: redis
|
15
15
|
requirement: !ruby/object:Gem::Requirement
|
16
16
|
requirements:
|
17
|
-
- - ~>
|
17
|
+
- - "~>"
|
18
18
|
- !ruby/object:Gem::Version
|
19
19
|
version: '3'
|
20
|
-
- -
|
20
|
+
- - ">="
|
21
21
|
- !ruby/object:Gem::Version
|
22
22
|
version: 3.0.0
|
23
23
|
type: :runtime
|
24
24
|
prerelease: false
|
25
25
|
version_requirements: !ruby/object:Gem::Requirement
|
26
26
|
requirements:
|
27
|
-
- - ~>
|
27
|
+
- - "~>"
|
28
28
|
- !ruby/object:Gem::Version
|
29
29
|
version: '3'
|
30
|
-
- -
|
30
|
+
- - ">="
|
31
31
|
- !ruby/object:Gem::Version
|
32
32
|
version: 3.0.0
|
33
33
|
- !ruby/object:Gem::Dependency
|
34
34
|
name: coveralls
|
35
35
|
requirement: !ruby/object:Gem::Requirement
|
36
36
|
requirements:
|
37
|
-
- - ~>
|
37
|
+
- - "~>"
|
38
38
|
- !ruby/object:Gem::Version
|
39
39
|
version: 0.8.13
|
40
40
|
type: :development
|
41
41
|
prerelease: false
|
42
42
|
version_requirements: !ruby/object:Gem::Requirement
|
43
43
|
requirements:
|
44
|
-
- - ~>
|
44
|
+
- - "~>"
|
45
45
|
- !ruby/object:Gem::Version
|
46
46
|
version: 0.8.13
|
47
47
|
- !ruby/object:Gem::Dependency
|
48
48
|
name: rake
|
49
49
|
requirement: !ruby/object:Gem::Requirement
|
50
50
|
requirements:
|
51
|
-
- - ~>
|
51
|
+
- - "~>"
|
52
52
|
- !ruby/object:Gem::Version
|
53
53
|
version: '11.1'
|
54
|
-
- -
|
54
|
+
- - ">="
|
55
55
|
- !ruby/object:Gem::Version
|
56
56
|
version: 11.1.2
|
57
57
|
type: :development
|
58
58
|
prerelease: false
|
59
59
|
version_requirements: !ruby/object:Gem::Requirement
|
60
60
|
requirements:
|
61
|
-
- - ~>
|
61
|
+
- - "~>"
|
62
62
|
- !ruby/object:Gem::Version
|
63
63
|
version: '11.1'
|
64
|
-
- -
|
64
|
+
- - ">="
|
65
65
|
- !ruby/object:Gem::Version
|
66
66
|
version: 11.1.2
|
67
67
|
- !ruby/object:Gem::Dependency
|
68
68
|
name: rspec
|
69
69
|
requirement: !ruby/object:Gem::Requirement
|
70
70
|
requirements:
|
71
|
-
- - ~>
|
71
|
+
- - "~>"
|
72
72
|
- !ruby/object:Gem::Version
|
73
73
|
version: '3'
|
74
|
-
- -
|
74
|
+
- - ">="
|
75
75
|
- !ruby/object:Gem::Version
|
76
76
|
version: 3.0.0
|
77
77
|
type: :development
|
78
78
|
prerelease: false
|
79
79
|
version_requirements: !ruby/object:Gem::Requirement
|
80
80
|
requirements:
|
81
|
-
- - ~>
|
81
|
+
- - "~>"
|
82
82
|
- !ruby/object:Gem::Version
|
83
83
|
version: '3'
|
84
|
-
- -
|
84
|
+
- - ">="
|
85
85
|
- !ruby/object:Gem::Version
|
86
86
|
version: 3.0.0
|
87
87
|
description: Distributed lock using Redis written in Ruby. Highly inspired by https://github.com/antirez/redlock-rb.
|
@@ -91,9 +91,9 @@ executables: []
|
|
91
91
|
extensions: []
|
92
92
|
extra_rdoc_files: []
|
93
93
|
files:
|
94
|
-
- .gitignore
|
95
|
-
- .rspec
|
96
|
-
- .travis.yml
|
94
|
+
- ".gitignore"
|
95
|
+
- ".rspec"
|
96
|
+
- ".travis.yml"
|
97
97
|
- CONTRIBUTORS
|
98
98
|
- Gemfile
|
99
99
|
- Gemfile.lock
|
@@ -118,17 +118,17 @@ require_paths:
|
|
118
118
|
- lib
|
119
119
|
required_ruby_version: !ruby/object:Gem::Requirement
|
120
120
|
requirements:
|
121
|
-
- -
|
121
|
+
- - ">="
|
122
122
|
- !ruby/object:Gem::Version
|
123
123
|
version: '0'
|
124
124
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
125
125
|
requirements:
|
126
|
-
- -
|
126
|
+
- - ">="
|
127
127
|
- !ruby/object:Gem::Version
|
128
128
|
version: '0'
|
129
129
|
requirements: []
|
130
130
|
rubyforge_project:
|
131
|
-
rubygems_version: 2.4.
|
131
|
+
rubygems_version: 2.4.5
|
132
132
|
signing_key:
|
133
133
|
specification_version: 4
|
134
134
|
summary: Distributed lock using Redis written in Ruby.
|