zermelo 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +16 -0
- data/.rspec +10 -0
- data/.travis.yml +27 -0
- data/Gemfile +20 -0
- data/LICENSE.txt +22 -0
- data/README.md +512 -0
- data/Rakefile +1 -0
- data/lib/zermelo/associations/association_data.rb +24 -0
- data/lib/zermelo/associations/belongs_to.rb +115 -0
- data/lib/zermelo/associations/class_methods.rb +244 -0
- data/lib/zermelo/associations/has_and_belongs_to_many.rb +128 -0
- data/lib/zermelo/associations/has_many.rb +120 -0
- data/lib/zermelo/associations/has_one.rb +109 -0
- data/lib/zermelo/associations/has_sorted_set.rb +124 -0
- data/lib/zermelo/associations/index.rb +50 -0
- data/lib/zermelo/associations/index_data.rb +18 -0
- data/lib/zermelo/associations/unique_index.rb +44 -0
- data/lib/zermelo/backends/base.rb +115 -0
- data/lib/zermelo/backends/influxdb_backend.rb +178 -0
- data/lib/zermelo/backends/redis_backend.rb +281 -0
- data/lib/zermelo/filters/base.rb +235 -0
- data/lib/zermelo/filters/influxdb_filter.rb +162 -0
- data/lib/zermelo/filters/redis_filter.rb +558 -0
- data/lib/zermelo/filters/steps/base_step.rb +22 -0
- data/lib/zermelo/filters/steps/diff_range_step.rb +17 -0
- data/lib/zermelo/filters/steps/diff_step.rb +17 -0
- data/lib/zermelo/filters/steps/intersect_range_step.rb +17 -0
- data/lib/zermelo/filters/steps/intersect_step.rb +17 -0
- data/lib/zermelo/filters/steps/limit_step.rb +17 -0
- data/lib/zermelo/filters/steps/offset_step.rb +17 -0
- data/lib/zermelo/filters/steps/sort_step.rb +17 -0
- data/lib/zermelo/filters/steps/union_range_step.rb +17 -0
- data/lib/zermelo/filters/steps/union_step.rb +17 -0
- data/lib/zermelo/locks/no_lock.rb +16 -0
- data/lib/zermelo/locks/redis_lock.rb +221 -0
- data/lib/zermelo/records/base.rb +62 -0
- data/lib/zermelo/records/class_methods.rb +127 -0
- data/lib/zermelo/records/collection.rb +14 -0
- data/lib/zermelo/records/errors.rb +24 -0
- data/lib/zermelo/records/influxdb_record.rb +35 -0
- data/lib/zermelo/records/instance_methods.rb +224 -0
- data/lib/zermelo/records/key.rb +19 -0
- data/lib/zermelo/records/redis_record.rb +27 -0
- data/lib/zermelo/records/type_validator.rb +20 -0
- data/lib/zermelo/version.rb +3 -0
- data/lib/zermelo.rb +102 -0
- data/spec/lib/zermelo/associations/belongs_to_spec.rb +6 -0
- data/spec/lib/zermelo/associations/has_many_spec.rb +6 -0
- data/spec/lib/zermelo/associations/has_one_spec.rb +6 -0
- data/spec/lib/zermelo/associations/has_sorted_set.spec.rb +6 -0
- data/spec/lib/zermelo/associations/index_spec.rb +6 -0
- data/spec/lib/zermelo/associations/unique_index_spec.rb +6 -0
- data/spec/lib/zermelo/backends/influxdb_backend_spec.rb +0 -0
- data/spec/lib/zermelo/backends/moneta_backend_spec.rb +0 -0
- data/spec/lib/zermelo/filters/influxdb_filter_spec.rb +0 -0
- data/spec/lib/zermelo/filters/redis_filter_spec.rb +0 -0
- data/spec/lib/zermelo/locks/redis_lock_spec.rb +170 -0
- data/spec/lib/zermelo/records/influxdb_record_spec.rb +258 -0
- data/spec/lib/zermelo/records/key_spec.rb +6 -0
- data/spec/lib/zermelo/records/redis_record_spec.rb +1426 -0
- data/spec/lib/zermelo/records/type_validator_spec.rb +6 -0
- data/spec/lib/zermelo/version_spec.rb +6 -0
- data/spec/lib/zermelo_spec.rb +6 -0
- data/spec/spec_helper.rb +67 -0
- data/spec/support/profile_all_formatter.rb +44 -0
- data/spec/support/uncolored_doc_formatter.rb +74 -0
- data/zermelo.gemspec +30 -0
- 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
|
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
|
File without changes
|
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
|