openwferu 0.9.11 → 0.9.12

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.
Files changed (50) hide show
  1. data/README.txt +0 -3
  2. data/examples/engine_template.rb +10 -4
  3. data/lib/openwfe/engine/engine.rb +336 -63
  4. data/lib/openwfe/engine/file_persisted_engine.rb +9 -1
  5. data/lib/openwfe/expool/errorjournal.rb +379 -0
  6. data/lib/openwfe/expool/expressionpool.rb +84 -55
  7. data/lib/openwfe/expool/expstorage.rb +54 -18
  8. data/lib/openwfe/expool/journal.rb +31 -22
  9. data/lib/openwfe/expool/yamlexpstorage.rb +57 -16
  10. data/lib/openwfe/expressions/fe_sequence.rb +1 -1
  11. data/lib/openwfe/expressions/flowexpression.rb +13 -1
  12. data/lib/openwfe/expressions/{fe_raw.rb → raw.rb} +5 -2
  13. data/lib/openwfe/expressions/raw_prog.rb +1 -1
  14. data/lib/openwfe/expressions/raw_xml.rb +1 -1
  15. data/lib/openwfe/expressions/time.rb +2 -0
  16. data/lib/openwfe/flowexpressionid.rb +21 -6
  17. data/lib/openwfe/omixins.rb +37 -14
  18. data/lib/openwfe/participants/atomparticipants.rb +6 -5
  19. data/lib/openwfe/participants/participantmap.rb +2 -0
  20. data/lib/openwfe/rest/controlclient.rb +1 -0
  21. data/lib/openwfe/rudefinitions.rb +5 -1
  22. data/lib/openwfe/storage/yamlfilestorage.rb +7 -3
  23. data/lib/openwfe/util/otime.rb +1 -1
  24. data/lib/openwfe/util/safe.rb +14 -0
  25. data/lib/openwfe/util/scheduler.rb +8 -5
  26. data/lib/openwfe/util/workqueue.rb +9 -2
  27. data/lib/openwfe/utils.rb +18 -0
  28. data/lib/openwfe/version.rb +1 -1
  29. data/test/atom_test.rb +27 -26
  30. data/test/fei_test.rb +3 -3
  31. data/test/file_persistence_test.rb +19 -2
  32. data/test/ft_0c_testname.rb +6 -3
  33. data/test/ft_26_load.rb +14 -7
  34. data/test/ft_26b_load.rb +87 -0
  35. data/test/ft_26c_load.rb +71 -0
  36. data/test/ft_27_getflowpos.rb +22 -3
  37. data/test/ft_34_cancelwfid.rb +3 -2
  38. data/test/ft_42_environments.rb +3 -1
  39. data/test/ft_58_ejournal.rb +119 -0
  40. data/test/ft_59_ps.rb +118 -0
  41. data/test/ft_60_ecancel.rb +87 -0
  42. data/test/ft_tests.rb +4 -0
  43. data/test/hparticipant_test.rb +1 -1
  44. data/test/orest_test.rb +27 -4
  45. data/test/param_test.rb +5 -1
  46. data/test/participant_test.rb +39 -0
  47. data/test/rake_qtest.rb +3 -5
  48. data/test/rest_test.rb +2 -2
  49. data/test/scheduler_test.rb +10 -15
  50. metadata +10 -3
data/README.txt CHANGED
@@ -3,9 +3,6 @@
3
3
  == Prerequisites
4
4
  Ruby 1.8.5 or later
5
5
 
6
- == Supported platforms
7
- TODO
8
-
9
6
  == Installation
10
7
  Installation can be handled by Ruby gems. This will pull in any libraries
11
8
  you will need to install
@@ -8,9 +8,6 @@ require 'rubygems'
8
8
 
9
9
  require 'openwfe/engine/engine'
10
10
  require 'openwfe/engine/file_persisted_engine'
11
- require 'openwfe/expool/history'
12
- require 'openwfe/expool/journal'
13
- require 'openwfe/listeners/listeners'
14
11
  require 'openwfe/participants/participants'
15
12
 
16
13
 
@@ -62,9 +59,11 @@ engine = OpenWFE::CachedFilePersistedEngine.new
62
59
 
63
60
  # -- process history
64
61
 
62
+ #require 'openwfe/expool/history'
63
+
65
64
  #engine.init_service("history", InMemoryHistory)
