restruct 0.0.3 → 0.1.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.
@@ -1,31 +1,35 @@
1
1
  module Restruct
2
2
  class Structure
3
3
 
4
- attr_reader :redis, :id
4
+ attr_reader :connection, :id
5
5
 
6
6
  def initialize(options={})
7
- @redis = options[:redis] || Restruct.redis
7
+ @connection = options[:connection] || Restruct.connection
8
8
  @id = Id.new options[:id] || Restruct.generate_id
9
9
  end
10
10
 
11
11
  def ==(object)
12
12
  object.class == self.class &&
13
13
  object.id == id &&
14
- object.redis == redis
14
+ object.connection == connection
15
15
  end
16
16
  alias_method :eql?, :==
17
17
 
18
18
  def dump
19
- redis.call 'DUMP', id
19
+ connection.call 'DUMP', id
20
20
  end
21
21
 
22
22
  def restore(dump)
23
23
  destroy
24
- redis.call 'RESTORE', id, 0, dump
24
+ connection.lazy 'RESTORE', id, 0, dump
25
25
  end
26
26
 
27
27
  def destroy
28
- redis.call 'DEL', id
28
+ connection.lazy 'DEL', id
29
+ end
30
+
31
+ def exists?
32
+ connection.call('EXISTS', id) == 1
29
33
  end
30
34
 
31
35
  end
@@ -1,3 +1,3 @@
1
1
  module Restruct
2
- VERSION = '0.0.3'
2
+ VERSION = '0.1.0'
3
3
  end
@@ -0,0 +1,21 @@
1
+ local locker_id = ARGV[1]
2
+ local lock_key = ARGV[2]
3
+ local exclusive = ARGV[3]
4
+
5
+ if redis.call('EXISTS', locker_id) == 1 then
6
+ local actual_lock_key = redis.call('HGET', locker_id, 'key')
7
+ local actual_exclusive = redis.call('HGET', locker_id, 'exclusive')
8
+
9
+ local error_message = "Lock " .. lock_key .. " (exclusive=" .. exclusive .. ") fail. Alradey locked by " .. actual_lock_key .. " (exclusive=" .. actual_exclusive .. ")"
10
+
11
+ if exclusive == "true" or
12
+ actual_exclusive == 'true' or
13
+ actual_lock_key ~= lock_key then
14
+
15
+ error(error_message)
16
+ end
17
+ end
18
+
19
+ redis.call('HSET', locker_id, 'key', lock_key)
20
+ redis.call('HSET', locker_id, 'exclusive', exclusive)
21
+ redis.call("HINCRBY", locker_id, 'nested', 1)
@@ -0,0 +1,10 @@
1
+ local locker_id = ARGV[1]
2
+
3
+ if redis.call('EXISTS', locker_id) == 0 then
4
+ error("Unregister Error. Dont exist key " .. locker_id)
5
+ else
6
+ redis.call("HINCRBY", locker_id, 'nested', -1)
7
+ if redis.call('HGET', locker_id, 'nested') == '0' then
8
+ redis.call('DEL', locker_id)
9
+ end
10
+ end
@@ -18,12 +18,13 @@ Gem::Specification.new do |spec|
18
18
  spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
19
  spec.require_paths = ['lib']
20
20
 
21
- spec.add_dependency 'redic', '~> 1.2.0'
21
+ spec.add_dependency 'redic', '~> 1.5.0'
22
22
  spec.add_dependency 'class_config', '~> 0.0.1'
23
23
 
24
24
  spec.add_development_dependency 'bundler', '~> 1.6'
25
25
  spec.add_development_dependency 'rake'
26
26
  spec.add_development_dependency 'minitest', '~> 4.7'
27
+ spec.add_development_dependency "minitest-great_expectations"
27
28
  spec.add_development_dependency 'turn', '~> 0.9'
28
29
  spec.add_development_dependency 'simplecov'
29
30
  spec.add_development_dependency 'pry-nav'
@@ -7,7 +7,7 @@ require 'minitest_helper'
7
7
  let(:array) { klass.new }
8
8
 
9
9
  def fill(elements)
10
- redis.call 'RPUSH', array.id, *(elements.map { |e| array.send(:serialize, e) })
10
+ connection.call 'RPUSH', array.id, *(elements.map { |e| array.send(:serialize, e) })
11
11
  end
12
12
 
13
13
  describe 'Getters' do
