zermelo 1.0.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.
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