66
65
  #
67
- # keeps all process history in an arry in memory
66
+ # keeps all process history in an array in memory
68
67
  # use only for test purposes !
69
68
 
70
69
  #engine.init_service("history", FileHistory)
@@ -74,12 +73,17 @@ engine = OpenWFE::CachedFilePersistedEngine.new
74
73
 
75
74
  # -- process journaling
76
75
 
76
+ #require 'openwfe/expool/journal'
77
77
  #engine.init_service("journal", Journal)
78
78
  #
79
79
  # activates 'journaling',
80
80
  #
81
81
  # see http://openwferu.rubyforge.org/journal.html
82
82
  #
83
+ # Journaling has a cost in terms of performace.
84
+ # Journaling should be used only in case you might want to migrate
85
+ # [segments of] running processes.
86
+ #
83
87
  #engine.application_context[:keep_journals] = true
84
88
  #
85
89
  # if set to true, the journal of terminated processes will be kept
@@ -93,6 +97,8 @@ engine = OpenWFE::CachedFilePersistedEngine.new
93
97
  # participants or LaunchItem requesting the launch of a particular flow)
94
98
  #
95
99
 
100
+ #require 'openwfe/listeners/listeners'
101
+
96
102
  #sl = OpenWFE::SocketListener.new(
97
103
  # "socket_listener", @engine.application_context, 7008)
98
104
  #engine.add_workitem_listener(sl)
@@ -43,15 +43,18 @@
43
43
  require 'logger'
44
44
  require 'fileutils'
45
45
 
46
- require 'openwfe/workitem'
46
+ require 'openwfe/omixins'
47
47
  require 'openwfe/rudefinitions'
48
48
  require 'openwfe/service'
49
+ require 'openwfe/workitem'
49
50
  require 'openwfe/util/irb'
50
51
  require 'openwfe/util/scheduler'
51
52
  require 'openwfe/util/schedulers'
52
53
  require 'openwfe/expool/wfidgen'
53
54
  require 'openwfe/expool/expressionpool'
54
55
  require 'openwfe/expool/expstorage'
56
+ require 'openwfe/expool/errorjournal'
57
+ require 'openwfe/expressions/environment'
55
58
  require 'openwfe/expressions/expressionmap'
56
59
  require 'openwfe/participants/participantmap'
57
60
 
@@ -64,6 +67,7 @@ module OpenWFE
64
67
  #
65
68
  class Engine < Service
66
69
  include OwfeServiceLocator
70
+ include FeiMixin
67
71
 
68
72
  #
69
73
  # Builds an OpenWFEru engine.
@@ -82,8 +86,10 @@ module OpenWFE
82
86
  $OWFE_LOG = application_context[:logger]
83
87
 
84
88
  unless $OWFE_LOG
89
+ #puts "Creating logs in " + FileUtils.pwd
85
90
  FileUtils.mkdir("logs") unless File.exist?("logs")
86
- $OWFE_LOG = Logger.new("logs/openwferu.log")
91
+ $OWFE_LOG = Logger.new("logs/openwferu.log", 10, 1024000)
92
+ $OWFE_LOG.level = Logger::INFO
87
93
  end
88
94
 
89
95
  # build order matters.
@@ -92,14 +98,40 @@ module OpenWFE
92
98
  # pool and thus needs to be instantiated after it.
93
99
 
94
100
  build_scheduler()
101
+ #
102
+ # for delayed or repetitive executions (it's the engine's clock)
103
+ # see http://openwferu.rubyforge.org/scheduler.html
95
104
 
96
105
  build_expression_map()
106
+ #
107
+ # mapping expression names ('sequence', 'if', 'concurrence',
108
+ # 'when'...) to their implementations (SequenceExpression,
109
+ # IfExpression, ConcurrenceExpression, ...)
97
110
 
98
111
  build_wfid_generator()
112
+ #
113
+ # the workflow instance (process instance) id generator
114
+ # making sure each process instance has a unique identifier
115
+
99
116
  build_expression_pool()
117
+ #
118
+ # the core (hairy ball) of the engine
119
+
100
120
  build_expression_storage()
121
+ #
122
+ # the engine persistence (persisting the expression instances
123
+ # that make up process instances)
101
124
 
102
125
  build_participant_map()
