zermelo 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (69) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +16 -0
  3. data/.rspec +10 -0
  4. data/.travis.yml +27 -0
  5. data/Gemfile +20 -0
  6. data/LICENSE.txt +22 -0
  7. data/README.md +512 -0
  8. data/Rakefile +1 -0
  9. data/lib/zermelo/associations/association_data.rb +24 -0
  10. data/lib/zermelo/associations/belongs_to.rb +115 -0
  11. data/lib/zermelo/associations/class_methods.rb +244 -0
  12. data/lib/zermelo/associations/has_and_belongs_to_many.rb +128 -0
  13. data/lib/zermelo/associations/has_many.rb +120 -0
  14. data/lib/zermelo/associations/has_one.rb +109 -0
  15. data/lib/zermelo/associations/has_sorted_set.rb +124 -0
  16. data/lib/zermelo/associations/index.rb +50 -0
  17. data/lib/zermelo/associations/index_data.rb +18 -0
  18. data/lib/zermelo/associations/unique_index.rb +44 -0
  19. data/lib/zermelo/backends/base.rb +115 -0
  20. data/lib/zermelo/backends/influxdb_backend.rb +178 -0
  21. data/lib/zermelo/backends/redis_backend.rb +281 -0
  22. data/lib/zermelo/filters/base.rb +235 -0
  23. data/lib/zermelo/filters/influxdb_filter.rb +162 -0
  24. data/lib/zermelo/filters/redis_filter.rb +558 -0
  25. data/lib/zermelo/filters/steps/base_step.rb +22 -0
  26. data/lib/zermelo/filters/steps/diff_range_step.rb +17 -0
  27. data/lib/zermelo/filters/steps/diff_step.rb +17 -0
  28. data/lib/zermelo/filters/steps/intersect_range_step.rb +17 -0
  29. data/lib/zermelo/filters/steps/intersect_step.rb +17 -0
  30. data/lib/zermelo/filters/steps/limit_step.rb +17 -0
  31. data/lib/zermelo/filters/steps/offset_step.rb +17 -0
  32. data/lib/zermelo/filters/steps/sort_step.rb +17 -0
  33. data/lib/zermelo/filters/steps/union_range_step.rb +17 -0
  34. data/lib/zermelo/filters/steps/union_step.rb +17 -0
  35. data/lib/zermelo/locks/no_lock.rb +16 -0
  36. data/lib/zermelo/locks/redis_lock.rb +221 -0
  37. data/lib/zermelo/records/base.rb +62 -0
  38. data/lib/zermelo/records/class_methods.rb +127 -0
  39. data/lib/zermelo/records/collection.rb +14 -0
  40. data/lib/zermelo/records/errors.rb +24 -0
  41. data/lib/zermelo/records/influxdb_record.rb +35 -0
  42. data/lib/zermelo/records/instance_methods.rb +224 -0
  43. data/lib/zermelo/records/key.rb +19 -0
  44. data/lib/zermelo/records/redis_record.rb +27 -0
  45. data/lib/zermelo/records/type_validator.rb +20 -0
  46. data/lib/zermelo/version.rb +3 -0
  47. data/lib/zermelo.rb +102 -0
  48. data/spec/lib/zermelo/associations/belongs_to_spec.rb +6 -0
  49. data/spec/lib/zermelo/associations/has_many_spec.rb +6 -0
  50. data/spec/lib/zermelo/associations/has_one_spec.rb +6 -0
  51. data/spec/lib/zermelo/associations/has_sorted_set.spec.rb +6 -0
  52. data/spec/lib/zermelo/associations/index_spec.rb +6 -0
  53. data/spec/lib/zermelo/associations/unique_index_spec.rb +6 -0
  54. data/spec/lib/zermelo/backends/influxdb_backend_spec.rb +0 -0
  55. data/spec/lib/zermelo/backends/moneta_backend_spec.rb +0 -0
  56. data/spec/lib/zermelo/filters/influxdb_filter_spec.rb +0 -0
  57. data/spec/lib/zermelo/filters/redis_filter_spec.rb +0 -0
  58. data/spec/lib/zermelo/locks/redis_lock_spec.rb +170 -0
  59. data/spec/lib/zermelo/records/influxdb_record_spec.rb +258 -0
  60. data/spec/lib/zermelo/records/key_spec.rb +6 -0
  61. data/spec/lib/zermelo/records/redis_record_spec.rb +1426 -0
  62. data/spec/lib/zermelo/records/type_validator_spec.rb +6 -0
  63. data/spec/lib/zermelo/version_spec.rb +6 -0
  64. data/spec/lib/zermelo_spec.rb +6 -0
  65. data/spec/spec_helper.rb +67 -0
  66. data/spec/support/profile_all_formatter.rb +44 -0
  67. data/spec/support/uncolored_doc_formatter.rb +74 -0
  68. data/zermelo.gemspec +30 -0
  69. metadata +174 -0
