oflow 0.4.0 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: a5ebeca0ca2d46e8134511f55eeb680352c49a0a
4
- data.tar.gz: 7aacf1ca2bde1fd1413695f607a6d087bfbc8935
3
+ metadata.gz: c60c057bc56ccdf1457715ab9e7c73af54614bb2
4
+ data.tar.gz: b20b4b16b01f875c7848bb9f442ca3e67213cda7
5
5
  SHA512:
6
- metadata.gz: 4d8a98f7a6362f81f1fe83d56017b5cf09ced84f59be798fa60edbd647031a83772e0045ac2559d720d79b2367559e18471e24aadc80a789305932cb529c7a7b
7
- data.tar.gz: ab9864fcea8cb033cbe1c7926ef8edee98934a741d2eae89a7db1f8a7b9e3ac85de436a50931e7110652c38ec0158530f047b0c6e0591ffe696e76465a026e29
6
+ metadata.gz: a22ef32835f66cb4cccc81330a76d82c25a03ce3c646a878ea9ebe4b89fc4b2011747e540a53fe1e87c7e530e34407af8309139e5a87bbec1cd124edc3157930
7
+ data.tar.gz: d9e903de2ecafa7b7cb386182bcc194905f7d571801dccde61ebbab82e4236f4b28626854d61d8f2556164d8e710536d120e5a1ec103a0edf10d9752af2d52bb
data/README.md CHANGED
@@ -25,6 +25,10 @@ Follow [@peterohler on Twitter](http://twitter.com/#!/peterohler) for announceme
25
25
 
26
26
  ## Release Notes
27
27
 
28
+ ### Release 0.5
29
+
30
+ - Added Persister Actor that acts as a simple local store database.
31
+
28
32
  ### Release 0.4
29
33
 
30
34
  - Added support for dynamic Timer options updates.
@@ -11,4 +11,5 @@ require 'oflow/actors/errorhandler'
11
11
 
12
12
  require 'oflow/actors/balancer'
13
13
  require 'oflow/actors/merger'
14
+ require 'oflow/actors/persister'
14
15
  require 'oflow/actors/timer'
@@ -0,0 +1,267 @@
1
+ require 'oj'
2
+
3
+ module OFlow
4
+ module Actors
5
+
6
+ # Actor that persists records to the local file system as JSON
7
+ # representations of the records. Records can be the whole contents of the
8
+ # box received or a sub element of the contents. The key to the records are
9
+ # keys provided either in the record data or outside the data but somewhere
10
+ # else in the box received. Options for maintaining historic records and
11
+ # sequence number locking are included. If no sequence number is provide the
12
+ # Persister will assume there is no checking required and write anyway.
13
+ #
14
+ # Records are stored as JSON with the filename as the key and sequence
15
+ # number. The format of the file name is <key>~<seq>.json. As an example, a
16
+ # record stored with a key of 'first' and a sequence number of 3 (third time
17
+ # saved) would be 'first~3.json.
18
+ class Persister < Actor
19
+
20
+ attr_reader :dir
21
+ attr_reader :key_path
22
+ attr_reader :seq_path
23
+ attr_reader :data_path
24
+ attr_reader :historic
25
+
26
+ # Initializes the persister with options of:
27
+ # @param [Hash] options with keys of
28
+ # - :dir [String] directory to store the persisted records
29
+ # - :key_data [String] path to record data (default: nil (all))
30
+ # - :key_path [String] path to key for the record (default: 'key')
31
+ # - :seq_path [String] path to sequence for the record (default: 'seq')
32
+ # - :cache [Boolean] if true, cache records in memory
33
+ # - :historic [Boolean] if true, do not delete previous versions
34
+ def initialize(task, options)
35
+ super
36
+ @dir = options[:dir]
37
+ if @dir.nil?
38
+ @dir = File.join('db', task.full_name.gsub(':', '/'))
39
+ end
40
+ @key_path = options.fetch(:key_path, 'key')
41
+ @seq_path = options.fetch(:seq_path, 'seq')
42
+ @data_path = options.fetch(:data_path, nil) # nil means all contents
43
+ if options.fetch(:cache, true)
44
+ # key is record key, value is [seq, rec]
45
+ @cache = {}
46
+ else
47
+ @cache = nil
48
+ end
49
+ @historic = options.fetch(:historic, false)
50
+
51
+ if Dir.exist?(@dir)
52
+ unless @cache.nil?
53
+ Dir.glob(File.join('**', '*.json')).each do |path|
54
+ path = File.join(@dir, path)
55
+ if File.symlink?(path)
56
+ rec = load(path)
57
+ unless @cache.nil?
58
+ key, seq = key_seq_from_path(path)
59
+ @cache[key] = [seq, rec]
60
+ end
61
+ end
62
+ end
63
+ end
64
+ else
65
+ `mkdir -p #{@dir}`
66
+ end
67
+ end
68
+
69
+ def perform(op, box)
70
+ dest = box.contents[:dest]
71
+ result = nil
72
+ case op
73
+ when :insert, :create
74
+ result = insert(box)
75
+ when :get, :read
76
+ result = read(box)
77
+ when :update
78
+ result = update(box)
79
+ when :delete, :remove
80
+ result = delete(box)
81
+ when :query
82
+ result = query(box)
83
+ when :clear
84
+ result = clear(box)
85
+ else
86
+ raise OpError.new(task.full_name, op)
87
+ end
88
+ task.ship(dest, Box.new(result, box.tracker))
89
+ end
90
+
91
+ def insert(box)
92
+ key = box.get(@key_path)
93
+ raise KeyError.new(:insert) if key.nil?
94
+ box = box.set(@seq_path, 1)
95
+ rec = box.get(@data_path)
96
+ @cache[key] = [1, rec] unless @cache.nil?
97
+ save(rec, key, 1)
98
+ end
99
+
100
+ # Returns true if the actor is caching records.
101
+ def caching?()
102
+ !@cache.nil?
103
+ end
104
+
105
+ def read(box)
106
+ # Should be a Hash.
107
+ key = box.contents[:key]
108
+ raise KeyError(:read) if key.nil?
109
+ if @cache.nil?
110
+ linkpath = File.join(@dir, "#{key}.json")
111
+ rec = load(linkpath)
112
+ else
113
+ unless (seq_rec = @cache[key]).nil?
114
+ rec = seq_rec[1]
115
+ end
116
+ end
117
+ # If not found rec will be nil, that is okay.
118
+ rec
119
+ end
120
+
121
+ def update(box)
122
+ key = box.get(@key_path)
123
+ raise KeyError.new(:update) if key.nil?
124
+ seq = box.get(@seq_path)
125
+ if @cache.nil?
126
+ if (seq_rec = @cache[key]).nil?
127
+ raise NotFoundError.new(key)
128
+ end
129
+ seq = seq_rec[0] if seq.nil?
130
+ else
131
+ seq = 0
132
+ has_rec = false
133
+ Dir.glob(File.join(@dir, '**', "#{key}*.json")).each do |path|
134
+ if File.symlink?(path)
135
+ has_rec = true
136
+ next
137
+ end
138
+ _, s = key_seq_from_path(path)
139
+ seq = s if seq < s
140
+ end
141
+ end
142
+ raise NotFoundError.new(key) unless has_rec
143
+ raise SeqError.new(:update, key) if seq.nil? || 0 == seq
144
+
145
+ seq += 1
146
+ box = box.set(@seq_path, seq)
147
+ rec = box.get(@data_path)
148
+ @cache[key] = [seq, rec] unless @cache.nil?
149
+ rec = save(rec, key, seq)
150
+ delete_historic(key, seq) unless @historic
151
+ rec
152
+ end
153
+
154
+ def delete(box)
155
+ key = box.get(@key_path)
156
+ @cache.delete(key) unless @cache.nil?
157
+ linkpath = File.join(@dir, "#{key}.json")
158
+ File.delete(linkpath)
159
+ delete_historic(key, nil) unless @historic
160
+ nil
161
+ end
162
+
163
+ def query(box)
164
+ recs = {}
165
+ expr = box.get('expr')
166
+ if expr.nil?
167
+ if @cache.nil?
168
+ Dir.glob(File.join(@dir, '**/*.json')).each do |path|
169
+ recs[File.basename(path)[0..-6]] = load(path) if File.symlink?(path)
170
+ end
171
+ else
172
+ @cache.each do |key,seq_rec|
173
+ recs[key] = seq_rec[1]
174
+ end
175
+ end
176
+ elsif expr.is_a?(Proc)
177
+ if @cache.nil?
178
+ Dir.glob(File.join(@dir, '**/*.json')).each do |path|
179
+ next unless File.symlink?(path)
180
+ rec = load(path)
181
+ key, seq = key_seq_from_path(path)
182
+ recs[key] = rec if expr.call(rec, key, seq)
183
+ end
184
+ else
185
+ @cache.each do |key,seq_rec|
186
+ rec = seq_rec[1]
187
+ recs[key] = rec if expr.call(rec, key, seq_rec[0])
188
+ end
189
+ end
190
+ else
191
+ # TBD add support for string safe expressions in the future
192
+ raise Exception.new("expr can only be a Proc, not a #{expr.class}")
193
+ end
194
+ recs
195
+ end
196
+
197
+ def clear(box)
198
+ @cache = {} unless @cache.nil?
199
+ `rm -rf #{@dir}`
200
+ # remake the dir in preparation for future inserts
201
+ `mkdir -p #{@dir}`
202
+ nil
203
+ end
204
+
205
+ # internal use only
206
+ def save(rec, key, seq)
207
+ filename = "#{key}~#{seq}.json"
208
+ path = File.join(@dir, filename)
209
+ linkpath = File.join(@dir, "#{key}.json")
210
+ raise ExistsError.new(key, seq) if File.exist?(path)
211
+ Oj.to_file(path, rec, :mode => :object)
212
+ begin
213
+ File.delete(linkpath)
214
+ rescue Exception
215
+ # ignore
216
+ end
217
+ File.symlink(filename, linkpath)
218
+ rec
219
+ end
220
+
221
+ def load(path)
222
+ return nil unless File.exist?(path)
223
+ Oj.load_file(path, :mode => :object)
224
+ end
225
+
226
+ def delete_historic(key, seq)
227
+ Dir.glob(File.join(@dir, '**', "#{key}~*.json")).each do |path|
228
+ _, s = key_seq_from_path(path)
229
+ next if s == seq
230
+ File.delete(path)
231
+ end
232
+ end
233
+
234
+ def key_seq_from_path(path)
235
+ path = File.readlink(path) if File.symlink?(path)
236
+ base = File.basename(path)[0..-6] # strip off '.json'
237
+ a = base.split('~')
238
+ [a[0..-2].join('~'), a[-1].to_i]
239
+ end
240
+
241
+ class KeyError < Exception
242
+ def initialize(op)
243
+ super("No key found for #{op}")
244
+ end
245
+ end # KeyError
246
+
247
+ class SeqError < Exception
248
+ def initialize(op, key)
249
+ super("No sequence number found for #{op} of #{key}")
250
+ end
251
+ end # SeqError
252
+
253
+ class ExistsError < Exception
254
+ def initialize(key, seq)
255
+ super("#{key}:#{seq} already exists")
256
+ end
257
+ end # ExistsError
258
+
259
+ class NotFoundError < Exception
260
+ def initialize(key)
261
+ super("#{key} not found")
262
+ end
263
+ end # NotFoundError
264
+
265
+ end # Persister
266
+ end # Actors
267
+ end # OFlow
@@ -29,6 +29,14 @@ module OFlow
29
29
  end
30
30
  end # ConfigError
31
31
 
32
+ # An Exception indicating an invalid operation used in a call to receive() or
33
+ # perform().
34
+ class OpError < Exception
35
+ def initialize(name, op)
36
+ super("'#{op}' is not a valid operation for #{name}.")
37
+ end
38
+ end # OpError
39
+
32
40
  # An Exception raised when no destination is found.
33
41
  class LinkError < Exception
34
42
  def initialize(dest)
@@ -21,7 +21,10 @@ module OFlow
21
21
  yield(f) if block_given?
22
22
  f.resolve_all_links()
23
23
  # Wait to validate until at the top so up-links don't fail validation.
24
- f.validate() if Env == self
24
+ if Env == self
25
+ f.validate()
26
+ f.start()
27
+ end
25
28
  f
26
29
  end
27
30
 
@@ -33,6 +36,8 @@ module OFlow
33
36
  # @param block [Proc] block to yield to with the new Task instance
34
37
  # @return [Task] new Task
35
38
  def task(name, actor_class, options={}, &block)
39
+ has_state = options.has_key?(:state)
40
+ options[:state] = Task::STOPPED unless has_state
36
41
  t = Task.new(self, name, actor_class, options)
37
42
  @tasks[t.name] = t
38
43
  yield(t) if block_given?
@@ -55,7 +55,7 @@ module OFlow
55
55
  @actor = actor_class.new(self, options)
56
56
  raise Exception.new("#{actor} does not respond to the perform() method.") unless @actor.respond_to?(:perform)
57
57
 
58
- @state = RUNNING
58
+ @state = options.fetch(:state, RUNNING)
59
59
  return unless @actor.with_own_thread()
60
60
 
61
61
  @loop = Thread.start(self) do |me|
@@ -49,8 +49,13 @@ module OFlow
49
49
  if @starting
50
50
  @before << [op, box]
51
51
  else
52
- @actor.perform(op, box)
52
+ begin
53
+ @actor.perform(op, box)
54
+ rescue Exception => e
55
+ ship(:error, Box.new([e, full_name()]))
56
+ end
53
57
  end
58
+ nil
54
59
  end
55
60
 
56
61
  # Task API that adds entry to history.
@@ -1,5 +1,5 @@
1
1
 
2
2
  module OFlow
3
3
  # Current version of the module.
4
- VERSION = '0.4.0'
4
+ VERSION = '0.5.0'
5
5
  end
@@ -0,0 +1,410 @@
1
+ #!/usr/bin/env ruby
2
+ # encoding: UTF-8
3
+
4
+ [ File.dirname(__FILE__),
5
+ File.join(File.dirname(__FILE__), "../../lib"),
6
+ File.join(File.dirname(__FILE__), "..")
7
+ ].each { |path| $: << path unless $:.include?(path) }
8
+
9
+ require 'test/unit'
10
+ require 'oflow'
11
+ require 'oflow/test'
12
+
13
+ class PersisterTest < ::Test::Unit::TestCase
14
+
15
+ def test_persister_config
16
+ t = ::OFlow::Test::ActorWrap.new('test', ::OFlow::Actors::Persister, state: ::OFlow::Task::BLOCKED,
17
+ dir: 'db/something',
18
+ key_path: 'key',
19
+ cache: false,
20
+ data_path: 'data',
21
+ with_tracker: true,
22
+ with_seq_num: true,
23
+ historic: true,
24
+ seq_path: 'seq')
25
+ assert_equal('db/something', t.actor.dir, 'dir set from options')
26
+ assert_equal('key', t.actor.key_path, 'key_path set from options')
27
+ assert_equal('seq', t.actor.seq_path, 'seq_path set from options')
28
+ assert_equal('data', t.actor.data_path, 'data_path set from options')
29
+ assert_equal(false, t.actor.caching?, 'cache set from options')
30
+ assert_equal(true, t.actor.historic, 'historic set from options')
31
+ assert(Dir.exist?(t.actor.dir), 'dir exists')
32
+ `rm -r #{t.actor.dir}`
33
+
34
+ t = ::OFlow::Test::ActorWrap.new('persist', ::OFlow::Actors::Persister, state: ::OFlow::Task::BLOCKED)
35
+ assert_equal('db/test/persist', t.actor.dir, 'dir set from options')
36
+ assert_equal('key', t.actor.key_path, 'key_path set from options')
37
+ assert_equal('seq', t.actor.seq_path, 'seq_path set from options')
38
+ assert_equal(true, t.actor.caching?, 'cache set from options')
39
+ assert_equal(nil, t.actor.data_path, 'data_path set from options')
40
+ assert_equal(false, t.actor.historic, 'historic set from options')
41
+ assert(Dir.exist?(t.actor.dir), 'dir exists')
42
+ `rm -r #{t.actor.dir}`
43
+ end
44
+
45
+ def test_persister_historic
46
+ `rm -rf db/test/persist`
47
+ t = ::OFlow::Test::ActorWrap.new('persist', ::OFlow::Actors::Persister, state: ::OFlow::Task::BLOCKED,
48
+ historic: true)
49
+ # insert
50
+ t.receive(:insert, ::OFlow::Box.new({dest: :here, key: 'one', data: 0}))
51
+ assert_equal(1, t.history.size, 'one entry should be in the history')
52
+ assert_equal(:here, t.history[0].dest, 'should have shipped to :here')
53
+ assert_equal({:dest=>:here, :key=>'one', :data=>0, :seq=>1},
54
+ t.history[0].box.contents, 'should correct contents in shipment')
55
+
56
+ # read
57
+ t.reset()
58
+ t.receive(:read, ::OFlow::Box.new({dest: :read, key: 'one'}))
59
+ assert_equal(1, t.history.size, 'one entry should be in the history')
60
+ assert_equal(:read, t.history[0].dest, 'should have shipped to :read')
61
+ assert_equal({:dest=>:here, :key=>'one', :data=>0, :seq=>1},
62
+ t.history[0].box.contents, 'should correct contents in shipment')
63
+
64
+ # update
65
+ t.reset()
66
+ t.receive(:update, ::OFlow::Box.new({dest: :here, key: 'one', seq: 1, data: 1}))
67
+ # no seq so try to find max
68
+ t.receive(:update, ::OFlow::Box.new({dest: :here, key: 'one', data: 2}))
69
+ assert_equal(2, t.history.size, 'one entry for each update expected')
70
+ # check for 3 files in the db
71
+ files = Dir.glob(File.join(t.actor.dir, '**/*~[0123456789].json'))
72
+ assert_equal(3, files.size, 'should be 3 history historic files')
73
+ # make sure current object is last one saved
74
+ t.reset()
75
+ t.receive(:read, ::OFlow::Box.new({dest: :read, key: 'one'}))
76
+ assert_equal({:dest=>:here, :key=>'one', :data=>2, :seq=>3},
77
+ t.history[0].box.contents, 'should correct contents in shipment')
78
+
79
+ # delete
80
+ t.reset()
81
+ t.receive(:delete, ::OFlow::Box.new({dest: :deleted, key: 'one'}))
82
+ assert_equal(1, t.history.size, 'one entry should be in the history')
83
+ assert_equal(:deleted, t.history[0].dest, 'should have shipped to :read')
84
+ assert_equal(nil, t.history[0].box.contents, 'should correct contents in shipment')
85
+ # check for 3 files in the db
86
+ files = Dir.glob(File.join(t.actor.dir, '**/*~[0123456789].json'))
87
+ assert_equal(3, files.size, 'should be 3 history historic files')
88
+ # make sure object was deleted
89
+ t.reset()
90
+ t.receive(:read, ::OFlow::Box.new({dest: :read, key: 'one'}))
91
+ assert_equal(nil, t.history[0].box.contents, 'should correct contents in shipment')
92
+
93
+ # query
94
+ 10.times do |i|
95
+ t.receive(:insert, ::OFlow::Box.new({dest: :here, key: "rec-#{i}", data: i}))
96
+ end
97
+ t.reset()
98
+ t.receive(:query, ::OFlow::Box.new({dest: :query, expr: nil}))
99
+ assert_equal(10, t.history[0].box.contents.size, 'query with nil returns all')
100
+
101
+ t.reset()
102
+ t.receive(:query, ::OFlow::Box.new({dest: :query, expr: Proc.new{|rec,key,seq| 4 < rec[:data] }}))
103
+ assert_equal(5, t.history[0].box.contents.size, 'query return check')
104
+
105
+ # clear
106
+ t.reset()
107
+ t.receive(:clear, ::OFlow::Box.new({dest: :cleared}))
108
+ assert_equal(1, t.history.size, 'one entry should be in the history')
109
+ assert_equal(nil, t.history[0].box.contents, 'should correct contents in shipment')
110
+ files = Dir.glob(File.join(t.actor.dir, '**/*.json'))
111
+ assert_equal(0, files.size, 'should be no files')
112
+ end
113
+
114
+ def test_persister_historic_cached
115
+ `rm -rf db/test/persist`
116
+ t = ::OFlow::Test::ActorWrap.new('persist', ::OFlow::Actors::Persister, state: ::OFlow::Task::BLOCKED,
117
+ historic: true,
118
+ cache: true)
119
+ assert(t.actor.caching?, 'verify caching is on')
120
+
121
+ # insert
122
+ t.receive(:insert, ::OFlow::Box.new({dest: :here, key: 'one', data: 0}))
123
+ assert_equal(1, t.history.size, 'one entry should be in the history')
124
+ assert_equal(:here, t.history[0].dest, 'should have shipped to :here')
125
+ assert_equal({:dest=>:here, :key=>'one', :data=>0, :seq=>1},
126
+ t.history[0].box.contents, 'insert record contents')
127
+
128
+ # read
129
+ t.reset()
130
+ t.receive(:read, ::OFlow::Box.new({dest: :read, key: 'one'}))
131
+ assert_equal(1, t.history.size, 'one entry should be in the history')
132
+ assert_equal(:read, t.history[0].dest, 'should have shipped to :read')
133
+ assert_equal({:dest=>:here, :key=>'one', :data=>0, :seq=>1},
134
+ t.history[0].box.contents, 'should correct contents in shipment')
135
+
136
+ # update
137
+ t.reset()
138
+ t.receive(:update, ::OFlow::Box.new({dest: :here, key: 'one', seq: 1, data: 1}))
139
+ t.receive(:update, ::OFlow::Box.new({dest: :here, key: 'one', seq: 2, data: 2}))
140
+ assert_equal(2, t.history.size, 'one entry for each update expected')
141
+ # check for 3 files in the db
142
+ files = Dir.glob(File.join(t.actor.dir, '**/*~[0123456789].json'))
143
+ assert_equal(3, files.size, 'should be 3 history historic files')
144
+ # make sure current object is last one saved
145
+ t.reset()
146
+ t.receive(:read, ::OFlow::Box.new({dest: :read, key: 'one'}))
147
+ assert_equal({:dest=>:here, :key=>'one', :data=>2, :seq=>3},
148
+ t.history[0].box.contents, 'should correct contents in shipment')
149
+
150
+ # delete
151
+ t.reset()
152
+ t.receive(:delete, ::OFlow::Box.new({dest: :deleted, key: 'one'}))
153
+ assert_equal(1, t.history.size, 'one entry should be in the history')
154
+ assert_equal(:deleted, t.history[0].dest, 'should have shipped to :read')
155
+ assert_equal(nil, t.history[0].box.contents, 'should correct contents in shipment')
156
+ # check for 3 files in the db
157
+ files = Dir.glob(File.join(t.actor.dir, '**/*~[0123456789].json'))
158
+ assert_equal(3, files.size, 'should be 3 history historic files')
159
+ # make sure object was deleted
160
+ t.reset()
161
+ t.receive(:read, ::OFlow::Box.new({dest: :read, key: 'one'}))
162
+ assert_equal(nil, t.history[0].box.contents, 'should correct contents in shipment')
163
+
164
+ # query
165
+ 10.times do |i|
166
+ t.receive(:insert, ::OFlow::Box.new({dest: :here, key: "rec-#{i}", data: i}))
167
+ end
168
+ t.reset()
169
+ t.receive(:query, ::OFlow::Box.new({dest: :query, expr: nil}))
170
+ assert_equal(10, t.history[0].box.contents.size, 'query with nil returns all')
171
+
172
+ t.reset()
173
+ t.receive(:query, ::OFlow::Box.new({dest: :query, expr: Proc.new{|rec,key,seq| 4 < rec[:data] }}))
174
+ assert_equal(5, t.history[0].box.contents.size, 'query return check')
175
+
176
+ # clear
177
+ t.reset()
178
+ t.receive(:clear, ::OFlow::Box.new({dest: :cleared}))
179
+ assert_equal(1, t.history.size, 'one entry should be in the history')
180
+ assert_equal(nil, t.history[0].box.contents, 'should correct contents in shipment')
181
+ files = Dir.glob(File.join(t.actor.dir, '**/*.json'))
182
+ assert_equal(0, files.size, 'should be no files')
183
+ end
184
+
185
+ def test_persister_not_historic
186
+ `rm -rf db/test/persist`
187
+ t = ::OFlow::Test::ActorWrap.new('persist', ::OFlow::Actors::Persister, state: ::OFlow::Task::BLOCKED,
188
+ historic: false)
189
+ # insert
190
+ t.receive(:insert, ::OFlow::Box.new({dest: :here, key: 'one', data: 0}))
191
+ assert_equal(1, t.history.size, 'one entry should be in the history')
192
+ assert_equal(:here, t.history[0].dest, 'should have shipped to :here')
193
+ assert_equal({:dest=>:here, :key=>'one', :data=>0, :seq=>1},
194
+ t.history[0].box.contents, 'should correct contents in shipment')
195
+
196
+ # read
197
+ t.reset()
198
+ t.receive(:read, ::OFlow::Box.new({dest: :read, key: 'one'}))
199
+ assert_equal(1, t.history.size, 'one entry should be in the history')
200
+ assert_equal(:read, t.history[0].dest, 'should have shipped to :read')
201
+ assert_equal({:dest=>:here, :key=>'one', :data=>0, :seq=>1},
202
+ t.history[0].box.contents, 'should correct contents in shipment')
203
+
204
+ # update
205
+ t.reset()
206
+ t.receive(:update, ::OFlow::Box.new({dest: :here, key: 'one', seq: 1, data: 1}))
207
+ # no seq so try to find max
208
+ t.receive(:update, ::OFlow::Box.new({dest: :here, key: 'one', data: 2}))
209
+ assert_equal(2, t.history.size, 'one entry for each update expected')
210
+ # check for 3 files in the db
211
+ files = Dir.glob(File.join(t.actor.dir, '**/*~[0123456789].json'))
212
+ assert_equal(1, files.size, 'should be just one file')
213
+ # make sure current object is last one saved
214
+ t.reset()
215
+ t.receive(:read, ::OFlow::Box.new({dest: :read, key: 'one'}))
216
+ assert_equal({:dest=>:here, :key=>'one', :data=>2, :seq=>3},
217
+ t.history[0].box.contents, 'should correct contents in shipment')
218
+
219
+ # delete
220
+ t.reset()
221
+ t.receive(:delete, ::OFlow::Box.new({dest: :deleted, key: 'one'}))
222
+ assert_equal(1, t.history.size, 'one entry should be in the history')
223
+ assert_equal(:deleted, t.history[0].dest, 'should have shipped to :read')
224
+ assert_equal(nil, t.history[0].box.contents, 'should correct contents in shipment')
225
+ # check for 3 files in the db
226
+ files = Dir.glob(File.join(t.actor.dir, '**/*~[0123456789].json'))
227
+ assert_equal(0, files.size, 'should no historic files')
228
+ # make sure object was deleted
229
+ t.reset()
230
+ t.receive(:read, ::OFlow::Box.new({dest: :read, key: 'one'}))
231
+ assert_equal(nil, t.history[0].box.contents, 'should correct contents in shipment')
232
+
233
+ # query
234
+ 10.times do |i|
235
+ t.receive(:insert, ::OFlow::Box.new({dest: :here, key: "rec-#{i}", data: i}))
236
+ end
237
+ t.reset()
238
+ t.receive(:query, ::OFlow::Box.new({dest: :query, expr: nil}))
239
+ assert_equal(10, t.history[0].box.contents.size, 'query with nil returns all')
240
+
241
+ t.reset()
242
+ t.receive(:query, ::OFlow::Box.new({dest: :query, expr: Proc.new{|rec,key,seq| 4 < rec[:data] }}))
243
+ assert_equal(5, t.history[0].box.contents.size, 'query return check')
244
+
245
+ # clear
246
+ t.reset()
247
+ t.receive(:clear, ::OFlow::Box.new({dest: :cleared}))
248
+ assert_equal(1, t.history.size, 'one entry should be in the history')
249
+ assert_equal(nil, t.history[0].box.contents, 'should correct contents in shipment')
250
+ files = Dir.glob(File.join(t.actor.dir, '**/*.json'))
251
+ assert_equal(0, files.size, 'should be no files')
252
+ end
253
+
254
+ def test_persister_not_historic_cached
255
+ `rm -rf db/test/persist`
256
+ t = ::OFlow::Test::ActorWrap.new('persist', ::OFlow::Actors::Persister, state: ::OFlow::Task::BLOCKED,
257
+ historic: false,
258
+ cache: true)
259
+ # insert
260
+ t.receive(:insert, ::OFlow::Box.new({dest: :here, key: 'one', data: 0}))
261
+ assert_equal(1, t.history.size, 'one entry should be in the history')
262
+ assert_equal(:here, t.history[0].dest, 'should have shipped to :here')
263
+ assert_equal({:dest=>:here, :key=>'one', :data=>0, :seq=>1},
264
+ t.history[0].box.contents, 'should correct contents in shipment')
265
+
266
+ # read
267
+ t.reset()
268
+ t.receive(:read, ::OFlow::Box.new({dest: :read, key: 'one'}))
269
+ assert_equal(1, t.history.size, 'one entry should be in the history')
270
+ assert_equal(:read, t.history[0].dest, 'should have shipped to :read')
271
+ assert_equal({:dest=>:here, :key=>'one', :data=>0, :seq=>1},
272
+ t.history[0].box.contents, 'should correct contents in shipment')
273
+
274
+ # update
275
+ t.reset()
276
+ t.receive(:update, ::OFlow::Box.new({dest: :here, key: 'one', seq: 1, data: 1}))
277
+ # no seq so try to find max
278
+ t.receive(:update, ::OFlow::Box.new({dest: :here, key: 'one', data: 2}))
279
+ assert_equal(2, t.history.size, 'one entry for each update expected')
280
+ # check for 3 files in the db
281
+ files = Dir.glob(File.join(t.actor.dir, '**/*~[0123456789].json'))
282
+ assert_equal(1, files.size, 'should be just one file')
283
+ # make sure current object is last one saved
284
+ t.reset()
285
+ t.receive(:read, ::OFlow::Box.new({dest: :read, key: 'one'}))
286
+ assert_equal({:dest=>:here, :key=>'one', :data=>2, :seq=>3},
287
+ t.history[0].box.contents, 'should correct contents in shipment')
288
+
289
+ # delete
290
+ t.reset()
291
+ t.receive(:delete, ::OFlow::Box.new({dest: :deleted, key: 'one'}))
292
+ assert_equal(1, t.history.size, 'one entry should be in the history')
293
+ assert_equal(:deleted, t.history[0].dest, 'should have shipped to :read')
294
+ assert_equal(nil, t.history[0].box.contents, 'should correct contents in shipment')
295
+ # check for 3 files in the db
296
+ files = Dir.glob(File.join(t.actor.dir, '**/*~[0123456789].json'))
297
+ assert_equal(0, files.size, 'should no historic files')
298
+ # make sure object was deleted
299
+ t.reset()
300
+ t.receive(:read, ::OFlow::Box.new({dest: :read, key: 'one'}))
301
+ assert_equal(nil, t.history[0].box.contents, 'should correct contents in shipment')
302
+
303
+ # query
304
+ 10.times do |i|
305
+ t.receive(:insert, ::OFlow::Box.new({dest: :here, key: "rec-#{i}", data: i}))
306
+ end
307
+ t.reset()
308
+ t.receive(:query, ::OFlow::Box.new({dest: :query, expr: nil}))
309
+ assert_equal(10, t.history[0].box.contents.size, 'query with nil returns all')
310
+
311
+ t.reset()
312
+ t.receive(:query, ::OFlow::Box.new({dest: :query, expr: Proc.new{|rec,key,seq| 4 < rec[:data] }}))
313
+ assert_equal(5, t.history[0].box.contents.size, 'query return check')
314
+
315
+ # clear
316
+ t.reset()
317
+ t.receive(:clear, ::OFlow::Box.new({dest: :cleared}))
318
+ assert_equal(1, t.history.size, 'one entry should be in the history')
319
+ assert_equal(nil, t.history[0].box.contents, 'should correct contents in shipment')
320
+ files = Dir.glob(File.join(t.actor.dir, '**/*.json'))
321
+ assert_equal(0, files.size, 'should be no files')
322
+ end
323
+
324
+ def test_persister_data_path
325
+ `rm -rf db/test/persist`
326
+ t = ::OFlow::Test::ActorWrap.new('persist', ::OFlow::Actors::Persister, state: ::OFlow::Task::BLOCKED,
327
+ historic: false,
328
+ cache: true,
329
+ data_path: 'data')
330
+ # insert
331
+ t.receive(:insert, ::OFlow::Box.new({dest: :here, key: 'one', data: 0}))
332
+ assert_equal(1, t.history.size, 'one entry should be in the history')
333
+ assert_equal(:here, t.history[0].dest, 'should have shipped to :here')
334
+ assert_equal(0, t.history[0].box.contents, 'should correct contents in shipment')
335
+
336
+ # read
337
+ t.reset()
338
+ t.receive(:read, ::OFlow::Box.new({dest: :read, key: 'one'}))
339
+ assert_equal(1, t.history.size, 'one entry should be in the history')
340
+ assert_equal(:read, t.history[0].dest, 'should have shipped to :read')
341
+ assert_equal(0, t.history[0].box.contents, 'should correct contents in shipment')
342
+
343
+ # update
344
+ t.reset()
345
+ t.receive(:update, ::OFlow::Box.new({dest: :here, key: 'one', seq: 1, data: 1}))
346
+ # no seq so try to find max
347
+ t.receive(:update, ::OFlow::Box.new({dest: :here, key: 'one', data: 2}))
348
+ assert_equal(2, t.history.size, 'one entry for each update expected')
349
+ # check for 3 files in the db
350
+ files = Dir.glob(File.join(t.actor.dir, '**/*~[0123456789].json'))
351
+ assert_equal(1, files.size, 'should be just one file')
352
+ # make sure current object is last one saved
353
+ t.reset()
354
+ t.receive(:read, ::OFlow::Box.new({dest: :read, key: 'one'}))
355
+ assert_equal(2, t.history[0].box.contents, 'should correct contents in shipment')
356
+
357
+ # delete
358
+ t.reset()
359
+ t.receive(:delete, ::OFlow::Box.new({dest: :deleted, key: 'one'}))
360
+ assert_equal(1, t.history.size, 'one entry should be in the history')
361
+ assert_equal(:deleted, t.history[0].dest, 'should have shipped to :read')
362
+ assert_equal(nil, t.history[0].box.contents, 'should correct contents in shipment')
363
+ # check for 3 files in the db
364
+ files = Dir.glob(File.join(t.actor.dir, '**/*~[0123456789].json'))
365
+ assert_equal(0, files.size, 'should no historic files')
366
+ # make sure object was deleted
367
+ t.reset()
368
+ t.receive(:read, ::OFlow::Box.new({dest: :read, key: 'one'}))
369
+ assert_equal(nil, t.history[0].box.contents, 'should correct contents in shipment')
370
+
371
+ # query
372
+ 10.times do |i|
373
+ t.receive(:insert, ::OFlow::Box.new({dest: :here, key: "rec-#{i}", data: i}))
374
+ end
375
+ t.reset()
376
+ t.receive(:query, ::OFlow::Box.new({dest: :query, expr: nil}))
377
+ assert_equal(10, t.history[0].box.contents.size, 'query with nil returns all')
378
+
379
+ t.reset()
380
+ t.receive(:query, ::OFlow::Box.new({dest: :query, expr: Proc.new{|rec,key,seq| 4 < rec }}))
381
+ assert_equal(5, t.history[0].box.contents.size, 'query return check')
382
+
383
+ # clear
384
+ t.reset()
385
+ t.receive(:clear, ::OFlow::Box.new({dest: :cleared}))
386
+ assert_equal(1, t.history.size, 'one entry should be in the history')
387
+ assert_equal(nil, t.history[0].box.contents, 'should correct contents in shipment')
388
+ files = Dir.glob(File.join(t.actor.dir, '**/*.json'))
389
+ assert_equal(0, files.size, 'should be no files')
390
+ end
391
+
392
+ def test_persister_errors
393
+ `rm -rf db/test/persist`
394
+ t = ::OFlow::Test::ActorWrap.new('persist', ::OFlow::Actors::Persister, state: ::OFlow::Task::BLOCKED)
395
+
396
+ # insert with no key
397
+ t.receive(:insert, ::OFlow::Box.new({dest: :here, nokey: 'one', data: 0}))
398
+ action = t.history[0]
399
+ assert_equal(:error, action.dest, 'insert with no key destination')
400
+ assert_equal(::OFlow::Actors::Persister::KeyError, action.box.contents[0].class, 'insert with key error')
401
+
402
+ # update non-existant record
403
+ t.reset()
404
+ t.receive(:update, ::OFlow::Box.new({dest: :here, key: 'not-me', data: 0}))
405
+ action = t.history[0]
406
+ assert_equal(:error, action.dest, 'error destination')
407
+ assert_equal(::OFlow::Actors::Persister::NotFoundError, action.box.contents[0].class, 'insert with not found error')
408
+ end
409
+
410
+ end
@@ -22,8 +22,8 @@ require 'flow_nest_test'
22
22
  require 'flow_tracker_test'
23
23
 
24
24
  # Actor tests
25
- require 'actors/log_test'
26
- require 'actors/timer_test'
27
25
  require 'actors/balancer_test'
26
+ require 'actors/log_test'
28
27
  require 'actors/merger_test'
29
-
28
+ require 'actors/persister_test'
29
+ require 'actors/timer_test'
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: oflow
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.0
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Peter Ohler
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2014-02-08 00:00:00.000000000 Z
11
+ date: 2014-02-16 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: Operations Workflow in Ruby. This implements a workflow/process flow
14
14
  using multiple task nodes that each have their own queues and execution thread.
@@ -28,6 +28,7 @@ files:
28
28
  - lib/oflow/actors/ignore.rb
29
29
  - lib/oflow/actors/log.rb
30
30
  - lib/oflow/actors/merger.rb
31
+ - lib/oflow/actors/persister.rb
31
32
  - lib/oflow/actors/relay.rb
32
33
  - lib/oflow/actors/timer.rb
33
34
  - lib/oflow/box.rb
@@ -52,6 +53,7 @@ files:
52
53
  - test/actors/balancer_test.rb
53
54
  - test/actors/log_test.rb
54
55
  - test/actors/merger_test.rb
56
+ - test/actors/persister_test.rb
55
57
  - test/actors/timer_test.rb
56
58
  - test/actorwrap_test.rb
57
59
  - test/all_tests.rb