126
+ #
127
+ # building the services that maps participant names to
128
+ # participant implementations / instances.
129
+
130
+ build_error_journal()
131
+ #
132
+ # builds the error journal (keeping track of failures
133
+ # in business process executions, and an opportunity to
134
+ # fix and replay)
103
135
 
104
136
  linfo { "new() --- engine started --- #{self.object_id}" }
105
137
  end
@@ -136,6 +168,10 @@ module OpenWFE
136
168
  # either a String containing the URL of the process definition
137
169
  # to launch (with an empty LaunchItem created on the fly).
138
170
  #
171
+ # The launch object can also be a String containing the XML process
172
+ # definition or directly a class extending OpenWFE::ProcessDefinition
173
+ # (Ruby process definition).
174
+ #
139
175
  # Returns the FlowExpressionId instance of the expression at the
140
176
  # root of the newly launched process.
141
177
  #
@@ -163,7 +199,12 @@ module OpenWFE
163
199
  li
164
200
  end
165
201
 
166
- get_expression_pool.launch launchitem
202
+ fei = get_expression_pool.launch launchitem
203
+
204
+ fei.dup
205
+ #
206
+ # so that users of this launch() method can play with their
207
+ # fei without breaking things
167
208
  end
168
209
 
169
210
  #
@@ -292,6 +333,21 @@ module OpenWFE
292
333
  get_scheduler.join
293
334
  end
294
335
 
336
+ #
337
+ # Calling this method makes the control flow block until the
338
+ # workflow engine is inactive.
339
+ #
340
+ # TODO : implement idle_for
341
+ #
342
+ def join_until_idle ()
343
+
344
+ storage = get_expression_storage
345
+
346
+ while storage.size > 1
347
+ sleep 1
348
+ end
349
+ end
350
+
295
351
  #
296
352
  # Enabling the console means that hitting CTRL-C on the window /
297
353
  # term / dos box / whatever does run the OpenWFEru engine will
@@ -346,56 +402,139 @@ module OpenWFE
346
402
  end
347
403
 
348
404
  #
405
+ # Waits for a given process instance to terminate.
406
+ # The method only exits when the flow terminates, but beware : if
407
+ # the process already terminated, the method will never exit.
408
+ #
409
+ # The parameter can be a FlowExpressionId instance, for example the
410
+ # one given back by a launch(), or directly a workflow instance id
411
+ # (String).
412
+ #
413
+ # This method is mainly used in utests.
414
+ #
415
+ def wait_for (fei_or_wfid)
416
+
417
+ wfid = if fei_or_wfid.kind_of?(FlowExpressionId)
418
+ fei_or_wfid.workflow_instance_id
419
+ else
420
+ fei_or_wfid
421
+ end
422
+
423
+ #Thread.pass
424
+ # #
425
+ # # let the flow 'stabilize' or progress before enquiring
426
+ #fexp = get_expression_pool.fetch_expression(fei)
427
+ #return unless fexp
428
+ #
429
+ # doesn't work well
430
+
431
+ t = Thread.new { Thread.stop }
432
+
433
+ get_expression_pool.add_observer(:terminate) do |channel, fe, wi|
434
+ t.wakeup if fe.fei.workflow_instance_id == wfid
435
+ end
436
+
437
+ ldebug { "wait_for() #{wfid}" }
438
+
439
+ t.join
440
+ end
441
+
442
+ #
443
+ # Returns a hash of ProcessStatus instances. The keys of the hash
444
+ # are workflow instance ids.
445
+ #
446
+ # A ProcessStatus is a description of the state of a process instance
447
+ # it enumerates the expressions where the process is currently
448
+ # located (waiting certainly) and the errors the process currently
449
+ # has (hopefully none).
450
+ #
451
+ def get_process_status (wfid=nil)
452
+
453
+ wfid = to_wfid(wfid) if wfid
454
+
455
+ result = {}
456
+
457
+ get_expression_storage.real_each(wfid) do |fei, fexp|
458
+ next if fexp.kind_of?(Environment)
459
+ next unless fexp.apply_time
460
+ (result[fei.parent_wfid] ||= ProcessStatus.new) << fexp
461
+ end
462
+
463
+ result.values.each do |ps|
464
+ get_error_journal.get_error_log(ps.wfid).each do |error|
465
+ ps << error
466
+ end
467
+ end
468
+
469
+ class << result
470
+ def to_s
471
+ pretty_print_process_status(self)
472
+ end
473
+ end
474
+
475
+ result
476
+ end
477
+
478
+ #--
349
479
  # METHODS FROM THE EXPRESSION POOL
