flor 0.17.0 → 0.18.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.
@@ -68,6 +68,9 @@ class Flor::Pro::Task < Flor::Procedure
68
68
  # clean_up assign: 'alan'
69
69
  # "clean up" assign: 'alan'
70
70
 
71
+ @node['message'] = message
72
+ # keep copy for Executor#return integrity enforcement
73
+
71
74
  nis = atts(nil)
72
75
  ta = att('by', 'for', 'assign')
73
76
  tn = att('with', 'task')
@@ -79,8 +82,7 @@ class Flor::Pro::Task < Flor::Procedure
79
82
 
80
83
  attl, attd = determine_atts
81
84
 
82
- @node['task'] =
83
- { 'tasker' => tasker, 'name' => taskname }
85
+ @node['task'] = { 'tasker' => tasker, 'name' => taskname }
84
86
 
85
87
  wrap(
86
88
  'point' => 'task',
@@ -96,6 +98,9 @@ class Flor::Pro::Task < Flor::Procedure
96
98
 
97
99
  close_node
98
100
 
101
+ @node['message'] = message
102
+ # keep copy for Executor#return integrity enforcement
103
+
99
104
  attl, attd = determine_atts
100
105
 
101
106
  wrap(
@@ -103,6 +108,7 @@ class Flor::Pro::Task < Flor::Procedure
103
108
  'exid' => exid, 'nid' => nid,
104
109
  'tags' => list_tags,
105
110
  'tasker' => att(nil),
111
+ 'taskname' => @node['task']['name'],
106
112
  'attl' => attl, 'attd' => attd,
107
113
  'payload' => determine_payload)
108
114
  end
@@ -178,6 +178,37 @@ class Flor::Pro::Trap < Flor::Procedure
178
178
  # twice.
179
179
  #
180
180
  #
181
+ # ## the payload: directive
182
+ #
183
+ # The `payload:` attribute tells the trap what payload to hand to the
184
+ # trap handler.
185
+ #
186
+ # When `payload: 'event'`, the handler is given a copy of the fields as
187
+ # seen in the event that triggered the trap.
188
+ #
189
+ # When `payload: 'trap'`, the handler is given a copy of the fields as
190
+ # they were when and where the trap was set.
191
+ #
192
+ # When `payload: { a: 'A' }`, the given payload is used (here
193
+ # `{ 'a' => 'A' }`).
194
+ #
195
+ #
196
+ # ## blocking trap
197
+ #
198
+ # One can use a trap without a function. It will block the execution branch
199
+ # until the expect event, signal, etc occurs
200
+ # ```
201
+ # sequence
202
+ # technical_writer 'prepare documentation'
203
+ # team_manager 'submit documentation to review team'
204
+ # trap signal: 'green' # go on after the 'green' signal...
205
+ # team_manager 'deploy documentation'
206
+ # ```
207
+ # Here the execution will block after the team manager submits the doc
208
+ # for review. When the 'green' signal comes, the flow resumes to 'deploy
209
+ # documentation'.
210
+ #
211
+ #
181
212
  # ## see also
182
213
  #
183
214
  # On and signal.
@@ -153,11 +153,15 @@ module Flor
153
153
 
154
154
  def return(message)
155
155
 
156
- [ { 'point' => 'receive',
157
- 'exid' => message['exid'],
158
- 'nid' => message['nid'],
159
- 'payload' => message['payload'],
160
- 'tasker' => message['tasker'] } ]
156
+ n = @execution['nodes'][message['nid']] || {}
157
+ m = n['message'] || {}
158
+ c = m['cause']
159
+
160
+ rm = message.dup
161
+ rm['point'] = 'receive'
162
+ rm['cause'] = c if c # preserve 'cause' for routing
163
+
164
+ [ rm ]
161
165
  end
162
166
 
163
167
  def schedule(message)
@@ -22,6 +22,12 @@ module Flor
22
22
  def shutdown
23
23
  end
24
24
 
25
+ # Used by flor when it looks up for a variable and finds nothing.
26
+ # The last step is to ask the ganger if it knows about a tasker under
27
+ # the given (domain and) name.
28
+ #
29
+ # If it returns true, flor knows there is a tasker under that name.
30
+ #
25
31
  def has_tasker?(exid, name)
26
32
 
27
33
  #return false if RESERVED_NAMES.include?(name)
@@ -34,6 +40,9 @@ module Flor
34
40
  @unit.loader.tasker(d, name))