@@ -333,6 +333,42 @@ require 'minitest_helper'
333
333
  other.to_primitive.must_equal array.to_primitive
334
334
  end
335
335
 
336
+ it 'Batch' do
337
+ fill %w(a b c)
338
+
339
+ array.connection.batch do
340
+ array[1] = 'x' #a x c
341
+ array << 'd' #a x c d
342
+ array.shift #x c d
343
+ array.pop(2) #x
344
+ %w(y h z).each {|e| array << e} #x y h z
345
+ array.delete 'h' #x y z
346
+
347
+ array.to_a.must_equal %w(a b c)
348
+ end
349
+
350
+ array.to_a.must_equal %w(x y z)
351
+ end
352
+
353
+ it 'Restore/Destroy in batch' do
354
+ fill %w(a b c)
355
+
356
+ dump = array.dump
357
+ other = klass.new
358
+
359
+ array.connection.batch do
360
+ array.destroy
361
+ other.restore dump
362
+ array.must_be :exists?
363
+ other.wont_be :exists?
364
+ array.to_a.must_equal %w(a b c)
365
+ end
366
+
367
+ array.wont_be :exists?
368
+ other.must_be :exists?
369
+ other.to_a.must_equal %w(a b c)
370
+ end
371
+
336
372
  end
337
373
 
338
374
  end
@@ -0,0 +1,116 @@
1
+ require 'minitest_helper'
2
+
3
+ describe Restruct::Connection do
4
+
5
+ describe 'Success' do
6
+ it 'call' do
7
+ connection.call('HSET', 'test_id', 'key', 'test_key')
8
+ connection.call('HGET', 'test_id', 'key').must_equal 'test_key'
9
+ end
10
+
11
+ it 'script' do
12
+ script = %Q{
13
+ local id = ARGV[1]
14
+ local key = ARGV[2]
15
+ local value = ARGV[3]
16
+ redis.call('HSET', id, key, value)
17
+ return redis.call('HGET', id, key)
18
+ }
19
+ connection.script(script, 0, 'test_id', 'key', 'test_key').must_equal 'test_key'
20
+ end
21
+
22
+ it 'reload script when remove from the redis cache' do
23
+ script = %Q{
24
+ local id = ARGV[1]
25
+ return id
26
+ }
27
+ connection.script(script, 0, 'test').must_equal 'test'
28
+ connection.call 'SCRIPT', 'FLUSH'
29
+ connection.script(script, 0, 'test').must_equal 'test'
30
+ end
31
+
32
+ end
33
+
34
+ describe 'Errors' do
35
+
36
+ it 'ArgumentError' do
37
+ proc { connection.call }.must_raise ArgumentError
38
+ end
39
+
40
+ it 'NoScriptError' do
41
+ proc { connection.call 'EVALSHA', 'invalid sha', 0 }.must_raise Restruct::NoScriptError
42
+ end
43
+
44
+ it 'Invalid Script' do
45
+ proc { connection.script 'xyz', 0 }.must_raise Restruct::ConnectionError
46
+ end
47
+
48
+ it 'RuntimeScriptError' do
49
+ proc { connection.script 'return x', 0 }.must_raise Restruct::ConnectionError
50
+ end
51
+
52
+ end
53
+
54
+ describe 'Batch' do
55
+
56
+ let (:id) {Restruct.generate_id}
57
+
58
+ it 'Execute' do
59
+ connection.lazy('HSET', id, 'key_1', 'x')
60
+
61
+ connection.batch do
62
+ connection.lazy('HSET', id, 'key_1', 'y')
63
+ connection.lazy('HSET', id, 'key_2', 'x')
64
+
65
+ connection.call('HKEYS', id).must_equal ['key_1']
66
+ connection.call('HGET', id, 'key_1').must_equal 'x'
67
+ connection.call('HGET', id, 'key_2').must_be_nil
68
+ end
69
+
70
+ connection.call('HKEYS', id).must_equal ['key_1', 'key_2']
71
+ connection.call('HGET', id, 'key_1').must_equal 'y'
72
+ connection.call('HGET', id, 'key_2').must_equal 'x'
73
+ end
74
+
75
+ it 'Discard' do
76
+ connection.lazy('HSET', id, 'key_1', 'x')
77
+
78
+ proc do
79
+ connection.batch do
80
+ connection.lazy('HSET', id, 'key_1', 'y')
81
+ connection.lazy('HSET', id, 'key_2', 'x')
82
+ raise 'Test error'
83
+ end
84
+ end.must_raise RuntimeError
85
+
86
+ connection.call('HKEYS', id).must_equal ['key_1']
87
+ connection.call('HGET', id, 'key_1').must_equal 'x'
88
+ end
89
+
90
+ it 'Nested' do
91
+ connection.lazy('HSET', id, 'key_1', 'x')
92
+
93
+ connection.batch do
94
+ connection.lazy('HSET', id, 'key_1', 'y')
95
+ connection.lazy('HSET', id, 'key_2', 'x')
96
+
97
+ connection.batch do
98
+ connection.lazy('HSET', id, 'key_1', 'z')
99
+ connection.lazy('HSET', id, 'key_2', 'z')
100
+
101
+ connection.call('HGET', id, 'key_1').must_equal 'x'
102
+ connection.call('HGET', id, 'key_2').must_be_nil
103
+ end
104
+
105
+ connection.call('HGET', id, 'key_1').must_equal 'x'
106
+ connection.call('HGET', id, 'key_2').must_be_nil
107
+ end
108
+
109
+ connection.call('HKEYS', id).must_equal ['key_1', 'key_2']
110
+ connection.call('HGET', id, 'key_1').must_equal 'z'
111
+ connection.call('HGET', id, 'key_2').must_equal 'z'
112
+ end
113
+
114
+ end
115
+
116
+ end
@@ -8,7 +8,7 @@ require 'minitest_helper'
8
8
 
