concurrent-ruby-edge 0.1.0.pre2
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 +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +284 -0
- data/lib/concurrent-edge.rb +11 -0
- data/lib/concurrent/actor.rb +98 -0
- data/lib/concurrent/actor/behaviour.rb +143 -0
- data/lib/concurrent/actor/behaviour/abstract.rb +51 -0
- data/lib/concurrent/actor/behaviour/awaits.rb +21 -0
- data/lib/concurrent/actor/behaviour/buffer.rb +56 -0
- data/lib/concurrent/actor/behaviour/errors_on_unknown_message.rb +12 -0
- data/lib/concurrent/actor/behaviour/executes_context.rb +17 -0
- data/lib/concurrent/actor/behaviour/linking.rb +83 -0
- data/lib/concurrent/actor/behaviour/pausing.rb +123 -0
- data/lib/concurrent/actor/behaviour/removes_child.rb +16 -0
- data/lib/concurrent/actor/behaviour/sets_results.rb +37 -0
- data/lib/concurrent/actor/behaviour/supervising.rb +39 -0
- data/lib/concurrent/actor/behaviour/terminates_children.rb +14 -0
- data/lib/concurrent/actor/behaviour/termination.rb +74 -0
- data/lib/concurrent/actor/context.rb +167 -0
- data/lib/concurrent/actor/core.rb +220 -0
- data/lib/concurrent/actor/default_dead_letter_handler.rb +9 -0
- data/lib/concurrent/actor/envelope.rb +41 -0
- data/lib/concurrent/actor/errors.rb +27 -0
- data/lib/concurrent/actor/internal_delegations.rb +59 -0
- data/lib/concurrent/actor/public_delegations.rb +40 -0
- data/lib/concurrent/actor/reference.rb +106 -0
- data/lib/concurrent/actor/root.rb +37 -0
- data/lib/concurrent/actor/type_check.rb +48 -0
- data/lib/concurrent/actor/utils.rb +10 -0
- data/lib/concurrent/actor/utils/ad_hoc.rb +27 -0
- data/lib/concurrent/actor/utils/balancer.rb +43 -0
- data/lib/concurrent/actor/utils/broadcast.rb +52 -0
- data/lib/concurrent/actor/utils/pool.rb +54 -0
- data/lib/concurrent/agent.rb +289 -0
- data/lib/concurrent/channel.rb +6 -0
- data/lib/concurrent/channel/blocking_ring_buffer.rb +82 -0
- data/lib/concurrent/channel/buffered_channel.rb +87 -0
- data/lib/concurrent/channel/channel.rb +19 -0
- data/lib/concurrent/channel/ring_buffer.rb +65 -0
- data/lib/concurrent/channel/unbuffered_channel.rb +39 -0
- data/lib/concurrent/channel/waitable_list.rb +48 -0
- data/lib/concurrent/edge/atomic_markable_reference.rb +184 -0
- data/lib/concurrent/edge/future.rb +1226 -0
- data/lib/concurrent/edge/lock_free_stack.rb +85 -0
- metadata +110 -0
@@ -0,0 +1,14 @@
|
|
1
|
+
module Concurrent
|
2
|
+
module Actor
|
3
|
+
module Behaviour
|
4
|
+
# Terminates all children when the actor terminates.
|
5
|
+
class TerminatesChildren < Abstract
|
6
|
+
def on_event(public, event)
|
7
|
+
event_name, _ = event
|
8
|
+
children.map { |ch| ch << :terminate! } if event_name == :terminated
|
9
|
+
super public, event
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
module Concurrent
|
2
|
+
module Actor
|
3
|
+
module Behaviour
|
4
|
+
|
5
|
+
# Handles actor termination.
|
6
|
+
# @note Actor rejects envelopes when terminated.
|
7
|
+
# @note TODO missing example
|
8
|
+
class Termination < Abstract
|
9
|
+
|
10
|
+
# @!attribute [r] terminated
|
11
|
+
# @return [Edge::Event] event which will become set when actor is terminated.
|
12
|
+
# @!attribute [r] reason
|
13
|
+
attr_reader :terminated, :reason
|
14
|
+
|
15
|
+
def initialize(core, subsequent, core_options, trapping = false)
|
16
|
+
super core, subsequent, core_options
|
17
|
+
@terminated = Concurrent.event
|
18
|
+
@public_terminated = @terminated.hide_completable
|
19
|
+
@reason = nil
|
20
|
+
@trapping = trapping
|
21
|
+
end
|
22
|
+
|
23
|
+
# @note Actor rejects envelopes when terminated.
|
24
|
+
# @return [true, false] if actor is terminated
|
25
|
+
def terminated?
|
26
|
+
@terminated.completed?
|
27
|
+
end
|
28
|
+
|
29
|
+
def trapping?
|
30
|
+
@trapping
|
31
|
+
end
|
32
|
+
|
33
|
+
def trapping=(val)
|
34
|
+
@trapping = !!val
|
35
|
+
end
|
36
|
+
|
37
|
+
def on_envelope(envelope)
|
38
|
+
command, reason = envelope.message
|
39
|
+
case command
|
40
|
+
when :terminated?
|
41
|
+
terminated?
|
42
|
+
when :terminate!
|
43
|
+
if trapping? && reason != :kill
|
44
|
+
pass envelope
|
45
|
+
else
|
46
|
+
terminate! reason
|
47
|
+
end
|
48
|
+
when :termination_event
|
49
|
+
@public_terminated
|
50
|
+
else
|
51
|
+
if terminated?
|
52
|
+
reject_envelope envelope
|
53
|
+
MESSAGE_PROCESSED
|
54
|
+
else
|
55
|
+
pass envelope
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
# Terminates the actor. Any Envelope received after termination is rejected.
|
61
|
+
# Terminates all its children, does not wait until they are terminated.
|
62
|
+
def terminate!(reason = :normal)
|
63
|
+
# TODO return after all children are terminated
|
64
|
+
return true if terminated?
|
65
|
+
@reason = reason
|
66
|
+
terminated.complete
|
67
|
+
broadcast(true, [:terminated, reason]) # TODO do not end up in Dead Letter Router
|
68
|
+
parent << :remove_child if parent
|
69
|
+
true
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
@@ -0,0 +1,167 @@
|
|
1
|
+
require 'concurrent/concern/logging'
|
2
|
+
|
3
|
+
module Concurrent
|
4
|
+
module Actor
|
5
|
+
|
6
|
+
# New actor is defined by subclassing {RestartingContext}, {Context} and defining its abstract methods.
|
7
|
+
# {AbstractContext} can be subclassed directly to implement more specific behaviour see {Root} implementation.
|
8
|
+
#
|
9
|
+
# - {Context}
|
10
|
+
#
|
11
|
+
# > {include:Actor::Context}
|
12
|
+
#
|
13
|
+
# - {RestartingContext}.
|
14
|
+
#
|
15
|
+
# > {include:Actor::RestartingContext}
|
16
|
+
#
|
17
|
+
# Example of ac actor definition:
|
18
|
+
#
|
19
|
+
# {include:file:doc/actor/define.out.rb}
|
20
|
+
#
|
21
|
+
# See methods of {AbstractContext} what else can be tweaked, e.g {AbstractContext#default_reference_class}
|
22
|
+
#
|
23
|
+
# @abstract implement {AbstractContext#on_message} and {AbstractContext#behaviour_definition}
|
24
|
+
class AbstractContext
|
25
|
+
include TypeCheck
|
26
|
+
include InternalDelegations
|
27
|
+
include Concern::Logging
|
28
|
+
|
29
|
+
attr_reader :core
|
30
|
+
|
31
|
+
# @abstract override to define Actor's behaviour
|
32
|
+
# @param [Object] message
|
33
|
+
# @return [Object] a result which will be used to set the Future supplied to Reference#ask
|
34
|
+
# @note self should not be returned (or sent to other actors), {#reference} should be used
|
35
|
+
# instead
|
36
|
+
def on_message(message)
|
37
|
+
raise NotImplementedError
|
38
|
+
end
|
39
|
+
|
40
|
+
# override to add custom code invocation on internal events like `:terminated`, `:resumed`, `anError`.
|
41
|
+
def on_event(event)
|
42
|
+
end
|
43
|
+
|
44
|
+
# @api private
|
45
|
+
def on_envelope(envelope)
|
46
|
+
@envelope = envelope
|
47
|
+
on_message envelope.message
|
48
|
+
ensure
|
49
|
+
@envelope = nil
|
50
|
+
end
|
51
|
+
|
52
|
+
# if you want to pass the message to next behaviour, usually
|
53
|
+
# {Behaviour::ErrorsOnUnknownMessage}
|
54
|
+
def pass
|
55
|
+
core.behaviour!(Behaviour::ExecutesContext).pass envelope
|
56
|
+
end
|
57
|
+
|
58
|
+
# Defines an actor responsible for dead letters. Any rejected message send
|
59
|
+
# with {Reference#tell} is sent there, a message with future is considered
|
60
|
+
# already monitored for failures. Default behaviour is to use
|
61
|
+
# {AbstractContext#dead_letter_routing} of the parent, so if no
|
62
|
+
# {AbstractContext#dead_letter_routing} method is overridden in
|
63
|
+
# parent-chain the message ends up in `Actor.root.dead_letter_routing`
|
64
|
+
# agent which will log warning.
|
65
|
+
# @return [Reference]
|
66
|
+
def dead_letter_routing
|
67
|
+
parent.dead_letter_routing
|
68
|
+
end
|
69
|
+
|
70
|
+
# @return [Array<Array(Behavior::Abstract, Array<Object>)>]
|
71
|
+
def behaviour_definition
|
72
|
+
raise NotImplementedError
|
73
|
+
end
|
74
|
+
|
75
|
+
# @return [Envelope] current envelope, accessible inside #on_message processing
|
76
|
+
def envelope
|
77
|
+
@envelope or raise 'envelope not set'
|
78
|
+
end
|
79
|
+
|
80
|
+
# override if different class for reference is needed
|
81
|
+
# @return [CLass] descendant of {Reference}
|
82
|
+
def default_reference_class
|
83
|
+
Reference
|
84
|
+
end
|
85
|
+
|
86
|
+
# override to se different default executor, e.g. to change it to global_operation_pool
|
87
|
+
# @return [Executor]
|
88
|
+
def default_executor
|
89
|
+
Concurrent.global_io_executor
|
90
|
+
end
|
91
|
+
|
92
|
+
# tell self a message
|
93
|
+
def tell(message)
|
94
|
+
reference.tell message
|
95
|
+
end
|
96
|
+
|
97
|
+
def ask(message)
|
98
|
+
raise 'actor cannot ask itself'
|
99
|
+
end
|
100
|
+
|
101
|
+
alias_method :<<, :tell
|
102
|
+
alias_method :ask!, :ask
|
103
|
+
|
104
|
+
# Behaves as {Concurrent::Actor.spawn} but :class is auto-inserted based on receiver so it can be omitted.
|
105
|
+
# @example by class and name
|
106
|
+
# AdHoc.spawn(:ping1) { -> message { message } }
|
107
|
+
#
|
108
|
+
# @example by option hash
|
109
|
+
# inc2 = AdHoc.spawn(name: 'increment by 2',
|
110
|
+
# args: [2],
|
111
|
+
# executor: Concurrent.configuration.global_task_pool) do |increment_by|
|
112
|
+
# lambda { |number| number + increment_by }
|
113
|
+
# end
|
114
|
+
# inc2.ask!(2) # => 4
|
115
|
+
# @see Concurrent::Actor.spawn
|
116
|
+
def self.spawn(name_or_opts, *args, &block)
|
117
|
+
Actor.spawn to_spawn_options(name_or_opts, *args), &block
|
118
|
+
end
|
119
|
+
|
120
|
+
# behaves as {Concurrent::Actor.spawn!} but :class is auto-inserted based on receiver so it can be omitted.
|
121
|
+
def self.spawn!(name_or_opts, *args, &block)
|
122
|
+
Actor.spawn! to_spawn_options(name_or_opts, *args), &block
|
123
|
+
end
|
124
|
+
|
125
|
+
private
|
126
|
+
|
127
|
+
def initialize_core(core)
|
128
|
+
@core = Type! core, Core
|
129
|
+
end
|
130
|
+
|
131
|
+
def self.to_spawn_options(name_or_opts, *args)
|
132
|
+
if name_or_opts.is_a? Hash
|
133
|
+
if name_or_opts.key?(:class) && name_or_opts[:class] != self
|
134
|
+
raise ArgumentError,
|
135
|
+
':class option is ignored when calling on context class, use Actor.spawn instead'
|
136
|
+
end
|
137
|
+
name_or_opts.merge class: self
|
138
|
+
else
|
139
|
+
{ class: self, name: name_or_opts, args: args }
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
# to avoid confusion with Kernel.spawn
|
144
|
+
undef_method :spawn
|
145
|
+
end
|
146
|
+
|
147
|
+
# Basic Context of an Actor. It supports only linking and it simply terminates on error.
|
148
|
+
# Uses {Behaviour.basic_behaviour_definition}:
|
149
|
+
#
|
150
|
+
# @abstract implement {AbstractContext#on_message}
|
151
|
+
class Context < AbstractContext
|
152
|
+
def behaviour_definition
|
153
|
+
Behaviour.basic_behaviour_definition
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
# Context of an Actor for robust systems. It supports supervision, linking, pauses on error.
|
158
|
+
# Uses {Behaviour.restarting_behaviour_definition}
|
159
|
+
#
|
160
|
+
# @abstract implement {AbstractContext#on_message}
|
161
|
+
class RestartingContext < AbstractContext
|
162
|
+
def behaviour_definition
|
163
|
+
Behaviour.restarting_behaviour_definition
|
164
|
+
end
|
165
|
+
end
|
166
|
+
end
|
167
|
+
end
|
@@ -0,0 +1,220 @@
|
|
1
|
+
require 'concurrent/concern/logging'
|
2
|
+
require 'concurrent/executors'
|
3
|
+
|
4
|
+
module Concurrent
|
5
|
+
module Actor
|
6
|
+
|
7
|
+
require 'set'
|
8
|
+
|
9
|
+
# Core of the actor.
|
10
|
+
# @note Whole class should be considered private. An user should use {Context}s and {Reference}s only.
|
11
|
+
# @note devel: core should not block on anything, e.g. it cannot wait on children to terminate
|
12
|
+
# that would eat up all threads in task pool and deadlock
|
13
|
+
class Core < Synchronization::Object
|
14
|
+
include TypeCheck
|
15
|
+
include Concern::Logging
|
16
|
+
|
17
|
+
# @!attribute [r] reference
|
18
|
+
# Reference to this actor which can be safely passed around.
|
19
|
+
# @return [Reference]
|
20
|
+
# @!attribute [r] name
|
21
|
+
# The name of actor instance, it should be uniq (not enforced). Allows easier orientation
|
22
|
+
# between actor instances.
|
23
|
+
# @return [String]
|
24
|
+
# @!attribute [r] path
|
25
|
+
# Path of this actor. It is used for easier orientation and logging.
|
26
|
+
# Path is constructed recursively with: `parent.path + self.name` up to a {Actor.root},
|
27
|
+
# e.g. `/an_actor/its_child`.
|
28
|
+
# @return [String]
|
29
|
+
# @!attribute [r] executor
|
30
|
+
# Executor which is used to process messages.
|
31
|
+
# @return [Executor]
|
32
|
+
# @!attribute [r] actor_class
|
33
|
+
# A subclass of {AbstractContext} representing Actor's behaviour.
|
34
|
+
# @return [Context]
|
35
|
+
attr_reader :reference, :name, :path, :executor, :context_class, :context, :behaviour_definition
|
36
|
+
|
37
|
+
# @option opts [String] name
|
38
|
+
# @option opts [Context] actor_class a class to be instantiated defining Actor's behaviour
|
39
|
+
# @option opts [Array<Object>] args arguments for actor_class instantiation
|
40
|
+
# @option opts [Executor] executor, default is `global_io_executor`
|
41
|
+
# @option opts [true, false] link, atomically link the actor to its parent (default: true)
|
42
|
+
# @option opts [Class] reference a custom descendant of {Reference} to use
|
43
|
+
# @option opts [Array<Array(Behavior::Abstract, Array<Object>)>] behaviour_definition, array of pairs
|
44
|
+
# where each pair is behaviour class and its args, see {Behaviour.basic_behaviour_definition}
|
45
|
+
# @option opts [CompletableFuture, nil] initialized, if present it'll be set or failed after {Context} initialization
|
46
|
+
# @option opts [Reference, nil] parent **private api** parent of the actor (the one spawning )
|
47
|
+
# @option opts [Proc, nil] logger a proc accepting (level, progname, message = nil, &block) params,
|
48
|
+
# can be used to hook actor instance to any logging system, see {Concurrent::Concern::Logging}
|
49
|
+
# @param [Proc] block for class instantiation
|
50
|
+
def initialize(opts = {}, &block)
|
51
|
+
super(&nil)
|
52
|
+
synchronize { ns_initialize(opts, &block) }
|
53
|
+
end
|
54
|
+
|
55
|
+
# A parent Actor. When actor is spawned the {Actor.current} becomes its parent.
|
56
|
+
# When actor is spawned from a thread outside of an actor ({Actor.current} is nil) {Actor.root} is assigned.
|
57
|
+
# @return [Reference, nil]
|
58
|
+
def parent
|
59
|
+
@parent_core && @parent_core.reference
|
60
|
+
end
|
61
|
+
|
62
|
+
# @see AbstractContext#dead_letter_routing
|
63
|
+
def dead_letter_routing
|
64
|
+
@context.dead_letter_routing
|
65
|
+
end
|
66
|
+
|
67
|
+
# @return [Array<Reference>] of children actors
|
68
|
+
def children
|
69
|
+
guard!
|
70
|
+
@children.to_a
|
71
|
+
end
|
72
|
+
|
73
|
+
# @api private
|
74
|
+
def add_child(child)
|
75
|
+
guard!
|
76
|
+
Type! child, Reference
|
77
|
+
@children.add child
|
78
|
+
nil
|
79
|
+
end
|
80
|
+
|
81
|
+
# @api private
|
82
|
+
def remove_child(child)
|
83
|
+
guard!
|
84
|
+
Type! child, Reference
|
85
|
+
@children.delete child
|
86
|
+
nil
|
87
|
+
end
|
88
|
+
|
89
|
+
# is executed by Reference scheduling processing of new messages
|
90
|
+
# can be called from other alternative Reference implementations
|
91
|
+
# @param [Envelope] envelope
|
92
|
+
def on_envelope(envelope)
|
93
|
+
schedule_execution do
|
94
|
+
log DEBUG, "was #{envelope.future ? 'asked' : 'told'} #{envelope.message.inspect} by #{envelope.sender}"
|
95
|
+
process_envelope envelope
|
96
|
+
end
|
97
|
+
nil
|
98
|
+
end
|
99
|
+
|
100
|
+
# ensures that we are inside of the executor
|
101
|
+
def guard!
|
102
|
+
unless Actor.current == reference
|
103
|
+
raise "can be called only inside actor #{reference} but was #{Actor.current}"
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
def log(level, message = nil, &block)
|
108
|
+
super level, @path, message, &block
|
109
|
+
end
|
110
|
+
|
111
|
+
# Schedules blocks to be executed on executor sequentially,
|
112
|
+
# sets Actress.current
|
113
|
+
def schedule_execution
|
114
|
+
@serialized_execution.post(@executor) do
|
115
|
+
synchronize do
|
116
|
+
begin
|
117
|
+
Thread.current[:__current_actor__] = reference
|
118
|
+
yield
|
119
|
+
rescue => e
|
120
|
+
log FATAL, e
|
121
|
+
ensure
|
122
|
+
Thread.current[:__current_actor__] = nil
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
nil
|
128
|
+
end
|
129
|
+
|
130
|
+
def broadcast(public, event)
|
131
|
+
log DEBUG, "event: #{event.inspect} (#{public ? 'public' : 'private'})"
|
132
|
+
@first_behaviour.on_event(public, event)
|
133
|
+
end
|
134
|
+
|
135
|
+
# @param [Class] behaviour_class
|
136
|
+
# @return [Behaviour::Abstract, nil] based on behaviour_class
|
137
|
+
def behaviour(behaviour_class)
|
138
|
+
@behaviours[behaviour_class]
|
139
|
+
end
|
140
|
+
|
141
|
+
# @param [Class] behaviour_class
|
142
|
+
# @return [Behaviour::Abstract] based on behaviour_class
|
143
|
+
# @raise [KeyError] when no behaviour
|
144
|
+
def behaviour!(behaviour_class)
|
145
|
+
@behaviours.fetch behaviour_class
|
146
|
+
end
|
147
|
+
|
148
|
+
# @api private
|
149
|
+
def allocate_context
|
150
|
+
@context = @context_class.allocate
|
151
|
+
end
|
152
|
+
|
153
|
+
# @api private
|
154
|
+
def build_context
|
155
|
+
@context.send :initialize_core, self
|
156
|
+
@context.send :initialize, *@args, &@block
|
157
|
+
end
|
158
|
+
|
159
|
+
# @api private
|
160
|
+
def process_envelope(envelope)
|
161
|
+
@first_behaviour.on_envelope envelope
|
162
|
+
end
|
163
|
+
|
164
|
+
private
|
165
|
+
|
166
|
+
def ns_initialize(opts, &block)
|
167
|
+
@mailbox = Array.new
|
168
|
+
@serialized_execution = SerializedExecution.new
|
169
|
+
@children = Set.new
|
170
|
+
|
171
|
+
@context_class = Child! opts.fetch(:class), AbstractContext
|
172
|
+
allocate_context
|
173
|
+
|
174
|
+
@executor = Type! opts.fetch(:executor, @context.default_executor), Concurrent::AbstractExecutorService
|
175
|
+
raise ArgumentError, 'ImmediateExecutor is not supported' if @executor.is_a? ImmediateExecutor
|
176
|
+
|
177
|
+
@reference = (Child! opts[:reference_class] || @context.default_reference_class, Reference).new self
|
178
|
+
@name = (Type! opts.fetch(:name), String, Symbol).to_s
|
179
|
+
|
180
|
+
parent = opts[:parent]
|
181
|
+
@parent_core = (Type! parent, Reference, NilClass) && parent.send(:core)
|
182
|
+
if @parent_core.nil? && @name != '/'
|
183
|
+
raise 'only root has no parent'
|
184
|
+
end
|
185
|
+
|
186
|
+
@path = @parent_core ? File.join(@parent_core.path, @name) : @name
|
187
|
+
@logger = opts[:logger]
|
188
|
+
|
189
|
+
@parent_core.add_child reference if @parent_core
|
190
|
+
|
191
|
+
initialize_behaviours opts
|
192
|
+
|
193
|
+
@args = opts.fetch(:args, [])
|
194
|
+
@block = block
|
195
|
+
initialized = Type! opts[:initialized], Edge::CompletableFuture, NilClass
|
196
|
+
|
197
|
+
schedule_execution do
|
198
|
+
begin
|
199
|
+
build_context
|
200
|
+
initialized.success reference if initialized
|
201
|
+
log DEBUG, 'spawned'
|
202
|
+
rescue => ex
|
203
|
+
log ERROR, ex
|
204
|
+
@first_behaviour.terminate!
|
205
|
+
initialized.fail ex if initialized
|
206
|
+
end
|
207
|
+
end
|
208
|
+
end
|
209
|
+
|
210
|
+
def initialize_behaviours(opts)
|
211
|
+
@behaviour_definition = (Type! opts[:behaviour_definition] || @context.behaviour_definition, Array).each do |(behaviour, *args)|
|
212
|
+
Child! behaviour, Behaviour::Abstract
|
213
|
+
end
|
214
|
+
@behaviours = {}
|
215
|
+
@first_behaviour = @behaviour_definition.reverse.
|
216
|
+
reduce(nil) { |last, (behaviour, *args)| @behaviours[behaviour] = behaviour.new(self, last, opts, *args) }
|
217
|
+
end
|
218
|
+
end
|
219
|
+
end
|
220
|
+
end
|