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