35
41
  end
36
42
 
43
+ # Called by Flor::Scheduler. The ganger then has to hand the task
44
+ # (the message) to the proper tasker.
45
+ #
37
46
  def task(executor, message)
38
47
 
39
48
  domain = message['exid'].split('-', 2).first
@@ -50,12 +59,11 @@ module Flor
50
59
  ) unless tconf
51
60
 
52
61
  if tconf.is_a?(Array)
53
- tconf =
54
- tconf.find { |h| h['point'] == message['point'] } ||
55
- tconf.find { |h| h['point'] == nil }
56
- tconf ||=
57
- tconf.find { |h| h['point'] == 'cancel' } \
58
- if message['point'] == 'detask'
62
+
63
+ points = [ nil, message['point'] ]
64
+ points << 'detask' if points.include?('cancel')
65
+
66
+ tconf = tconf.find { |h| points.include?(h['point']) }
59
67
  end
60
68
 
61
69
  message['tconf'] = tconf unless tconf['include_tconf'] == false
@@ -74,6 +82,9 @@ module Flor
74
82
  # especially if it's a domain tasker
75
83
  end
76
84
 
85
+ # Called by the tasker implementations when they're done with a task
86
+ # and want to hand it back to flor. It might be a failure message.
87
+ #
77
88
  def return(message)
78
89
 
79
90
  @unit.return(message)
@@ -120,6 +131,10 @@ module Flor
120
131
  }.compact
121
132
  end
122
133
 
134
+ # By default, taskers don't see the flor variables in the execution.
135
+ # If 'include_vars' or 'exclude_vars' is present in the configuration
136
+ # of the tasker, some or all of the variables are passed.
137
+ #
123
138
  def gather_vars(executor, tconf, message)
124
139
 
125
140
  # try to return before a potentially costly call to executor.vars(nid)
@@ -42,9 +42,9 @@ module Flor
42
42
 
43
43
  exid = @values[:exid]; return nil unless exid
44
44
 
45
- @execution = nil if reload
45
+ @flor_model_cache_execution = nil if reload
46
46
 
47
- @execution ||= unit.executions[exid: exid]
47
+ @flor_model_cache_execution ||= unit.executions[exid: exid]
48
48
  end
49
49
 
50
50
  # Returns the node hash linked to this model
@@ -64,17 +64,20 @@ module Flor
64
64
  nod ? nod['payload'] : nil
65
65
  end
66
66
 
67
- def _data
68
-
69
- d = Flor::Storage.from_blob(content)
70
- d['id'] = id if d.is_a?(Hash)
67
+ def data(cache=true)
71
68
 
72
- d
69
+ cache ? (@flor_model_cache_data = _data) : _data
73
70
  end
74
71
 
75
- def data(cache=true)
72
+ def refresh
76
73
 
77
- cache ? (@data = _data) : _data
74
+ instance_variables
75
+ .each do |k|
76
+ instance_variable_set(k, nil) \
77
+ if k.to_s.start_with?('@flor_model_cache_')
78
+ end
79
+
80
+ super
78
81
  end
79
82
 
80
83
  def to_h
@@ -88,6 +91,34 @@ module Flor
88
91
  h
89
92
  end
90
93
  end