350
480
  #
351
481
  # These methods are 'proxy' to method found in the expression pool.
352
482
  # They are made available here for a simpler model.
353
- #
483
+ #++
354
484
 
355
485
  #
356
486
  # Returns the list of applied expressions belonging to a given
357
487
  # workflow instance.
358
488
  # May be used to determine where a process instance currently is.
359
489
  #
360
- def get_flow_position (workflow_instance_id)
361
- get_expression_pool.get_flow_position(workflow_instance_id)
490
+ # This method returns all the expressions (the stack) a process
491
+ # went through to reach its current state.
492
+ #
493
+ def get_process_stack (workflow_instance_id)
494
+
495
+ get_expression_pool.get_process_stack(workflow_instance_id)
362
496
  end
363
- alias :get_process_position :get_flow_position
497
+ alias :get_flow_stack :get_process_stack
364
498
 
365
499
  #
366
- # Lists all workflows (processes) currently in the expool (in
500
+ # Lists all workflow (process) instances currently in the expool (in
367
501
  # the engine).
368
502
  # This method will return a list of "process-definition" expressions
369
503
  # (root of flows).
370
504
  #
371
- # If consider_subprocesses is set to true, "process-definition"
372
- # expressions of subprocesses will be returned as well.
373
- #
374
505
  # "wfid_prefix" allows your to query for specific workflow instance
375
506
  # id prefixes.
376
507
  #
377
- def list_workflows (consider_subprocesses=false, wfid_prefix=nil)
508
+ # If consider_subprocesses is set to true, "process-definition"
509
+ # expressions of subprocesses will be returned as well.
510
+ #
511
+ def list_processes (consider_subprocesses=false, wfid_prefix=nil)
378
512
 
