openwferu 0.9.11 → 0.9.12

Sign up to get free protection for your applications and to get access to all the features.
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