94
+
95
+ class << self
96
+
97
+ def from_h(h)
98
+
99
+ cols = columns
100
+
101
+ h
102
+ .inject({}) { |r, (k, v)|
103
+ k = k.to_sym
104
+ if k == :data
105
+ r[:content] = Flor.to_blob(v)
106
+ elsif cols.include?(k)
107
+ r[k] = v
108
+ end
109
+ r }
110
+ end
111
+ end
112
+
113
+ protected
114
+
115
+ def _data
116
+
117
+ d = Flor::Storage.from_blob(content)
118
+ d['id'] = id if d.is_a?(Hash)
119
+
120
+ d
121
+ end
91
122
  end
92
123
 
93
124
  MODELS = [ :executions, :timers, :traces, :traps, :pointers, :messages ]
@@ -88,9 +88,11 @@ module Flor
88
88
 
89
89
  c = c - 1
90
90
  data['count'] = c
91
- self[:status] = c > 0 ? 'active' : 'consumed'
91
+ self[:status] = s = (c > 0) ? 'active' : 'consumed'
92
92
 
93
- self.update(content: Flor::Storage.to_blob(@data), status: self[:status])
93
+ self.update(
94
+ content: Flor::Storage.to_blob(@flor_model_cache_data),
95
+ status: s)
94
96
 
95
97
  c < 1
96
98
  end
@@ -107,9 +109,15 @@ module Flor
107
109
  if sig = (message['point'] == 'signal' && message['name'])
108
110
  args << [ 'sig', sig ]
109
111
  end
110
- if dat['pl'] == 'event'
112
+
113
+ case pl = dat['pl']
114
+ when 'event'
111
115
  args << [ 'payload', msg['payload'] ]
112
- msg['payload'] = Flor.dup(message['payload']) # FIXME try without this line...
116
+ msg['payload'] = Flor.dup(message['payload'])
117
+ #when 'trap'
118
+ when Hash
119
+ msg['payload'] = Flor.dup(pl)
120
+ #else
113
121
  end
114
122
 