379
- get_expression_pool.list_workflows(
513
+ get_expression_pool.list_processes(
380
514
  consider_subprocesses, wfid_prefix)
381
515
  end
382
- alias :list_processes :list_workflows
516
+ alias :list_workflows :list_processes
383
517
 
384
518
  #
385
519
  # Given any expression of a process, cancels the complete process
386
520
  # instance.
387
521
  #
388
- def cancel_flow (exp_or_wfid)
389
- get_expression_pool.cancel_flow(exp_or_wfid)
522
+ def cancel_process (exp_or_wfid)
523
+
524
+ get_expression_pool.cancel_process(exp_or_wfid)
390
525
  end
391
- alias :cancel_process :cancel_flow
526
+ alias :cancel_flow :cancel_process
392
527
 
393
528
  #
394
529
  # Cancels the given expression (and its children if any)
395
530
  # (warning : advanced method)
396
531
  #
532
+ # Cancelling the root expression of a process is equivalent to
533
+ # cancelling the process.
534
+ #
397
535
  def cancel_expression (exp_or_fei)
398
- get_expression_pool.cancel(exp_or_fei)
536
+
537
+ get_expression_pool.cancel_expression(exp_or_fei)
399
538
  end
400
539
 
401
540
  #
@@ -403,53 +542,16 @@ module OpenWFE
403
542
  # (warning : advanced method)
404
543
  #
405
544
  def forget_expression (exp_or_fei)
406
- get_expression_pool.forget(exp_or_fei)
407
- end
408
545
 
409
- #
410
- # Waits for a given process instance to terminate.
411
- # The method only exits when the flow terminates, but beware : if
412
- # the process already terminated, the method will never exit.
413
- #
414
- # The parameter can be a FlowExpressionId instance, for example the
415
- # one given back by a launch(), or directly a workflow instance id
416
- # (String).
417
- #
418
- # This method is mainly used in utests.
419
- #
420
- def wait_for (fei_or_wfid)
421
-
422
- wfid = if fei_or_wfid.kind_of?(FlowExpressionId)
423
- fei_or_wfid.workflow_instance_id
424
- else
425
- fei_or_wfid
426
- end
427
-
428
- #Thread.pass
429
- # #
430
- # # let the flow 'stabilize' or progress before enquiring
431
- #fexp = get_expression_pool.fetch_expression(fei)
432
- #return unless fexp
433
- #
434
- # doesn't work well
435
-
436
- t = Thread.new { Thread.stop }
437
-
438
- get_expression_pool.add_observer(:terminate) do |channel, fe, wi|
439
- t.wakeup if fe.fei.workflow_instance_id == wfid
440
- end
441
-
442
- ldebug { "wait_for() #{wfid}" }
443
-
444
- t.join
546
+ get_expression_pool.forget(exp_or_fei)
445
547
  end
446
548
 
447
549
  protected
448
550
 
449
- #
551
+ #--
450
552
  # the following methods may get overridden upon extension
451
553
  # see for example file_persisted_engine.rb
452
- #
554
+ #++
453
555
 
454
556
  def build_expression_map
455
557
 
@@ -458,13 +560,14 @@ module OpenWFE
458
560
  # the expression map is not a Service anymore,
459
561
  # it's a simple instance (that will be reused in other
460
562
  # OpenWFEru components)
461
-
462
- #ldebug do
463
- # "build_expression_map() :\n" +
464
- # get_expression_map.to_s
465
- #end
466
563
  end
467
564
 
565
+ #
566
+ # This implementation builds a KotobaWfidGenerator instance and
567
+ # binds it in the engine context.
568
+ # There are other WfidGeneration implementations available, like
569
+ # UuidWfidGenerator or FieldWfidGenerator.
570
+ #
468
571
  def build_wfid_generator
469
572
 
470
573
  #init_service S_WFID_GENERATOR, DefaultWfidGenerator
@@ -479,26 +582,196 @@ module OpenWFE
479
582
  # the field "wfid" of the LaunchItem.
480
583
  end
481
584
 
585
+ #
586
+ # Builds the OpenWFEru expression pool (the core of the engine)
587
+ # and binds it in the engine context.
588
+ # There is only one implementation of the expression pool, so
589
+ # this method is usually never overriden.
590
+ #
482
591
  def build_expression_pool
483
592
 
484
593
  init_service(S_EXPRESSION_POOL, ExpressionPool)
485
594
  end
486
595
 
596
+ #
597
+ # The implementation here builds an InMemoryExpressionStorage
598
+ # instance.
599
+ #
600
+ # See FilePersistedEngine or CachedFilePersistedEngine for
601
+ # overrides of this method.
602
+ #
487
603
  def build_expression_storage
488
604
 
489
605
  init_service(S_EXPRESSION_STORAGE, InMemoryExpressionStorage)
490
606
  end
491
607
 
608
+ #
609
+ # The ParticipantMap is a mapping between participant names
610
+ # (well rather regular expressions) and participant implementations
611
+ # (see http://openwferu.rubyforge.org/participants.html)
612
+ #
492
613
  def build_participant_map
493
614
 
494
615
  init_service(S_PARTICIPANT_MAP, ParticipantMap)
495
616
  end
496
617
 
618
+ #
619
+ # There is only one Scheduler implementation, that's the one
620
+ # built and bound here.
621
+ #
497
622
  def build_scheduler
498
623
 
499
624
  init_service(S_SCHEDULER, SchedulerService)
500
625
  end
501
626
 
627
+ #
628
+ # The default implementation of this method uses an
629
+ # InMemoryErrorJournal (do not use in production).
630
+ #
631
+ def build_error_journal
632
+
633
+ init_service(S_ERROR_JOURNAL, InMemoryErrorJournal)
634
+ end
635
+
636
+ end
637
+
638
+ #
639
+ # ProcessStatus gathers information about the status of a business
640
+ # process instance.
641
+ #
642
+ # The status is mainly a list of expressions and a hash of errors.
643
+ #
644
+ # Instances of this class are obtained via Engine.process_status().
645
+ #
646
+ class ProcessStatus
647
+
648
+ #
649
+ # the String workflow instance id of the Process.
650
+ #
651
+ attr_reader :wfid
652
+
653
+ #
654
+ # The list of the expressions currently active in the process instance.
655
+ #
656
+ # For instance, if your process definition is currently in a
657
+ # concurrence, more than one expressions may be listed here.
658
+ #
659
+ attr_reader :expressions
660
+
661
+ #
662
+ # a hash whose values are ProcessError instances, the keys
663
+ # are FlowExpressionId instances (fei) (identifying the expressions
664
+ # that are concerned with the error)
665
+ #
666
+ attr_reader :errors
667
+
668
+ def initialize
669
+ @wfid = nil
670
+ @expressions = []
671
+ @errors = {}
672
+ end
673
+
674
+ #
675
+ # this method is used by Engine.get_process_status() when
676
+ # it prepares its results.
677
+ #
678
+ def << (item)
679
+
680
+ if item.kind_of?(FlowExpression)
681
+ add_expression item
682
+ else
683
+ add_error item
684
+ end
685
+ end
686
+
687
+ #
688
+ # A String representation, handy for debugging, quick viewing.
689
+ #
690
+ def to_s
691
+ s = ""
692
+ s << "-- #{self.class.name} --\n"
693
+ s << " wfid : #{@wfid}\n"
694
+ s << " expressions :\n"
695
+ @expressions.each do |fexp|
696
+ s << " #{fexp.fei}\n"
697
+ end
698
+ s << " errors : #{@errors.size}"
699
+ s
700
+ end
701
+
702
+ protected
703
+
704
+ def add_expression (fexp)
705
+
706
+ set_wfid fexp.fei.parent_wfid
707
+
708
+ #@expressions << fexp
709
+
710
+ exps = @expressions
711
+ @expressions = []
712
+
713
+ added = false
714
+ @expressions = exps.collect do |fe|
715
+ if added or fe.fei.wfid != fexp.fei.wfid
716
+ fe
717
+ else
718
+ if OpenWFE::starts_with(fexp.fei.expid, fe.fei.expid)
719
+ added = true
720
+ fexp
721
+ elsif OpenWFE::starts_with(fe.fei.expid, fexp.fei.expid)
722
+ added = true
723
+ fe
724
+ else
725
+ fe
726
+ end
727
+ end
728
+ end
729
+ @expressions << fexp unless added
730
+ end
731
+
732
+ def add_error (error)
733
+ @errors[error.fei] = error
734
+ end
735
+
736
+ def set_wfid (wfid)
737
+
738
+ return if @wfid
739
+ @wfid = wfid
740
+ end
741
+ end
742
+
743
+ #
744
+ # Renders a nice, terminal oriented, representation of an
745
+ # Engine.get_process_status() result.
746
+ #
747
+ # You usually directly benefit from this when doing
748
+ #
749
+ # puts engine.get_process_status.to_s
750
+ #
751
+ def pretty_print_process_status (ps)
752
+
753
+ s = ""
754
+ s << "process_id | name | rev | brn | err\n"
755
+ s << "--------------------+-------------------+---------+-----+-----\n"
756
+
757
+ ps.keys.sort.each do |wfid|
758
+
759
+ status = ps[wfid]
760
+ fexp = status.expressions[0]
761
+ ffei = fexp.fei
762
+
763
+ s << "%-19s" % wfid[0, 19]
764
+ s << " | "
765
+ s << "%-17s" % ffei.workflow_definition_name[0, 17]
766
+ s << " | "
767
+ s << "%-7s" % ffei.workflow_definition_revision[0, 7]
768
+ s << " | "
769
+ s << "%3s" % status.expressions.size.to_s[0, 3]
770
+ s << " | "
771
+ s << "%3s" % status.errors.size.to_s[0, 3]
772
+ s << "\n"
773
+ end
774
+ s
502
775
  end
503
776
 
504
777
  end
@@ -68,6 +68,14 @@ module OpenWFE
68
68
 
69
69
  init_service(S_EXPRESSION_STORAGE, YamlFileExpressionStorage)
70
70
  end
71
+
72
+ #
73
+ # Uses a file persisted error journal.
74
+ #
75
+ def build_error_journal ()
76
+
77
+ init_service(S_ERROR_JOURNAL, YamlErrorJournal)
78
+ end
71
79
  end
72
80
 
73
81
  #
@@ -78,7 +86,7 @@ module OpenWFE
78
86
  # like 'sleep', 'cron', ... But if you do it before registering the
79
87
  # participants you'll end up with broken processes.
80
88
  #
81
- class CachedFilePersistedEngine < Engine
89
+ class CachedFilePersistedEngine < FilePersistedEngine
82
90
 
83
91
  protected
84
92