@@ -0,0 +1,224 @@
1
+ require 'zermelo/records/key'
2
+
3
+ module Zermelo
4
+
5
+ module Records
6
+
7
+ # module renamed to avoid ActiveSupport::Concern deprecation warning
8
+ module InstMethods
9
+
10
+ def initialize(attributes = {})
11
+ @is_new = true
12
+ @attributes = {}
13
+ attributes.each_pair do |k, v|
14
+ self.send("#{k}=".to_sym, v)
15
+ end
16
+ end
17
+
18
+ def persisted?
19
+ !@is_new
20
+ end
21
+
22
+ def load(id)
23
+ self.id = id
24
+ refresh
25
+ end
26
+
27
+ def refresh
28
+ # AM::Dirty -- private method 'clear_changes_information' in 4.2.0+,
29
+ # private method 'reset_changes' in 4.1.0+, internal state before that
30
+ if self.respond_to?(:clear_changes_information, true)
31
+ clear_changes_information
32
+ elsif self.respond_to?(:reset_changes, true)
33
+ reset_changes
34
+ else
35
+ @previously_changed.clear unless @previously_changed.nil?
36
+ @changed_attributes.clear
37
+ end
38
+
39
+ attr_types = self.class.attribute_types
40
+
41
+ @attributes = {'id' => self.id}
42
+
43
+ attrs = nil
44
+
45
+ self.class.lock do
46
+ class_key = self.class.send(:class_key)
47
+
48
+ # TODO: check for record existence in backend-agnostic fashion
49
+ @is_new = false
50
+
51
+ attr_types = self.class.attribute_types.reject {|k, v| k == :id}
52
+
53
+ attrs_to_load = attr_types.collect do |name, type|
54
+ Zermelo::Records::Key.new(:klass => class_key,
55
+ :id => self.id, :name => name, :type => type, :object => :attribute)
56
+ end
57
+
58
+ attrs = backend.get_multiple(*attrs_to_load)[class_key][self.id]
59
+ end
60
+
61
+ return false unless attrs.present?
62
+ @attributes.update(attrs)
63
+ true
64
+ end
65
+
66
+ # TODO limit to only those attribute names defined in define_attributes
67
+ def update_attributes(attributes = {})
68
+ attributes.each_pair do |att, v|
69
+ unless value == @attributes[att.to_s]
70
+ @attributes[att.to_s] = v
71
+ send("#{att}_will_change!")
72
+ end
73
+ end
74
+ save
75
+ end
76
+
77
+ def save
78
+ return unless @is_new || self.changed?
79
+ self.id ||= self.class.generate_id
80
+ return false unless valid?
81
+
82
+ creating = !self.persisted?
83
+
84
+ run_callbacks( (creating ? :create : :update) ) do
85
+
86
+ idx_attrs = self.class.send(:with_index_data) do |d|
87
+ idx_attrs = d.each_with_object({}) do |(name, data), memo|
88
+ memo[name.to_s] = data.index_klass
89
+ end
90
+ end
91
+
92
+ self.class.transaction do
93
+
94
+ apply_attribute = proc {|att, attr_key, old_new|
95
+ backend.set(attr_key, old_new.last) unless att.eql?('id')
96
+
97
+ if idx_attrs.has_key?(att)
98
+ # update indices
99
+ if creating
100
+ self.class.send("#{att}_index").add_id( @attributes['id'], old_new.last)
101
+ else
102
+ self.class.send("#{att}_index").move_id( @attributes['id'], old_new.first,
103
+ self.class.send("#{att}_index"), old_new.last)
104
+ end
105
+ end
106
+ }
107
+
108
+ attr_keys = attribute_keys
109
+
110
+ if creating
111
+ attribute_keys.each_pair do |att, attr_key|
112
+ apply_attribute.call(att, attr_key, [nil, @attributes[att]])
113
+ end
114
+ else
115
+ self.changes.each_pair do |att, old_new|
116
+ apply_attribute.call(att, attr_keys[att], old_new)
117
+ end
118
+ end
119
+
120
+ # ids is a set, so update won't create duplicates
121
+ # NB influxdb backend doesn't need this
122
+ self.class.add_id(@attributes['id'])
123
+ end
124
+
125
+ @is_new = false
126
+ end
127
+
128
+ # AM::Dirty -- private method in 4.1.0+, internal state before that
129
+ if self.respond_to?(:changes_applied, true)
130
+ changes_applied
131
+ else
132
+ @previously_changed = changes
133
+ @changed_attributes.clear
134
+ end
135
+
136
+ true
137
+ end
138
+
139
+ def destroy
140
+ raise "Record was not persisted" if !persisted?
141
+
142
+ run_callbacks :destroy do
143
+
144
+ assoc_classes = self.class.send(:associated_classes)
145
+ index_attrs = self.class.send(:with_index_data) {|d| d.keys }
146
+
147
+ self.class.lock(*assoc_classes) do
148
+ self.class.send(:with_associations, self) do |assoc|
149
+ assoc.send(:on_remove)
150
+ end
151
+
152
+ self.class.transaction do
153
+ self.class.delete_id(@attributes['id'])
154
+ index_attrs.each do |att|
155
+ idx = self.class.send("#{att}_index")
156
+ idx.delete_id( @attributes['id'], @attributes[att.to_s])
157
+ end
158
+
159
+ self.class.attribute_types.each_pair {|name, type|
160
+ key = Zermelo::Records::Key.new(:klass => self.class.send(:class_key),
161
+ :id => self.id, :name => name.to_s, :type => type, :object => :attribute)
162
+ backend.clear(key)
163
+ }
164
+
165
+ record_key = Zermelo::Records::Key.new(:klass => self.class.send(:class_key),
166
+ :id => self.id)
167
+ backend.purge(record_key)
168
+ end
169
+ end
170
+ end
171
+ end
172
+
173
+ private
174
+
175
+ def backend
176
+ self.class.send(:backend)
177
+ end
178
+
179
+ def attribute_keys
180
+ @attribute_keys ||= self.class.attribute_types.reject {|k, v|
181
+ k == :id
182
+ }.inject({}) {|memo, (name, type)|
183
+ memo[name.to_s] = Zermelo::Records::Key.new(:klass => self.class.send(:class_key),
184
+ :id => self.id, :name => name.to_s, :type => type, :object => :attribute)
185
+ memo
186
+ }
187
+ end
188
+
189
+ # http://stackoverflow.com/questions/7613574/activemodel-fields-not-mapped-to-accessors
190
+ #
191
+ # Simulate attribute writers from method_missing
192
+ def attribute=(att, value)
193
+ return if value == @attributes[att.to_s]
194
+ if att.to_s == 'id'
195
+ raise "Cannot reassign id" unless @attributes['id'].nil?
196
+ send("id_will_change!")
197
+ @attributes['id'] = value.to_s
198
+ else
199
+ send("#{att}_will_change!")
200
+ if (self.class.attribute_types[att.to_sym] == :set) && !value.is_a?(Set)
201
+ @attributes[att.to_s] = Set.new(value)
202
+ else
203
+ @attributes[att.to_s] = value
204
+ end
205
+ end
206
+ end
207
+
208
+ # Simulate attribute readers from method_missing
209
+ def attribute(att)
210
+ value = @attributes[att.to_s]
211
+ return value unless (self.class.attribute_types[att.to_sym] == :timestamp)
212
+ value.is_a?(Integer) ? Time.at(value) : value
213
+ end
214
+
215
+ # Used by ActiveModel to lookup attributes during validations.
216
+ def read_attribute_for_validation(att)
217
+ @attributes[att.to_s]
218
+ end
219
+
220
+ end
221
+
222
+ end
223
+
224
+ end
@@ -0,0 +1,19 @@
1
+ module Zermelo
2
+ module Records
3
+ class Key
4
+
5
+ # id / if nil, it's a class variable
6
+ # object / :association, :attribute or :index
7
+ # accessor / if a complex type, some way of getting sub-value
8
+ attr_reader :klass, :id, :name, :accessor, :type, :object
9
+
10
+ # TODO better validation of data, e.g. accessor valid for type, etc.
11
+ def initialize(opts = {})
12
+ [:klass, :id, :name, :accessor, :type, :object].each do |iv|
13
+ instance_variable_set("@#{iv}", opts[iv])
14
+ end
15
+ end
16
+
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,27 @@
1
+ require 'active_support/concern'
2
+
3
+ require 'zermelo/records/base'
4
+
5
+ # TODO check escaping of ids and index_keys -- shouldn't allow bare :, ' '
6
+
7
+ # TODO callbacks on before/after add/delete on association?
8
+
9
+ module Zermelo
10
+
11
+ module Records
12
+
13
+ module RedisRecord
14
+
15
+ extend ActiveSupport::Concern
16
+
17
+ include Zermelo::Records::Base
18
+
19
+ included do
20
+ set_backend :redis
21
+ end
22
+
23
+ end
24
+
25
+ end
26
+
27
+ end
@@ -0,0 +1,20 @@
1
+ module Zermelo
2
+ module Records
3
+ class TypeValidator < ActiveModel::Validator
4
+ def validate(record)
5
+ attr_types = record.class.attribute_types
6
+
7
+ attr_types.each_pair do |name, type|
8
+ value = record.send(name)
9
+ next if value.nil?
10
+ valid_type = Zermelo::ALL_TYPES[type]
11
+ unless valid_type.any? {|type| value.is_a?(type) }
12
+ count = (valid_type.size > 1) ? 'one of ' : ''
13
+ type_str = valid_type.collect {|type| type.name }.join(", ")
14
+ record.errors.add(name, "should be #{count}#{type_str} but is #{value.class.name}")
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,3 @@
1
+ module Zermelo
2
+ VERSION = '1.0.0'
3
+ end
data/lib/zermelo.rb ADDED
@@ -0,0 +1,102 @@
1
+ require 'zermelo/version'
2
+
3
+ require 'time'
4
+
5
+ module Zermelo
6
+
7
+ # backport for Ruby 1.8
8
+ unless Enumerable.instance_methods.include?(:each_with_object)
9
+ module ::Enumerable
10
+ def each_with_object(memo)
11
+ return to_enum :each_with_object, memo unless block_given?
12
+ each do |element|
13
+ yield element, memo
14
+ end
15
+ memo
16
+ end
17
+ end
18
+ end
19
+
20
+ # acceptable class types, which will be normalized on a per-backend basis
21
+ ATTRIBUTE_TYPES = {:string => [String],
22
+ :integer => [Integer],
23
+ :float => [Float],
24
+ :id => [String],
25
+ :timestamp => [Integer, Time, DateTime],
26
+ :boolean => [TrueClass, FalseClass],
27
+ }
28
+
29
+ COLLECTION_TYPES = {:list => [Enumerable],
30
+ :set => [Set],
31
+ :hash => [Hash],
32
+ :sorted_set => [Enumerable]
33
+ }
34
+
35
+ ALL_TYPES = ATTRIBUTE_TYPES.merge(COLLECTION_TYPES)
36
+
37
+ class << self
38
+
39
+ def valid_type?(type)
40
+ ALL_TYPES.keys.include?(type)
41
+ end
42
+
43
+ # Thread and fiber-local
44
+ [:redis, :influxdb].each do |backend|
45
+ define_method(backend) do
46
+ Thread.current["zermelo_#{backend.to_s}".to_sym]
47
+ end
48
+ end
49
+
50
+ def redis=(connection)
51
+ Thread.current[:zermelo_redis] = connection.nil? ? nil :
52
+ Zermelo::ConnectionProxy.new(connection)
53
+ Thread.current[:zermelo_redis_version] = nil
54
+ end
55
+
56
+ def influxdb=(connection)
57
+ Thread.current[:zermelo_influxdb] = Zermelo::ConnectionProxy.new(connection)
58
+ end
59
+
60
+ def redis_version
61
+ return nil if Zermelo.redis.nil?
62
+ rv = Thread.current[:zermelo_redis_version]
63
+ return rv unless rv.nil?
64
+ Thread.current[:zermelo_redis_version] = Zermelo.redis.info['redis_version']
65
+ end
66
+
67
+ def logger=(l)
68
+ Thread.current[:zermelo_logger] = l
69
+ end
70
+
71
+ def logger
72
+ Thread.current[:zermelo_logger]
73
+ end
74
+ end
75
+
76
+ class ConnectionProxy
77
+ def initialize(connection)
78
+ @proxied_connection = connection
79
+ end
80
+
81
+ # need to override Kernel.exec
82
+ def exec
83
+ @proxied_connection.exec
84
+ end
85
+
86
+ def respond_to?(name, include_private = false)
87
+ @proxied_connection.respond_to?(name, include_private)
88
+ end
89
+
90
+ def method_missing(name, *args, &block)
91
+ unless Zermelo.logger.nil?
92
+ Zermelo.logger.debug {
93
+ debug_str = "#{name}"
94
+ debug_str += " #{args.inspect}" unless args.empty?
95
+ debug_str
96
+ }
97
+ end
98
+ @proxied_connection.send(name, *args, &block)
99
+ end
100
+ end
101
+
102
+ end
@@ -0,0 +1,6 @@
1
+ require 'spec_helper'
2
+ require 'zermelo/associations/belongs_to'
3
+
4
+ describe Zermelo::Associations::BelongsTo, :redis => true do
5
+
6
+ end
@@ -0,0 +1,6 @@
1
+ require 'spec_helper'
2
+ require 'zermelo/associations/has_many'
3
+
4
+ describe Zermelo::Associations::HasMany, :redis => true do
5
+
6
+ end
@@ -0,0 +1,6 @@
1
+ require 'spec_helper'
2
+ require 'zermelo/associations/has_one'
3
+
4
+ describe Zermelo::Associations::HasOne, :redis => true do
5
+
6
+ end
@@ -0,0 +1,6 @@
1
+ require 'spec_helper'
2
+ require 'zermelo/associations/has_sorted_set'
3
+
4
+ describe Zermelo::Associations::HasSortedSet, :redis => true do
5
+
6
+ end
@@ -0,0 +1,6 @@
1
+ require 'spec_helper'
2
+ require 'zermelo/associations/index'
3
+
4
+ describe Zermelo::Associations::Index, :redis => true do
5
+
6
+ end
@@ -0,0 +1,6 @@
1
+ require 'spec_helper'
2
+ require 'zermelo/associations/unique_index'
3
+
4
+ describe Zermelo::Associations::UniqueIndex, :redis => true do
5
+
6
+ end
File without changes
File without changes
File without changes
@@ -0,0 +1,170 @@
1
+ require 'spec_helper'
2
+ require 'zermelo/locks/redis_lock'
3
+ require 'zermelo/records/redis_record'
4
+
5
+ describe Zermelo::Locks::RedisLock, :redis => true do
6
+
7
+ let(:redis) { Zermelo.redis }
8
+
9
+ module Zermelo
10
+ class RedisLockExample
11
+ include Zermelo::Records::RedisRecord
12
+ define_attributes :name => :string
13
+ end
14
+
15
+ class AnotherExample
16
+ include Zermelo::Records::RedisRecord
17
+ define_attributes :age => :integer
18
+ end
19
+ end
20
+
21
+ it "locks access to class-level data" do
22
+ expect(redis.keys('*')).to be_empty
23
+
24
+ slock = Zermelo::Locks::RedisLock.new
25
+ locked = slock.lock(Zermelo::RedisLockExample)
26
+ expect(locked).to be_truthy
27
+
28
+ lock_keys = ["redis_lock_example::lock:owner", "redis_lock_example::lock:expiry"]
29
+ expect(redis.keys('*')).to match_array(lock_keys)
30
+
31
+ example = Zermelo::RedisLockExample.new(:name => 'temporary')
32
+ example.save
33
+
34
+ example_keys = ["redis_lock_example::attrs:ids", "redis_lock_example:#{example.id}:attrs"]
35
+ expect(redis.keys('*')).to match_array(lock_keys + example_keys)
36
+
37
+ unlocked = slock.unlock
38
+ expect(unlocked).to be_truthy
39
+ expect(redis.keys('*')).to match_array(example_keys)
40
+ end
41
+
42
+ it "locks access to class-level data from two classes" do
43
+ expect(redis.keys('*')).to be_empty
44
+
45
+ slock = Zermelo::Locks::RedisLock.new
46
+ locked = slock.lock(Zermelo::RedisLockExample, Zermelo::AnotherExample)
47
+ expect(locked).to be_truthy
48
+
49
+ lock_keys = ["another_example::lock:owner", "another_example::lock:expiry",
50
+ "redis_lock_example::lock:owner", "redis_lock_example::lock:expiry"]
51
+ expect(redis.keys('*')).to match_array(lock_keys)
52
+
53
+ redis_lock_example = Zermelo::RedisLockExample.new(:name => 'temporary')
54
+ redis_lock_example.save
55
+
56
+ another_example = Zermelo::AnotherExample.new(:age => 36)
57
+ another_example.save
58
+
59
+ example_keys = ["redis_lock_example::attrs:ids", "redis_lock_example:#{redis_lock_example.id}:attrs",
60
+ "another_example::attrs:ids", "another_example:#{another_example.id}:attrs"]
61
+ expect(redis.keys('*')).to match_array(lock_keys + example_keys)
62
+
63
+ unlocked = slock.unlock
64
+ expect(unlocked).to be_truthy
65
+ expect(redis.keys('*')).to match_array(example_keys)
66
+ end
67
+
68
+ it "extends an existing lock", :time => true do
69
+ slock = Zermelo::Locks::RedisLock.new
70
+ slock.life = 60
71
+
72
+ time = Time.local(2012, 1, 1, 12, 0, 0)
73
+ Timecop.freeze(time)
74
+
75
+ locked = slock.lock(Zermelo::RedisLockExample)
76
+
77
+ expiry_time = redis.get("redis_lock_example::lock:expiry")
78
+ expect(expiry_time.to_i).to eq(time.to_i + 60)
79
+
80
+ Timecop.travel(time + 45)
81
+
82
+ extended = slock.extend_life(30)
83
+ expect(extended).to be_truthy
84
+
85
+ expiry_time = redis.get("redis_lock_example::lock:expiry")
86
+ expect(expiry_time.to_i).to eq(time.to_i + 75)
87
+
88
+ unlocked = slock.unlock
89
+ expect(unlocked).to be_truthy
90
+ end
91
+
92
+ it "expires a lock when its lifetime has expired"
93
+
94
+ it "stops another thread from accessing a lock on a single class while held" do
95
+ monitor = Monitor.new
96
+
97
+ times = {}
98
+
99
+ slock = Zermelo::Locks::RedisLock.new
100
+ locked = slock.lock(Zermelo::RedisLockExample)
101
+ expect(locked).to be_truthy
102
+
103
+ # ensure Redis connection is instantiated
104
+ redis
105
+
106
+ t = Thread.new do
107
+ Zermelo.redis = redis # thread-local
108
+
109
+ slock.lock(Zermelo::RedisLockExample)
110
+ expect(locked).to be_truthy
111
+
112
+ monitor.synchronize do
113
+ times['thread'] = Time.now
114
+ end
115
+
116
+ unlocked = slock.unlock
117
+ expect(unlocked).to be_truthy
118
+ end
119
+
120
+ monitor.synchronize do
121
+ times['main'] = Time.now
122
+ end
123
+
124
+ sleep 0.25
125
+ slock.unlock
126
+
127
+ t.join
128
+
129
+ expect(times['thread'] - times['main']).to be >= 0.25
130
+ end
131
+
132
+ it "stops another thread from accessing a lock on multiple classes while held" do
133
+ monitor = Monitor.new
134
+
135
+ times = {}
136
+
137
+ slock = Zermelo::Locks::RedisLock.new
138
+ locked = slock.lock(Zermelo::RedisLockExample, Zermelo::AnotherExample)
139
+ expect(locked).to be_truthy
140
+
141
+ # ensure Redis connection is instantiated
142
+ redis
143
+
144
+ t = Thread.new do
145
+ Zermelo.redis = redis # thread-local
146
+
147
+ slock.lock(Zermelo::RedisLockExample)
148
+ expect(locked).to be_truthy
149
+
150
+ monitor.synchronize do
151
+ times['thread'] = Time.now
152
+ end
153
+
154
+ unlocked = slock.unlock
155
+ expect(unlocked).to be_truthy
156
+ end
157
+
158
+ monitor.synchronize do
159
+ times['main'] = Time.now
160
+ end
161
+
162
+ sleep 0.25
163
+ slock.unlock
164
+
165
+ t.join
166
+
167
+ expect(times['thread'] - times['main']).to be >= 0.25
168
+ end
169
+
170
+ end