9
9
  def fill(data)
10
10
  data.each { |k,v| data[k] = hash.send(:serialize, v) }
11
- redis.call 'HMSET', hash.id, *data.flatten
11
+ connection.call 'HMSET', hash.id, *data.flatten
12
12
  end
13
13
 
14
14
  describe 'Getters' do
@@ -20,7 +20,7 @@ require 'minitest_helper'
20
20
  hash[:b].must_equal 'y'
21
21
  hash[:c].must_be_nil
22
22
  end
23
-
23
+
24
24
  it 'fetch' do
25
25
  fill a: 'x', b: 'y'
26
26
 
@@ -32,7 +32,7 @@ require 'minitest_helper'
32
32
  error = proc { hash.fetch(:c) }.must_raise KeyError
33
33
  error.message.must_equal 'key not found: c'
34
34
  end
35
-
35
+
36
36
  it 'key' do
37
37
  fill a: 'x', b: 'y', c: 'y'
38
38
 
@@ -237,6 +237,20 @@ require 'minitest_helper'
237
237
  other.to_primitive.must_equal hash.to_primitive
238
238
  end
239
239
 
240
+ it 'Batch' do
241
+ fill a: 'x', b: 'y', c: 'z'
242
+
243
+ hash.connection.batch do
244
+ hash[:d] = 'w'
245
+ hash.delete :a
246
+ hash.merge! b: 'x', e: 'v'
247
+
248
+ hash.to_h.must_equal 'a' => 'x', 'b' => 'y', 'c' => 'z'
249
+ end
250
+
251
+ hash.to_h.must_equal 'b' => 'x', 'c' => 'z', 'd' => 'w', 'e' => 'v'
252
+ end
253
+
240
254
  end
241
255
 
242
256
  end
