oflow 0.4.0 → 0.5.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 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