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 +4 -4
- data/README.md +4 -0
- data/lib/oflow/actors.rb +1 -0
- data/lib/oflow/actors/persister.rb +267 -0
- data/lib/oflow/errors.rb +8 -0
- data/lib/oflow/hastasks.rb +6 -1
- data/lib/oflow/task.rb +1 -1
- data/lib/oflow/test/actorwrap.rb +6 -1
- data/lib/oflow/version.rb +1 -1
- data/test/actors/persister_test.rb +410 -0
- data/test/all_tests.rb +3 -3
- metadata +4 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c60c057bc56ccdf1457715ab9e7c73af54614bb2
|
4
|
+
data.tar.gz: b20b4b16b01f875c7848bb9f442ca3e67213cda7
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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.
|
data/lib/oflow/actors.rb
CHANGED
@@ -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
|
data/lib/oflow/errors.rb
CHANGED
@@ -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)
|
data/lib/oflow/hastasks.rb
CHANGED
@@ -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
|
-
|
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?
|
data/lib/oflow/task.rb
CHANGED
@@ -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|
|
data/lib/oflow/test/actorwrap.rb
CHANGED
@@ -49,8 +49,13 @@ module OFlow
|
|
49
49
|
if @starting
|
50
50
|
@before << [op, box]
|
51
51
|
else
|
52
|
-
|
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.
|
data/lib/oflow/version.rb
CHANGED
@@ -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
|
data/test/all_tests.rb
CHANGED
@@ -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
|
+
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-
|
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
|