115
123
  { 'point' => 'trigger',
@@ -238,7 +238,7 @@ module Flor
238
238
  message
239
239
  else
240
240
  message
241
- .select { |k, _| %w[ exid nid payload tasker ].include?(k) }
241
+ .select { |k, _| %w[ exid nid payload tasker cause ].include?(k) }
242
242
  .merge!('point' => 'return')
243
243
  end
244
244
 
@@ -249,12 +249,21 @@ module Flor
249
249
 
250
250
  def cancel(exid, *as)
251
251
 
252
- queue(*prepare_message('cancel', [ exid, *as ]))
252
+ msg, opts = prepare_message('cancel', [ exid, *as ])
253
+ msg['nid'] ||= '0'
254
+
255
+ queue(msg, opts)
253
256
  end
254
257
 
255
258
  def kill(exid, *as)
256
259
 
257
- queue(*prepare_message('kill', [ exid, *as ]))
260
+ msg, opts = prepare_message('kill', [ exid, *as ])
261
+
262
+ msg['point'] = 'cancel'
263
+ msg['flavour'] = 'kill'
264
+ msg['nid'] ||= '0'
265
+
266
+ queue(msg, opts)
258
267
  end
259
268
 
260
269
  def signal(name, h={})
@@ -262,21 +271,38 @@ module Flor
262
271
  h[:payload] ||= {}
263
272
  h[:name] ||= name
264
273
 
265
- queue(*prepare_message('signal', [ h ]))
274
+ msg, opts = prepare_message('signal', [ h ])
275
+
276
+ fail ArgumentError.new('missing :name string key') \
277
+ unless msg['name'].is_a?(String)
278
+
279
+ queue(msg, opts)
266
280
  end
267
281
 
268
282
  def re_apply(exid, *as)
269
283
 
270
- queue(*prepare_message('cancel', [ exid, *as, { re_apply: true } ]))
284
+ msg, opts = prepare_message('cancel', [ exid, *as ])
285
+
286
+ msg['on_receive_last'] = prepare_re_apply_messages(msg, opts)
287
+
288
+ queue(msg, opts)
271
289
  end
272
290
  alias reapply re_apply
273
291
 
274
292
  def add_branches(exid, *as)
275
293
 
276
- m = prepare_message('add-branches', [ exid, *as ]).first
294
+ msg, opts = prepare_message('add-branches', [ exid, *as ])
295
+
296
+ msg['point'] = 'add'
297
+ msg['trees'] = prepare_trees(opts)
277
298
 
278
- exe = @storage.executions[exid: m['exid']]
279
- pnid = m['nid']
299
+ msg['tnid'] = tnid =
300
+ opts.delete(:tnid) || msg.delete('nid')
301
+ msg['nid'] =
302
+ msg.delete('nid') || opts.delete(:pnid) || Flor.parent_nid(tnid)
303
+
304
+ exe = @storage.executions[exid: msg['exid']]
305
+ pnid = msg['nid']
280
306
  ptree = exe.lookup_tree(pnid)
281
307
 
282
308
  fail ArgumentError.new(
@@ -286,7 +312,7 @@ module Flor
286
312
  # not likely to happen, since leaves reply immediately
287
313
 
288
314
  size = ptree[1].size
289
- tnid = (m['tnid'] ||= Flor.make_child_nid(pnid, size))
315
+ tnid = (msg['tnid'] ||= Flor.make_child_nid(pnid, size))
290
316
 
291
317
  cid = Flor.child_id(tnid)
292
318
 
@@ -306,19 +332,23 @@ module Flor
306
332
  "node #{pnid} has #{size} branch#{size == 1 ? '' : 'es'}"
307
333
  ) if cid > size
308
334
 
309
- queue(m)
335
+ queue(msg, opts)
310
336
  end
311
337
  alias add_branch add_branches
312
338
 
313
339
  def add_iterations(exid, *as)
314
340
 
315
- m = prepare_message('add-iterations', [ exid, *as ]).first
341
+ msg, opts = prepare_message('add-iterations', [ exid, *as ])
342
+
343
+ msg['point'] = 'add'
344
+ msg['elements'] = prepare_elements(opts)
345
+ msg['nid'] = msg.delete('nid') || opts.delete(:pnid)
316
346
 
317
- exe = @storage.executions[exid: m['exid']]
318
- nid = m['nid']
347
+ exe = @storage.executions[exid: msg['exid']]
348
+ nid = msg['nid']
319
349
 
320
350
  fail ArgumentError.new(
321
- "cannot add iteration to missing execution #{m['exid'].inspect}"
351
+ "cannot add iteration to missing execution #{msg['exid'].inspect}"
322
352
  ) unless exe
323
353
 
324
354
  fail ArgumentError.new(
@@ -329,7 +359,7 @@ module Flor
329
359
  "cannot add iteration to missing node #{nid.inspect}"
330
360
  ) unless exe.lookup_tree(nid)
331
361
 
332
- queue(m)
362
+ queue(msg, opts)
333
363
  end
334
364
  alias add_iteration add_iterations
335
365
 
@@ -387,6 +417,136 @@ module Flor
387
417
  ex ? ex.execution : nil
388
418
  end
389
419
 
420
+ DUMP_KEYS = %w[ timestamp executions timers traps pointers ]
421
+
422
+ # Dumps all or some of the executions to a JSON string.
423
+ # See Scheduler#load for importing.
424
+ #
425
+ # unit.dump -> string # returns a JSON string of all executions
426
+ # unit.dump(io) -> io # dumps the JSON to the given IO instance
427
+ #
428
+ # unit.dump(exid: i) # dumps only the given execution
429
+ # unit.dump(exids: [ i0, i1 ]) # dumps only the givens executions
430
+ # unit.dump(domain: d) # dumps exes from domains,
431
+ # unit.dump(domains: [ d0, d1 ]) # and their subdomains
432
+ # unit.dump(sdomain: d) # dumps strictly from given domains,
433
+ # unit.dump(sdomains: [ d0, d1 ]) # doesn't look at subdomains
434
+ #
435
+ # unit.dump() { |h| ... } # modify the has right before it's turned to JSON
436
+ #
437
+ def dump(io=nil, opts=nil, &block)
438
+
439
+ io, opts = nil, io if io.is_a?(Hash)
440
+ opts ||= {}
441
+
442
+ o = lambda { |k| v = opts[k] || opts["#{k}s".to_sym]; v ? Array(v) : nil }
443
+ #
444
+ exis = o[:exid]
445
+ doms = o[:domain]
446
+ sdms = o[:strict_domain] || o[:sdomain]
447
+ #
448
+ filter = lambda { |q|
449
+ q = q.where(
450
+ exid: exis) if exis
451
+ q = q.where {
452
+ Sequel.|(*doms
453
+ .inject([]) { |a, d|
454
+ a.concat([
455
+ { domain: d },
456
+ Sequel.like(:domain, d + '.%') ]) }) } if doms
457
+ q = q.where(
458
+ domain: sdms) if sdms
459
+ q }
460
+
461
+ exs, tms, tps, pts =
462
+ storage.db.transaction {
463
+ [ filter[executions].collect(&:to_h),
464
+ filter[timers].collect(&:to_h),
465
+ filter[traps].collect(&:to_h),
466
+ filter[pointers].collect(&:to_h) ] }
467
+
468
+ o = io ? io : StringIO.new
469
+
470
+ h = {
471
+ timestamp: Flor.tstamp,
472
+ executions: exs,
473
+ timers: tms,
474
+ traps: tps,
475
+ pointers: pts }
476
+
477
+ block.call(h) if block
478
+
479
+ JSON.dump(h, o)
480
+
481
+ io ? io : o.string
482
+ end
483
+
484
+ # Read a previous JSON dump and loads it into the storage.
485
+ # Can be useful when testing, dumping once and reloading multiple times
486
+ # to test variants.
487
+ #
488
+ # load(string) -> h # load all executions from given JSON string
489
+ # # returns object inserted stat hash
490
+ # load(io) # load all executions from the given IO
491
+ # load(io, close: true) # load from the given IO and close it after read
492
+ #
493
+ # load(x, exid: i) # load only given executions,
494
+ # load(x, exids: [ i0, i1 ]) # ignore the rest of the data in the source
495
+ # load(x, domain: d) # load only exes from given domains,
496
+ # load(x, domains: [ d0, d1 ]) # and their subdomains
497
+ # load(x, sdomain: d) # load only exes from strict domains,
498
+ # load(x, sdomains: [ d0, d1 ]) # ignores exes in their subdomains
499
+ #
500
+ def load(string_or_io, opts={}, &block)
501
+
502
+ s = string_or_io
503
+ s = s.read if s.respond_to?(:read)
504
+ string_or_io.close if string_or_io.respond_to?(:close) && opts[:close]
505
+ h = JSON.load(s)
506
+
507
+ mks = DUMP_KEYS - h.keys
508
+ fail Flor::FlorError.new("missing keys #{mks.inspect}") if mks.any?
509
+
510
+ o = lambda { |k| v = opts[k] || opts["#{k}s".to_sym]; v ? Array(v) : nil }
511
+ #
512
+ exis = o[:exid]
513
+ doms = o[:domain]
514
+ sdms = o[:strict_domain] || o[:sdomain]
515
+ #
516
+ doms = doms.collect { |d| /\A#{d}(\.#{Flor::NAME_REX})*\z/ } if doms
517
+
518
+ counts = { executions: 0, timers: 0, traps: 0, pointers: 0, total: 0 }
519
+
520
+ storage.db.transaction do
521
+
522
+ (DUMP_KEYS - %w[ timestamp ]).each do |k|
523
+
524
+ y = k.to_sym
525
+ cla = storage.send(k)
526
+ cols = cla.columns
527
+
528
+ rows = h[k]
529
+ .inject([]) { |a, hh|
530
+
531
+ next a if exis && ! exis.include?(hh['exid'])
532
+ next a if doms && ! doms.find { |d| d.match(hh['domain']) }
533
+ next a if sdms && ! sdms.include?(hh['domain'])
534
+
535
+ counts[y] += 1
536
+ counts[:total] += 1
537
+
538
+ vals = cla.from_h(hh)
539
+ a << cols.collect { |c| vals[c] } }
540
+
541
+ cla.import(cols, rows) if rows.any?
542
+ end
543
+
544
+ block.call(h) if block
545
+ end
546
+
547
+ counts
548
+ end
549
+
390
550
  protected
391
551
 
392
552
  def tick
@@ -412,8 +572,11 @@ module Flor
412
572
  notify(nil, make_idle_message)
413
573
  end
414
574
 
415
- sleep [ @heart_rate - (Time.now - t0), 0 ].max #\
416
- #unless should_wake_up?
575
+ if @idle_count < 1
576
+ sleep 0.001
577
+ else
578
+ sleep([ @heart_rate - (Time.now - t0), 0.001 ].max)
579
+ end
417
580
 
418
581
  rescue Exception => ex
419
582
 
@@ -422,8 +585,8 @@ module Flor
422
585
 
423
586
  def prepare_message(point, args)
424
587
 
425
- h =
426
- args.inject({}) { |hh, a|
588
+ h = args
589
+ .inject({}) { |hh, a|
427
590
  if a.is_a?(Hash) then a.each { |k, v| hh[k.to_s] = v }
428
591
  elsif ! hh.has_key?('exid') then hh['exid'] = a
429
592
  elsif ! hh.has_key?('nid') then hh['nid'] = a
@@ -441,37 +604,8 @@ module Flor
441
604
  end
442
605
  end
443
606
 
444
- if point == 'kill'
445
-
446
- msg['point'] = 'cancel'
447
- msg['flavour'] = 'kill'
448
-
449
- elsif point == 'add-branches'
450
-
451
- msg['point'] = 'add'
452
- msg['trees'] = prepare_trees(opts)
453
-
454
- msg['tnid'] = tnid =
455
- opts.delete(:tnid) || msg.delete('nid')
456
- msg['nid'] =
457
- msg.delete('nid') || opts.delete(:pnid) || Flor.parent_nid(tnid)
458
-
459
- elsif point == 'add-iterations'
460
-
461
- msg['point'] = 'add'
462
- msg['elements'] = prepare_elements(opts)
463
- msg['nid'] = msg.delete('nid') || opts.delete(:pnid)
464
- end
465
-
466
- if opts[:re_apply]
467
-
468
- msg['on_receive_last'] = prepare_re_apply_messages(msg, opts)
469
- end
470
-
471
607
  fail ArgumentError.new('missing :exid key') \
472
608
  unless msg['exid'].is_a?(String)
473
- fail ArgumentError.new('missing :name string key') \
474
- if point == 'signal' && ! msg['name'].is_a?(String)
475
609
 
476
610
  [ msg, opts ]
477
611
  end
@@ -503,8 +637,9 @@ module Flor
503
637
 
504
638
  def prepare_re_apply_messages(msg, opts)
505
639
 
506
- fail ArgumentError.new("missing 'payload' to re_apply") \
507
- unless msg['payload']
640
+ pl = msg['payload']
641
+
642
+ fail ArgumentError.new("missing 'payload' to re_apply") unless pl
508
643
 
509
644
  t = Flor.parse(opts[:tree], Flor.caller_fname, {})
510
645
 
@@ -512,7 +647,7 @@ module Flor
512
647
  'exid' => msg['exid'], 'nid' => msg['nid'],
513
648
  'from' => 'parent',
514
649
  'tree' => t,
515
- 'payload' => msg['payload'] } ]
650
+ 'payload' => pl } ]
516
651
  end
517
652
 
518
653
  def make_idle_message