@@ -0,0 +1,161 @@
1
+ require 'minitest_helper'
2
+
3
+ describe Restruct::Locker do
4
+
5
+ let(:locker) { Restruct::Locker.new }
6
+
7
+ it 'Flexible' do
8
+ locker.wont_be :locked?
9
+
10
+ locker.lock :process_1 do
11
+ locker.must_be :locked?
12
+ locker.locked_by.must_equal 'process_1'
13
+
14
+ locker.lock(:process_1) { }
15
+ locker.to_h.must_equal({"key"=>"process_1", "exclusive"=>"false", "nested"=>"1"})
16
+
17
+ error = proc { locker.lock :process_2 }.must_raise Restruct::LockerError
18
+ error.message.must_equal 'Lock process_2 (exclusive=false) fail. Alradey locked by process_1 (exclusive=false)'
19
+ end
20
+
21
+ locker.wont_be :locked?
22
+ end
23
+
24
+ it 'Strict' do
25
+ locker.wont_be :locked?
26
+
27
+ locker.lock! :process_1 do
28
+ locker.must_be :locked?
29
+ locker.locked_by.must_equal 'process_1'
30
+
31
+ error = proc { locker.lock! :process_1 }.must_raise Restruct::LockerError
32
+ error.message.must_equal 'Lock process_1 (exclusive=true) fail. Alradey locked by process_1 (exclusive=true)'
33
+
34
+ error = proc { locker.lock :process_2 }.must_raise Restruct::LockerError
35
+ error.message.must_equal 'Lock process_2 (exclusive=false) fail. Alradey locked by process_1 (exclusive=true)'
36
+ end
37
+
38
+ locker.wont_be :locked?
39
+ end
40
+
41
+ it 'Force unlock' do
42
+ connection.call('HSET', locker.id, 'key', :process_1)
43
+
44
+ locker.must_be :locked?
45
+ locker.locked_by.must_equal 'process_1'
46
+
47
+ locker.unlock!
48
+
49
+ locker.wont_be :locked?
50
+ end
51
+
52
+ it 'Threads safe' do
53
+ test_id = Restruct.generate_id[:test]
54
+
55
+ threads = 10.times.map do |thread_number|
56
+ Thread.new do
57
+ 10.times do |iteration|
58
+ locker.lock :process_1 do
59
+ connection.call 'RPUSH', test_id, "#{thread_number}-#{iteration}"
60
+ end
61
+ end
62
+ end
63
+ end
64
+
65
+ threads.each(&:join)
66
+
67
+ locker.wont_be :locked?
68
+
69
+ expected_list = []
70
+ 10.times do |i|
71
+ 10.times do |j|
72
+ expected_list << "#{i}-#{j}"
73
+ end
74
+ end
75
+
76
+ list = connection.call('LRANGE', test_id, 0, -1)
77
+ list.sort.must_equal expected_list
78
+ list.wont_equal expected_list
79
+ end
80
+
81
+ if RUBY_ENGINE != 'jruby'
82
+ it 'Multiples process' do
83
+ test_id = Restruct.generate_id[:test]
84
+ locker_id = locker.id
85
+
86
+ pids = 10.times.map do |thread_number|
87
+ Process.fork do
88
+ connection = Restruct::Connection.new
89
+ locker = Restruct::Locker.new id: locker_id, connection: connection
90
+ 10.times do |iteration|
91
+ locker.lock :process_1 do
92
+ connection.call 'RPUSH', test_id, "#{thread_number}-#{iteration}"
93
+ end
94
+ end
95
+ end
96
+ end
97
+
98
+ Process.waitall
99
+
100
+ locker.wont_be :locked?
101
+
102
+ expected_list = []
103
+ 10.times do |i|
104
+ 10.times do |j|
105
+ expected_list << "#{i}-#{j}"
106
+ end
107
+ end
108
+
109
+ list = connection.call('LRANGE', test_id, 0, -1)
110
+ list.sort.must_equal expected_list
111
+ end
112
+ end
113
+
114
+ it 'Return block result' do
115
+ result = locker.lock :process_1 do
116
+ 'OK'
117
+ end
118
+
119
+ result.must_equal 'OK'
120
+ end
121
+
122
+ it 'Unlock when raise error in block' do
123
+ locker.wont_be :locked?
124
+
125
+ proc do
126
+ locker.lock :process_1 do
127
+ raise RuntimeError, 'ERROR'
128
+ end
129
+ end.must_raise RuntimeError, 'ERROR'
130
+
131
+ locker.wont_be :locked?
132
+ end
133
+
134
+ it 'Batch' do
135
+ id = Restruct.generate_id[:test]
136
+ connection.lazy('HSET', id, 'key_1', 'x')
137
+
138
+ connection.batch do
139
+ connection.lazy('HSET', id, 'key_1', 'y')
140
+
141
+ locker.wont_be :locked?
142
+
143
+ locker.lock :process_1 do
144
+ locker.must_be :locked?
145
+ connection.lazy('HSET', id, 'key_2', 'x')
146
+ locker.lock :process_1 do
147
+ connection.lazy('HSET', id, 'key_2', 'z')
148
+ locker.to_h['nested'].must_equal '2'
149
+ end
150
+ locker.must_be :locked?
151
+ locker.to_h['nested'].must_equal '1'
152
+ end
153
+
154
+ locker.wont_be :locked?
155
+ end
156
+ connection.call('HKEYS', id).must_equal ['key_1', 'key_2']
157
+ connection.call('HGET', id, 'key_1').must_equal 'y'
158
+ connection.call('HGET', id, 'key_2').must_equal 'z'
159
+ end
160
+
161
+ end