dynflow 0.8.3 → 0.8.4
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +8 -8
- data/doc/pages/source/documentation/index.md +14 -14
- data/dynflow.gemspec +0 -1
- data/examples/future_execution.rb +7 -7
- data/lib/dynflow.rb +3 -3
- data/lib/dynflow/action.rb +7 -9
- data/lib/dynflow/action/with_sub_plans.rb +9 -3
- data/lib/dynflow/config.rb +2 -2
- data/lib/dynflow/coordinator.rb +3 -3
- data/lib/dynflow/delayed_executors.rb +9 -0
- data/lib/dynflow/{schedulers → delayed_executors}/abstract.rb +3 -3
- data/lib/dynflow/{schedulers → delayed_executors}/abstract_core.rb +7 -7
- data/lib/dynflow/{schedulers → delayed_executors}/polling.rb +6 -6
- data/lib/dynflow/{scheduled_plan.rb → delayed_plan.rb} +4 -4
- data/lib/dynflow/execution_plan.rb +10 -10
- data/lib/dynflow/execution_plan/output_reference.rb +2 -2
- data/lib/dynflow/execution_plan/steps/error.rb +1 -1
- data/lib/dynflow/execution_plan/steps/plan_step.rb +2 -2
- data/lib/dynflow/middleware.rb +1 -1
- data/lib/dynflow/middleware/stack.rb +1 -1
- data/lib/dynflow/middleware/world.rb +1 -1
- data/lib/dynflow/persistence.rb +10 -10
- data/lib/dynflow/persistence_adapters/abstract.rb +4 -4
- data/lib/dynflow/persistence_adapters/sequel.rb +17 -17
- data/lib/dynflow/persistence_adapters/sequel_migrations/008_rename_scheduled_plans_to_delayed_plans.rb +5 -0
- data/lib/dynflow/serializable.rb +1 -1
- data/lib/dynflow/serializer.rb +1 -1
- data/lib/dynflow/testing/assertions.rb +1 -1
- data/lib/dynflow/utils.rb +205 -0
- data/lib/dynflow/version.rb +1 -1
- data/lib/dynflow/web/console_helpers.rb +1 -1
- data/lib/dynflow/web/filtering_helpers.rb +3 -3
- data/lib/dynflow/world.rb +16 -16
- data/test/action_test.rb +4 -2
- data/test/future_execution_test.rb +32 -32
- data/test/middleware_test.rb +5 -5
- data/test/persistence_test.rb +3 -3
- data/test/support/dummy_example.rb +2 -2
- data/test/support/middleware_example.rb +5 -5
- data/test/test_helper.rb +1 -1
- data/test/testing_test.rb +1 -1
- data/web/views/show.erb +3 -3
- metadata +9 -21
- data/lib/dynflow/schedulers.rb +0 -9
checksums.yaml
CHANGED
@@ -1,15 +1,15 @@
|
|
1
1
|
---
|
2
2
|
!binary "U0hBMQ==":
|
3
3
|
metadata.gz: !binary |-
|
4
|
-
|
4
|
+
ZWZjMGI1OWM2ZWQ4N2JiNDM5MDVmMDA1NmJlODJiMmJmNzg1ZDRjOA==
|
5
5
|
data.tar.gz: !binary |-
|
6
|
-
|
6
|
+
ZWFhMTBiOWU0NTVjZTEyMGM5NDlhYTY1NDg0ZDVjYjg5MzJhZDFhMg==
|
7
7
|
SHA512:
|
8
8
|
metadata.gz: !binary |-
|
9
|
-
|
10
|
-
|
11
|
-
|
9
|
+
ZDI4ZGVlNjQ5NGZkZGFhY2I3ZTlmZmJkNjRiZTMzODVkMDYzODAwNWNkYzU5
|
10
|
+
OTFjNGRiNWZjYzAwOTdjNDc5ZDcxMjk2MWNlMWVlNzdiYzhlNDVmMWExYTFl
|
11
|
+
MWZjYTAxZjMwODY1NTM2MWZiODliZjU2OTI3NGIyZThkNGU4YTI=
|
12
12
|
data.tar.gz: !binary |-
|
13
|
-
|
14
|
-
|
15
|
-
|
13
|
+
OGEwM2M2YzViODM3Njc2NTExNTE0ZTY5YjA1NjNiNzJhMzIwNDBiZmQ2MmY0
|
14
|
+
NTg1MTBiYzViYjRjYmQ2MzU3YTFhZTdhOWQzZWIxMDYyZDM0NjM4YTZjYjc2
|
15
|
+
YTQyZTU1MWQ0MzcxYjMwNmYwYTM1NmUyOTc0YWQwZjQ1ZTU0OWM=
|
@@ -235,7 +235,7 @@ TriggerResult = Algebrick.type do
|
|
235
235
|
PlaningFailed = type { fields! execution_plan_id: String, error: Exception }
|
236
236
|
# Returned by #trigger when planning is successful but execution fails to start.
|
237
237
|
ExecutionFailed = type { fields! execution_plan_id: String, error: Exception }
|
238
|
-
# Returned by #
|
238
|
+
# Returned by #delay when scheduling succeeded.
|
239
239
|
Scheduled = type { fields! execution_plan_id: String }
|
240
240
|
# Returned by #trigger when planning is successful, #future will resolve after
|
241
241
|
# ExecutionPlan is executed.
|
@@ -273,19 +273,19 @@ end
|
|
273
273
|
#### Scheduling
|
274
274
|
|
275
275
|
Scheduling an action means setting it up to be triggered at set time in future.
|
276
|
-
Any action can be
|
276
|
+
Any action can be delayed by calling:
|
277
277
|
|
278
278
|
```ruby
|
279
|
-
world_instance.
|
280
|
-
|
281
|
-
|
279
|
+
world_instance.delay(AnAction,
|
280
|
+
{ start_at: Time.now + 360, start_before: Time.now + 400 },
|
281
|
+
*args)
|
282
282
|
```
|
283
283
|
|
284
|
-
This snippet of code would
|
284
|
+
This snippet of code would delay `AnAction` with arguments `args` to be executed
|
285
285
|
in the time interval between `start_at` and `start_before`. Setting `start_before` to `nil`
|
286
|
-
would
|
286
|
+
would delay execution of this action without the timeout limit.
|
287
287
|
|
288
|
-
When an action is
|
288
|
+
When an action is delayed, an execution plan object is created with state set
|
289
289
|
to `scheduled`, but it doesn't run the the plan phase yet, the planning happens
|
290
290
|
when the `start_at` time comes. If the planning doesn't happen in time
|
291
291
|
(e.g. after `start_before`), the execution plan is marked as failed
|
@@ -293,12 +293,12 @@ when the `start_at` time comes. If the planning doesn't happen in time
|
|
293
293
|
|
294
294
|
Since the `args` have to be saved, there must be a mechanism to safely serialize and deserialize them
|
295
295
|
in order to make them survive being saved in a database. This is handled by a serializer.
|
296
|
-
Different serializers can be set per action by overriding its `
|
296
|
+
Different serializers can be set per action by overriding its `delay` method.
|
297
297
|
|
298
|
-
Planning of the
|
299
|
-
periodically checks for
|
300
|
-
plans don't do anything by themselves, they just wait to be picked up and planned by a
|
301
|
-
It means that if no
|
298
|
+
Planning of the delayed plans is handled by `DelayedExecutor`, an object which
|
299
|
+
periodically checks for delayed execution plans and plans them. Scheduled execution
|
300
|
+
plans don't do anything by themselves, they just wait to be picked up and planned by a DelayedExecutor.
|
301
|
+
It means that if no DelayedExecutor is present, their planning will be delayed until a scheduler
|
302
302
|
is spawned.
|
303
303
|
|
304
304
|
#### Plan phase
|
@@ -1015,7 +1015,7 @@ client worlds: useful in production, see [develpment vs. production](#developmen
|
|
1015
1015
|
client requests and other worlds
|
1016
1016
|
1. **executor dispatcher** - responsible for getting requests from
|
1017
1017
|
other worlds and sending the responses
|
1018
|
-
1. **
|
1018
|
+
1. **delayed executor** - responsible for planning and exectuion of scheduled tasks
|
1019
1019
|
|
1020
1020
|
{% plantuml %}
|
1021
1021
|
|
data/dynflow.gemspec
CHANGED
@@ -26,7 +26,7 @@ end
|
|
26
26
|
|
27
27
|
class DelayedAction < Dynflow::Action
|
28
28
|
|
29
|
-
def
|
29
|
+
def delay(delay_options, *args)
|
30
30
|
CustomPassedObjectSerializer.new(args)
|
31
31
|
end
|
32
32
|
|
@@ -49,9 +49,9 @@ if $0 == __FILE__
|
|
49
49
|
|
50
50
|
object = CustomPassedObject.new(1, 'CPS')
|
51
51
|
|
52
|
-
past_plan = ExampleHelper.world.
|
53
|
-
near_future_plan = ExampleHelper.world.
|
54
|
-
future_plan = ExampleHelper.world.
|
52
|
+
past_plan = ExampleHelper.world.delay(DelayedAction, { :start_at => past, :start_before => past }, object)
|
53
|
+
near_future_plan = ExampleHelper.world.delay(DelayedAction, { :start_at => near_future, :start_before => future }, object)
|
54
|
+
future_plan = ExampleHelper.world.delay(DelayedAction, { :start_at => future }, object)
|
55
55
|
|
56
56
|
puts <<-MSG.gsub(/^.*\|/, '')
|
57
57
|
|
|
@@ -61,9 +61,9 @@ if $0 == __FILE__
|
|
61
61
|
| This example shows the future execution functionality of Dynflow, which allows to plan actions to be executed at set time.
|
62
62
|
|
|
63
63
|
| Execution plans:
|
64
|
-
| #{past_plan.id} is
|
65
|
-
| #{near_future_plan.id} is
|
66
|
-
| #{future_plan.id} is
|
64
|
+
| #{past_plan.id} is "delayed" to execute before #{past} and should timeout on the first run of the scheduler.
|
65
|
+
| #{near_future_plan.id} is delayed to execute at #{near_future} and should run successfully.
|
66
|
+
| #{future_plan.id} is delayed to execute at #{future} and should run successfully.
|
67
67
|
|
|
68
68
|
| Visit http://localhost:4567 to see their status.
|
69
69
|
|
|
data/lib/dynflow.rb
CHANGED
@@ -2,7 +2,6 @@ require 'apipie-params'
|
|
2
2
|
require 'algebrick'
|
3
3
|
require 'thread'
|
4
4
|
require 'set'
|
5
|
-
require 'active_support/core_ext/hash/indifferent_access'
|
6
5
|
require 'base64'
|
7
6
|
require 'concurrent'
|
8
7
|
require 'concurrent-edge'
|
@@ -22,6 +21,7 @@ module Dynflow
|
|
22
21
|
class Error < StandardError
|
23
22
|
end
|
24
23
|
|
24
|
+
require 'dynflow/utils'
|
25
25
|
require 'dynflow/round_robin'
|
26
26
|
require 'dynflow/actor'
|
27
27
|
require 'dynflow/errors'
|
@@ -36,7 +36,7 @@ module Dynflow
|
|
36
36
|
require 'dynflow/flows'
|
37
37
|
require 'dynflow/execution_history'
|
38
38
|
require 'dynflow/execution_plan'
|
39
|
-
require 'dynflow/
|
39
|
+
require 'dynflow/delayed_plan'
|
40
40
|
require 'dynflow/action'
|
41
41
|
require 'dynflow/executors'
|
42
42
|
require 'dynflow/logger_adapters'
|
@@ -44,6 +44,6 @@ module Dynflow
|
|
44
44
|
require 'dynflow/connectors'
|
45
45
|
require 'dynflow/dispatcher'
|
46
46
|
require 'dynflow/serializers'
|
47
|
-
require 'dynflow/
|
47
|
+
require 'dynflow/delayed_executors'
|
48
48
|
require 'dynflow/config'
|
49
49
|
end
|
data/lib/dynflow/action.rb
CHANGED
@@ -1,5 +1,3 @@
|
|
1
|
-
require 'active_support/inflector'
|
2
|
-
|
3
1
|
module Dynflow
|
4
2
|
class Action < Serializable
|
5
3
|
|
@@ -127,13 +125,13 @@ module Dynflow
|
|
127
125
|
def input=(hash)
|
128
126
|
Type! hash, Hash
|
129
127
|
phase! Plan
|
130
|
-
@input = hash
|
128
|
+
@input = Utils.indifferent_hash(hash)
|
131
129
|
end
|
132
130
|
|
133
131
|
def output=(hash)
|
134
132
|
Type! hash, Hash
|
135
133
|
phase! Run
|
136
|
-
@output = hash
|
134
|
+
@output = Utlis.indifferent_hash(hash)
|
137
135
|
end
|
138
136
|
|
139
137
|
def output
|
@@ -282,10 +280,10 @@ module Dynflow
|
|
282
280
|
recursion.(input)
|
283
281
|
end
|
284
282
|
|
285
|
-
def
|
283
|
+
def execute_delay(delay_options, *args)
|
286
284
|
with_error_handling(true) do
|
287
|
-
world.middleware.execute(:
|
288
|
-
@serializer =
|
285
|
+
world.middleware.execute(:delay, self, delay_options, *args) do
|
286
|
+
@serializer = delay(delay_options, *args).tap do |serializer|
|
289
287
|
serializer.perform_serialization!
|
290
288
|
end
|
291
289
|
end
|
@@ -293,7 +291,7 @@ module Dynflow
|
|
293
291
|
end
|
294
292
|
|
295
293
|
def serializer
|
296
|
-
raise "The action must be
|
294
|
+
raise "The action must be delayed in order to access the serializer" if @serializer.nil?
|
297
295
|
@serializer
|
298
296
|
end
|
299
297
|
|
@@ -313,7 +311,7 @@ module Dynflow
|
|
313
311
|
@step.save
|
314
312
|
end
|
315
313
|
|
316
|
-
def
|
314
|
+
def delay(delay_options, *args)
|
317
315
|
Serializers::Noop.new(args)
|
318
316
|
end
|
319
317
|
|
@@ -67,7 +67,8 @@ module Dynflow
|
|
67
67
|
def wait_for_sub_plans(sub_plans)
|
68
68
|
output.update(total_count: 0,
|
69
69
|
failed_count: 0,
|
70
|
-
success_count: 0
|
70
|
+
success_count: 0,
|
71
|
+
pending_count: 0)
|
71
72
|
|
72
73
|
planned, failed = sub_plans.partition(&:planned?)
|
73
74
|
|
@@ -75,6 +76,7 @@ module Dynflow
|
|
75
76
|
|
76
77
|
output[:total_count] = sub_plan_ids.size
|
77
78
|
output[:failed_count] = failed.size
|
79
|
+
output[:pending_count] = planned.size
|
78
80
|
|
79
81
|
if planned.any?
|
80
82
|
notify_on_finish(planned)
|
@@ -123,6 +125,7 @@ module Dynflow
|
|
123
125
|
else
|
124
126
|
output[:failed_count] += 1
|
125
127
|
end
|
128
|
+
output[:pending_count] -= 1
|
126
129
|
end
|
127
130
|
|
128
131
|
def done?
|
@@ -144,7 +147,8 @@ module Dynflow
|
|
144
147
|
def recalculate_counts
|
145
148
|
output.update(total_count: 0,
|
146
149
|
failed_count: 0,
|
147
|
-
success_count: 0
|
150
|
+
success_count: 0,
|
151
|
+
pending_count: 0)
|
148
152
|
sub_plans.each do |sub_plan|
|
149
153
|
output[:total_count] += 1
|
150
154
|
if sub_plan.state == :stopped
|
@@ -153,12 +157,14 @@ module Dynflow
|
|
153
157
|
else
|
154
158
|
output[:success_count] += 1
|
155
159
|
end
|
160
|
+
else
|
161
|
+
output[:pending_count] += 1
|
156
162
|
end
|
157
163
|
end
|
158
164
|
end
|
159
165
|
|
160
166
|
def counts_set?
|
161
|
-
output[:total_count] && output[:success_count] && output[:failed_count]
|
167
|
+
output[:total_count] && output[:success_count] && output[:failed_count] && output[:pending_count]
|
162
168
|
end
|
163
169
|
|
164
170
|
def check_for_errors!
|
data/lib/dynflow/config.rb
CHANGED
@@ -93,10 +93,10 @@ module Dynflow
|
|
93
93
|
true
|
94
94
|
end
|
95
95
|
|
96
|
-
config_attr :
|
96
|
+
config_attr :delayed_executor, DelayedExecutors::Abstract, NilClass do |world|
|
97
97
|
options = { :poll_interval => 15,
|
98
98
|
:time_source => -> { Time.now.utc } }
|
99
|
-
|
99
|
+
DelayedExecutors::Polling.new(world, options)
|
100
100
|
end
|
101
101
|
|
102
102
|
config_attr :action_classes do
|
data/lib/dynflow/coordinator.rb
CHANGED
@@ -41,7 +41,7 @@ module Dynflow
|
|
41
41
|
|
42
42
|
def initialize(*args)
|
43
43
|
@data ||= {}
|
44
|
-
@data = @data.merge(class: self.class.name)
|
44
|
+
@data = Utils.indifferent_hash(@data.merge(class: self.class.name))
|
45
45
|
end
|
46
46
|
|
47
47
|
def from_hash(hash)
|
@@ -157,10 +157,10 @@ module Dynflow
|
|
157
157
|
|
158
158
|
end
|
159
159
|
|
160
|
-
class
|
160
|
+
class DelayedExecutorLock < LockByWorld
|
161
161
|
def initialize(world)
|
162
162
|
super
|
163
|
-
@data[:id] = "
|
163
|
+
@data[:id] = "delayed-executor"
|
164
164
|
end
|
165
165
|
end
|
166
166
|
|
@@ -1,5 +1,5 @@
|
|
1
1
|
module Dynflow
|
2
|
-
module
|
2
|
+
module DelayedExecutors
|
3
3
|
class Abstract
|
4
4
|
|
5
5
|
attr_reader :core
|
@@ -26,7 +26,7 @@ module Dynflow
|
|
26
26
|
|
27
27
|
def spawn
|
28
28
|
Concurrent.future.tap do |initialized|
|
29
|
-
@core = core_class.spawn name: '
|
29
|
+
@core = core_class.spawn name: 'delayed-executor',
|
30
30
|
args: [@world, @options],
|
31
31
|
initialized: initialized
|
32
32
|
end
|
@@ -34,4 +34,4 @@ module Dynflow
|
|
34
34
|
|
35
35
|
end
|
36
36
|
end
|
37
|
-
end
|
37
|
+
end
|
@@ -1,5 +1,5 @@
|
|
1
1
|
module Dynflow
|
2
|
-
module
|
2
|
+
module DelayedExecutors
|
3
3
|
class AbstractCore < Actor
|
4
4
|
|
5
5
|
include Algebrick::TypeCheck
|
@@ -19,7 +19,7 @@ module Dynflow
|
|
19
19
|
@time_source = options.fetch(:time_source, -> { Time.now.utc })
|
20
20
|
end
|
21
21
|
|
22
|
-
def
|
22
|
+
def check_delayed_plans
|
23
23
|
raise NotImplementedError
|
24
24
|
end
|
25
25
|
|
@@ -29,9 +29,9 @@ module Dynflow
|
|
29
29
|
@time_source.call()
|
30
30
|
end
|
31
31
|
|
32
|
-
def
|
32
|
+
def delayed_execution_plans(time)
|
33
33
|
with_error_handling([]) do
|
34
|
-
world.persistence.
|
34
|
+
world.persistence.find_past_delayed_plans(time)
|
35
35
|
end
|
36
36
|
end
|
37
37
|
|
@@ -42,9 +42,9 @@ module Dynflow
|
|
42
42
|
error_retval
|
43
43
|
end
|
44
44
|
|
45
|
-
def process(
|
45
|
+
def process(delayed_plans, check_time)
|
46
46
|
processed_plan_uuids = []
|
47
|
-
|
47
|
+
delayed_plans.each do |plan|
|
48
48
|
with_error_handling do
|
49
49
|
if !plan.start_before.nil? && plan.start_before < check_time
|
50
50
|
@logger.debug "Failing plan #{plan.execution_plan_uuid}"
|
@@ -57,7 +57,7 @@ module Dynflow
|
|
57
57
|
processed_plan_uuids << plan.execution_plan_uuid
|
58
58
|
end
|
59
59
|
end
|
60
|
-
world.persistence.
|
60
|
+
world.persistence.delete_delayed_plans(:execution_plan_uuid => processed_plan_uuids) unless processed_plan_uuids.empty?
|
61
61
|
end
|
62
62
|
|
63
63
|
end
|
@@ -1,9 +1,9 @@
|
|
1
1
|
module Dynflow
|
2
|
-
module
|
2
|
+
module DelayedExecutors
|
3
3
|
class Polling < Abstract
|
4
4
|
|
5
5
|
def core_class
|
6
|
-
Dynflow::
|
6
|
+
Dynflow::DelayedExecutors::PollingCore
|
7
7
|
end
|
8
8
|
|
9
9
|
end
|
@@ -17,15 +17,15 @@ module Dynflow
|
|
17
17
|
end
|
18
18
|
|
19
19
|
def start
|
20
|
-
|
20
|
+
check_delayed_plans
|
21
21
|
end
|
22
22
|
|
23
|
-
def
|
23
|
+
def check_delayed_plans
|
24
24
|
check_time = time
|
25
|
-
plans =
|
25
|
+
plans = delayed_execution_plans(check_time)
|
26
26
|
process plans, check_time
|
27
27
|
|
28
|
-
world.clock.ping(self, poll_interval, :
|
28
|
+
world.clock.ping(self, poll_interval, :check_delayed_plans)
|
29
29
|
end
|
30
30
|
end
|
31
31
|
end
|
@@ -1,5 +1,5 @@
|
|
1
1
|
module Dynflow
|
2
|
-
class
|
2
|
+
class DelayedPlan < Serializable
|
3
3
|
|
4
4
|
include Algebrick::TypeCheck
|
5
5
|
|
@@ -37,8 +37,8 @@ module Dynflow
|
|
37
37
|
end
|
38
38
|
|
39
39
|
def cancel
|
40
|
-
error("
|
41
|
-
@world.persistence.
|
40
|
+
error("Delayed task cancelled", "Delayed task cancelled")
|
41
|
+
@world.persistence.delete_delayed_plans(:execution_plan_uuid => execution_plan.id)
|
42
42
|
return true
|
43
43
|
end
|
44
44
|
|
@@ -56,7 +56,7 @@ module Dynflow
|
|
56
56
|
|
57
57
|
# @api private
|
58
58
|
def self.new_from_hash(world, hash, *args)
|
59
|
-
serializer = hash[:args_serializer].
|
59
|
+
serializer = Utils.constantize(hash[:args_serializer]).new(nil, hash[:serialized_args])
|
60
60
|
self.new(world,
|
61
61
|
hash[:execution_plan_uuid],
|
62
62
|
string_to_time(hash[:start_at]),
|
@@ -152,23 +152,23 @@ module Dynflow
|
|
152
152
|
@last_step_id += 1
|
153
153
|
end
|
154
154
|
|
155
|
-
def
|
155
|
+
def delay(action_class, delay_options, *args)
|
156
156
|
save
|
157
157
|
@root_plan_step = add_scheduling_step(action_class)
|
158
|
-
execution_history.add("
|
159
|
-
serializer = root_plan_step.
|
160
|
-
|
158
|
+
execution_history.add("delay", @world.id)
|
159
|
+
serializer = root_plan_step.delay(delay_options, args)
|
160
|
+
delayed_plan = DelayedPlan.new(@world,
|
161
161
|
id,
|
162
|
-
|
163
|
-
|
162
|
+
delay_options[:start_at],
|
163
|
+
delay_options.fetch(:start_before, nil),
|
164
164
|
serializer)
|
165
|
-
persistence.
|
165
|
+
persistence.save_delayed_plan(delayed_plan)
|
166
166
|
ensure
|
167
167
|
update_state(error? ? :stopped : :scheduled)
|
168
168
|
end
|
169
169
|
|
170
|
-
def
|
171
|
-
@
|
170
|
+
def delay_record
|
171
|
+
@delay_record ||= persistence.load_delayed_plan(id)
|
172
172
|
end
|
173
173
|
|
174
174
|
def prepare(action_class, options = {})
|
@@ -208,7 +208,7 @@ module Dynflow
|
|
208
208
|
# array with the future value of the cancel result)
|
209
209
|
def cancel
|
210
210
|
if state == :scheduled
|
211
|
-
[Concurrent.future.tap { |f| f.success
|
211
|
+
[Concurrent.future.tap { |f| f.success delay_record.cancel }]
|
212
212
|
else
|
213
213
|
steps_to_cancel.map do |step|
|
214
214
|
world.event(id, step.id, ::Dynflow::Action::Cancellable::Cancel)
|