ask-graph 0.6.0 → 0.7.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +78 -0
- data/README.md +145 -116
- data/lib/ask/graph/context.rb +2 -1
- data/lib/ask/graph/runner.rb +38 -22
- data/lib/ask/graph/version.rb +1 -1
- data/lib/ask/graph.rb +64 -26
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8035b4d2c2a93881c2733cb41e33696fe6e7403b2233a709c89ac75e1035323a
|
|
4
|
+
data.tar.gz: 04c96e95ec9b0afa00133f100627250ae26dc87d9208be62b22246f80548eaa7
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 3a365f235bb392a4be69981dd816bc6f961ea68a1fe740e094fe07a4b1a5f7f39839261ba75784b5028a6a82b7add2351e9f10c97695a7a9edeb00b512cf4d67
|
|
7
|
+
data.tar.gz: fabd27780966a9877e4a17b014388fcad7c3c57fdfbe9df8bc433ba15d05960fe3542f09c6f9501e388d147197a5d84c85964b59646f3b245a44cdb03ba13a6d
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,81 @@
|
|
|
1
|
+
## [0.7.0] — 2026-07-31
|
|
2
|
+
|
|
3
|
+
### Added
|
|
4
|
+
|
|
5
|
+
- **`workflow_timeout`** — total runtime cap for the entire workflow. The
|
|
6
|
+
whole run aborts with `Ask::Graph::WorkflowTimeout` if it exceeds the
|
|
7
|
+
limit, regardless of individual step timeouts.
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
class MyWorkflow < Ask::Graph
|
|
11
|
+
workflow_timeout 60 # whole workflow must finish within 60s
|
|
12
|
+
step FetchData, timeout: 30
|
|
13
|
+
step ProcessData, timeout: 30
|
|
14
|
+
end
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
- **`Ask::Graph.default_workflow_timeout`** — global default total cap for
|
|
18
|
+
all graphs that don't set their own.
|
|
19
|
+
|
|
20
|
+
### Changed
|
|
21
|
+
|
|
22
|
+
- **`timeout` renamed to `step_timeout`** — the class-level default timeout
|
|
23
|
+
per step is now `step_timeout`. The inline step option `step X, timeout: 10`
|
|
24
|
+
is unchanged. No alias is kept — the gem is pre-1.0.
|
|
25
|
+
|
|
26
|
+
```ruby
|
|
27
|
+
# Before
|
|
28
|
+
class MyWorkflow < Ask::Graph
|
|
29
|
+
timeout 30
|
|
30
|
+
end
|
|
31
|
+
Ask::Graph.timeout 30
|
|
32
|
+
|
|
33
|
+
# After
|
|
34
|
+
class MyWorkflow < Ask::Graph
|
|
35
|
+
step_timeout 30
|
|
36
|
+
end
|
|
37
|
+
Ask::Graph.default_step_timeout 30
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### Tested
|
|
41
|
+
|
|
42
|
+
- 95 tests, 129 assertions, 0 failures
|
|
43
|
+
|
|
44
|
+
## [0.6.1] — 2026-07-27
|
|
45
|
+
|
|
46
|
+
### Fixed
|
|
47
|
+
|
|
48
|
+
- **Hooks leak between classes** — `inherited` now deep-copies lifecycle
|
|
49
|
+
hook arrays instead of sharing them. Previously, a child class's
|
|
50
|
+
`after_step :foo` would mutate the parent class's hook arrays, causing
|
|
51
|
+
hooks to fire multiple times in sibling classes.
|
|
52
|
+
|
|
53
|
+
### Changed
|
|
54
|
+
|
|
55
|
+
- **Docs updated** to use the `Module::Workflow` convention throughout.
|
|
56
|
+
Recommended layout:
|
|
57
|
+
|
|
58
|
+
```
|
|
59
|
+
app/workflows/
|
|
60
|
+
notify_customer/
|
|
61
|
+
workflow.rb # module NotifyCustomer; class Workflow < Ask::Graph
|
|
62
|
+
steps/
|
|
63
|
+
send_email.rb
|
|
64
|
+
log_notification.rb
|
|
65
|
+
order_fulfillment/
|
|
66
|
+
workflow.rb
|
|
67
|
+
steps/
|
|
68
|
+
validate_payment.rb
|
|
69
|
+
notify_customer.rb
|
|
70
|
+
ship_order.rb
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Sub-graph composition via `Workflow.call(context)` or `context.run(Workflow)`.
|
|
74
|
+
|
|
75
|
+
### Tested
|
|
76
|
+
|
|
77
|
+
- 85 tests, 111 assertions, 0 failures
|
|
78
|
+
|
|
1
79
|
## [0.6.0] — 2026-07-27
|
|
2
80
|
|
|
3
81
|
### Changed
|
data/README.md
CHANGED
|
@@ -6,28 +6,30 @@
|
|
|
6
6
|
gem "ask-graph"
|
|
7
7
|
```
|
|
8
8
|
|
|
9
|
-
## Quick Start
|
|
10
|
-
|
|
11
|
-
```ruby
|
|
12
|
-
require "ask-graph"
|
|
13
|
-
|
|
14
|
-
# Define a workflow
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
9
|
+
## Quick Start
|
|
10
|
+
|
|
11
|
+
```ruby
|
|
12
|
+
require "ask-graph"
|
|
13
|
+
|
|
14
|
+
# Define a workflow
|
|
15
|
+
module ProcessOrder
|
|
16
|
+
class Workflow < Ask::Graph
|
|
17
|
+
step ValidateOrder
|
|
18
|
+
step ChargeCustomer, if: :valid?
|
|
19
|
+
step SendConfirmation, if: :valid?
|
|
20
|
+
step NotifyAdmin, unless: :valid?
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def valid?
|
|
25
|
+
context.order.valid?
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Run it
|
|
31
|
+
result = ProcessOrder::Workflow.call(order: order)
|
|
32
|
+
```
|
|
31
33
|
|
|
32
34
|
## Steps
|
|
33
35
|
|
|
@@ -41,86 +43,96 @@ class ValidateOrder
|
|
|
41
43
|
end
|
|
42
44
|
```
|
|
43
45
|
|
|
44
|
-
### Sequential steps
|
|
45
|
-
|
|
46
|
-
```ruby
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
```
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
```
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
```
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
class
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
end
|
|
123
|
-
|
|
46
|
+
### Sequential steps
|
|
47
|
+
|
|
48
|
+
```ruby
|
|
49
|
+
module HandleCall
|
|
50
|
+
class Workflow < Ask::Graph
|
|
51
|
+
step Transcribe
|
|
52
|
+
step Classify
|
|
53
|
+
step Respond
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### Conditional steps
|
|
59
|
+
|
|
60
|
+
```ruby
|
|
61
|
+
module HandleCall
|
|
62
|
+
class Workflow < Ask::Graph
|
|
63
|
+
step BookAppointment, if: :booking?
|
|
64
|
+
step EmergencyAlert, if: :emergency?
|
|
65
|
+
step HandleInquiry, unless: :known_intent?
|
|
66
|
+
|
|
67
|
+
private
|
|
68
|
+
|
|
69
|
+
def booking? = context.intent == "booking"
|
|
70
|
+
def emergency? = context.intent == "emergency"
|
|
71
|
+
def known_intent? = %w[booking emergency inquiry].include?(context.intent)
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### Parallel steps
|
|
77
|
+
|
|
78
|
+
Use `steps` (plural) to run multiple steps simultaneously:
|
|
79
|
+
|
|
80
|
+
```ruby
|
|
81
|
+
module SyncData
|
|
82
|
+
class Workflow < Ask::Graph
|
|
83
|
+
step FetchRecords
|
|
84
|
+
|
|
85
|
+
# All three run in parallel
|
|
86
|
+
steps CrmUpdate, CalendarSync, SendNotification
|
|
87
|
+
|
|
88
|
+
step ConfirmResponse
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### Human-in-the-loop (approve)
|
|
94
|
+
|
|
95
|
+
`approve` runs a step, then pauses the workflow and waits for external input:
|
|
96
|
+
|
|
97
|
+
```ruby
|
|
98
|
+
module ProcessBooking
|
|
99
|
+
class Workflow < Ask::Graph
|
|
100
|
+
step BookAppointment
|
|
101
|
+
approve ReviewBooking, if: :expensive?
|
|
102
|
+
step ConfirmBooking
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
After `approve` pauses, resume with:
|
|
108
|
+
|
|
109
|
+
```ruby
|
|
110
|
+
graph = ProcessBooking::Workflow.new
|
|
111
|
+
result = graph.run # runs, pauses after ReviewBooking
|
|
112
|
+
result = graph.resume(input: "approved") # resumes, runs ConfirmBooking
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
### Per-item loops
|
|
116
|
+
|
|
117
|
+
Use `context.each` inside a step for iteration with automatic checkpointing:
|
|
118
|
+
|
|
119
|
+
```ruby
|
|
120
|
+
class SendVoiceReminders
|
|
121
|
+
def call(context)
|
|
122
|
+
context.each(context.appointments) do |appt|
|
|
123
|
+
PhoneService.call(appt.number, appt.message)
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
module DailyReminders
|
|
129
|
+
class Workflow < Ask::Graph
|
|
130
|
+
step FetchAppointments
|
|
131
|
+
step SendVoiceReminders # loops with per-item checkpointing
|
|
132
|
+
step MarkComplete
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
```
|
|
124
136
|
|
|
125
137
|
If the process crashes mid-loop, it resumes from the last completed item.
|
|
126
138
|
|
|
@@ -146,33 +158,48 @@ end
|
|
|
146
158
|
|
|
147
159
|
### Sub-workflows
|
|
148
160
|
|
|
149
|
-
A step can delegate to another workflow
|
|
161
|
+
A step can delegate to another workflow by calling `Workflow.call(context)`.
|
|
162
|
+
The context is automatically exported, passed to the sub-workflow, and
|
|
163
|
+
results are merged back on completion:
|
|
150
164
|
|
|
151
165
|
```ruby
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
BookingWorkflow.new.call(context) # sub-workflow as a step
|
|
157
|
-
when "emergency"
|
|
158
|
-
EmergencyWorkflow.new.call(context)
|
|
166
|
+
module OrderFulfillment
|
|
167
|
+
class NotifyCustomer
|
|
168
|
+
def call(context)
|
|
169
|
+
NotifyCustomer::Workflow.call(context)
|
|
159
170
|
end
|
|
160
171
|
end
|
|
172
|
+
|
|
173
|
+
class Workflow < Ask::Graph
|
|
174
|
+
step ValidatePayment
|
|
175
|
+
step NotifyCustomer
|
|
176
|
+
step ShipOrder
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
Or use `context.run` for the same thing:
|
|
182
|
+
|
|
183
|
+
```ruby
|
|
184
|
+
class NotifyCustomer
|
|
185
|
+
def call(context)
|
|
186
|
+
context.run(NotifyCustomer::Workflow)
|
|
187
|
+
end
|
|
161
188
|
end
|
|
162
189
|
```
|
|
163
190
|
|
|
164
191
|
### Checkpointing
|
|
165
192
|
|
|
166
|
-
Pass a
|
|
193
|
+
Pass a `storage` backend to make workflows durable across crashes:
|
|
167
194
|
|
|
168
195
|
```ruby
|
|
169
196
|
store = Ask::State::Memory.new # or Redis, SQLite, etc.
|
|
170
197
|
|
|
171
198
|
# Runs through all steps, saving after each
|
|
172
|
-
result =
|
|
199
|
+
result = OrderFulfillment::Workflow.call(input, storage: store)
|
|
173
200
|
|
|
174
201
|
# If a crash occurs, resume from the last completed step
|
|
175
|
-
result =
|
|
202
|
+
result = OrderFulfillment::Workflow.call(input, storage: store)
|
|
176
203
|
```
|
|
177
204
|
|
|
178
205
|
### Error handling
|
|
@@ -180,12 +207,14 @@ result = MyWorkflow.run(input, checkpoint_store: store)
|
|
|
180
207
|
Failed steps raise `Ask::Graph::StepFailed` with the step name:
|
|
181
208
|
|
|
182
209
|
```ruby
|
|
183
|
-
|
|
184
|
-
|
|
210
|
+
module MyWorkflow
|
|
211
|
+
class Workflow < Ask::Graph
|
|
212
|
+
step RiskyOperation
|
|
213
|
+
end
|
|
185
214
|
end
|
|
186
215
|
|
|
187
216
|
begin
|
|
188
|
-
|
|
217
|
+
MyWorkflow::Workflow.call
|
|
189
218
|
rescue Ask::Graph::StepFailed => e
|
|
190
219
|
puts e.message # => "RiskyOperation failed: connection timeout"
|
|
191
220
|
end
|
data/lib/ask/graph/context.rb
CHANGED
|
@@ -131,9 +131,10 @@ module Ask
|
|
|
131
131
|
# @param graph_class [Class < Ask::Graph] the graph to run
|
|
132
132
|
# @return [Ask::Graph::Context] the sub-graph's context
|
|
133
133
|
# @example
|
|
134
|
+
# # Inside a step PORO
|
|
134
135
|
# class CalculateShipping
|
|
135
136
|
# def call(context)
|
|
136
|
-
# context.run(
|
|
137
|
+
# context.run(Shipping::Workflow)
|
|
137
138
|
# end
|
|
138
139
|
# end
|
|
139
140
|
def run(graph_class)
|
data/lib/ask/graph/runner.rb
CHANGED
|
@@ -11,6 +11,9 @@ module Ask
|
|
|
11
11
|
# Error raised when a step times out.
|
|
12
12
|
class StepTimeout < StandardError; end
|
|
13
13
|
|
|
14
|
+
# Error raised when the entire workflow exceeds its total runtime cap.
|
|
15
|
+
class WorkflowTimeout < StandardError; end
|
|
16
|
+
|
|
14
17
|
# Error raised when a step pauses for human approval.
|
|
15
18
|
class Paused < StandardError; end
|
|
16
19
|
|
|
@@ -24,39 +27,30 @@ module Ask
|
|
|
24
27
|
# @param storage [#set, #get, #delete] key-value store for checkpoint data
|
|
25
28
|
# @param hooks [Hash] lifecycle hook method names
|
|
26
29
|
# @param graph_instance [Object] the graph instance (for hooks)
|
|
27
|
-
# @param
|
|
30
|
+
# @param step_timeout [Integer, nil] fallback timeout for steps with no explicit timeout
|
|
31
|
+
# @param workflow_timeout [Integer, nil] total runtime cap for the whole run
|
|
28
32
|
def initialize(declarations, storage: nil,
|
|
29
33
|
hooks: { before_step: [], after_step: [], on_failure: [] },
|
|
30
34
|
graph_instance: nil,
|
|
31
|
-
|
|
35
|
+
step_timeout: nil,
|
|
36
|
+
workflow_timeout: nil)
|
|
32
37
|
@declarations = declarations
|
|
33
38
|
@store = storage
|
|
34
39
|
@hooks = hooks
|
|
35
40
|
@graph = graph_instance
|
|
36
|
-
@
|
|
41
|
+
@step_timeout = step_timeout
|
|
42
|
+
@workflow_timeout = workflow_timeout
|
|
37
43
|
@completed_steps = []
|
|
38
44
|
end
|
|
39
45
|
|
|
40
46
|
def run(context)
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
unless condition_met?(decl, context)
|
|
47
|
-
record_completion(idx, :skipped)
|
|
48
|
-
next
|
|
49
|
-
end
|
|
50
|
-
|
|
51
|
-
run_hooks(:before_step, decl, context)
|
|
52
|
-
execute_with_retry(decl, context)
|
|
53
|
-
run_hooks(:after_step, decl, context)
|
|
54
|
-
|
|
55
|
-
record_completion(idx, :completed)
|
|
56
|
-
save_checkpoint(context, idx)
|
|
47
|
+
if @workflow_timeout
|
|
48
|
+
::Timeout.timeout(@workflow_timeout) { run_steps(context) }
|
|
49
|
+
else
|
|
50
|
+
run_steps(context)
|
|
57
51
|
end
|
|
58
|
-
|
|
59
|
-
|
|
52
|
+
rescue ::Timeout::Error
|
|
53
|
+
raise WorkflowTimeout, "workflow timed out after #{@workflow_timeout}s"
|
|
60
54
|
end
|
|
61
55
|
|
|
62
56
|
def resume(context, input:)
|
|
@@ -77,6 +71,28 @@ module Ask
|
|
|
77
71
|
|
|
78
72
|
private
|
|
79
73
|
|
|
74
|
+
def run_steps(context)
|
|
75
|
+
resume_index = load_checkpoint(context)
|
|
76
|
+
|
|
77
|
+
@declarations.each_with_index do |decl, idx|
|
|
78
|
+
next if resume_index && idx <= resume_index
|
|
79
|
+
|
|
80
|
+
unless condition_met?(decl, context)
|
|
81
|
+
record_completion(idx, :skipped)
|
|
82
|
+
next
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
run_hooks(:before_step, decl, context)
|
|
86
|
+
execute_with_retry(decl, context)
|
|
87
|
+
run_hooks(:after_step, decl, context)
|
|
88
|
+
|
|
89
|
+
record_completion(idx, :completed)
|
|
90
|
+
save_checkpoint(context, idx)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
context
|
|
94
|
+
end
|
|
95
|
+
|
|
80
96
|
def execute_with_retry(decl, context)
|
|
81
97
|
retries = decl[:retry] || 0
|
|
82
98
|
|
|
@@ -93,7 +109,7 @@ module Ask
|
|
|
93
109
|
end
|
|
94
110
|
|
|
95
111
|
def run_with_timeout(decl, context, timeout_value)
|
|
96
|
-
timeout_value ||= @
|
|
112
|
+
timeout_value ||= @step_timeout
|
|
97
113
|
if timeout_value
|
|
98
114
|
::Timeout.timeout(timeout_value) { execute_step(decl, context) }
|
|
99
115
|
else
|
data/lib/ask/graph/version.rb
CHANGED
data/lib/ask/graph.rb
CHANGED
|
@@ -11,28 +11,32 @@ module Ask
|
|
|
11
11
|
# lifecycle hooks.
|
|
12
12
|
#
|
|
13
13
|
# @example Basic workflow
|
|
14
|
-
# class HandleCall < Ask::Graph
|
|
14
|
+
# class HandleCall::Workflow < Ask::Graph
|
|
15
15
|
# step Transcribe
|
|
16
16
|
# step Classify, if: :needs_classification?
|
|
17
17
|
# step BookAppointment
|
|
18
18
|
# end
|
|
19
19
|
#
|
|
20
20
|
# @example Sub-graph composition
|
|
21
|
-
# # Define a workflow
|
|
22
|
-
#
|
|
23
|
-
#
|
|
24
|
-
#
|
|
21
|
+
# # Define a reusable workflow
|
|
22
|
+
# module NotifyCustomer
|
|
23
|
+
# class Workflow < Ask::Graph
|
|
24
|
+
# step SendEmail
|
|
25
|
+
# step LogNotification
|
|
26
|
+
# end
|
|
25
27
|
# end
|
|
26
28
|
#
|
|
27
29
|
# # Wrap it in a PORO step
|
|
28
|
-
#
|
|
29
|
-
#
|
|
30
|
-
#
|
|
30
|
+
# module OrderFulfillment
|
|
31
|
+
# class NotifyCustomer
|
|
32
|
+
# def call(context)
|
|
33
|
+
# NotifyCustomer::Workflow.call(context)
|
|
34
|
+
# end
|
|
31
35
|
# end
|
|
32
36
|
# end
|
|
33
37
|
#
|
|
34
38
|
# # Compose — every step is a PORO
|
|
35
|
-
# class
|
|
39
|
+
# class OrderFulfillment::Workflow < Ask::Graph
|
|
36
40
|
# step ValidatePayment
|
|
37
41
|
# step NotifyCustomer
|
|
38
42
|
# step ShipOrder
|
|
@@ -65,39 +69,72 @@ module Ask
|
|
|
65
69
|
def inherited(subclass)
|
|
66
70
|
super
|
|
67
71
|
subclass.instance_variable_set(:@declarations, declarations.dup)
|
|
68
|
-
subclass.instance_variable_set(:@lifecycle_hooks,
|
|
72
|
+
subclass.instance_variable_set(:@lifecycle_hooks,
|
|
73
|
+
lifecycle_hooks.transform_values(&:dup))
|
|
69
74
|
end
|
|
70
75
|
|
|
71
|
-
# ---
|
|
76
|
+
# --- Timeouts ---
|
|
72
77
|
|
|
73
|
-
# Set or get the default timeout (in seconds) for
|
|
74
|
-
# Steps without an explicit +timeout:+ option
|
|
78
|
+
# Set or get the default timeout (in seconds) for each step in this graph.
|
|
79
|
+
# Steps without an explicit +timeout:+ option inherit this value.
|
|
75
80
|
# Setting on {Ask::Graph} itself applies to all graphs that don't set
|
|
76
81
|
# their own default — a global default.
|
|
77
82
|
#
|
|
78
83
|
# @example Global default for all graphs
|
|
79
|
-
# Ask::Graph.
|
|
84
|
+
# Ask::Graph.default_step_timeout 30
|
|
80
85
|
#
|
|
81
86
|
# @example Per-graph override
|
|
82
87
|
# class MyGraph < Ask::Graph
|
|
83
|
-
#
|
|
88
|
+
# step_timeout 15
|
|
84
89
|
# step FastOp # uses 15s
|
|
85
90
|
# step SlowOp, timeout: 60 # overrides
|
|
86
91
|
# end
|
|
87
92
|
#
|
|
88
93
|
# @param seconds [Integer, nil] timeout in seconds, or nil to clear
|
|
89
|
-
# @return [Integer, nil] the current default timeout for this graph
|
|
90
|
-
def
|
|
94
|
+
# @return [Integer, nil] the current default step timeout for this graph
|
|
95
|
+
def step_timeout(seconds = :not_set)
|
|
96
|
+
if seconds == :not_set
|
|
97
|
+
if instance_variable_defined?(:@step_timeout)
|
|
98
|
+
@step_timeout
|
|
99
|
+
elsif superclass.respond_to?(:step_timeout)
|
|
100
|
+
superclass.step_timeout
|
|
101
|
+
end
|
|
102
|
+
else
|
|
103
|
+
@step_timeout = seconds
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
alias default_step_timeout step_timeout
|
|
107
|
+
|
|
108
|
+
# Set or get the total runtime cap (in seconds) for the entire graph.
|
|
109
|
+
# The whole workflow aborts with {Ask::Graph::WorkflowTimeout} if it
|
|
110
|
+
# exceeds this limit, regardless of individual step timeouts.
|
|
111
|
+
# Setting on {Ask::Graph} itself applies to all graphs that don't set
|
|
112
|
+
# their own default — a global default.
|
|
113
|
+
#
|
|
114
|
+
# @example Global default for all graphs
|
|
115
|
+
# Ask::Graph.default_workflow_timeout 60
|
|
116
|
+
#
|
|
117
|
+
# @example Per-graph override
|
|
118
|
+
# class MyGraph < Ask::Graph
|
|
119
|
+
# workflow_timeout 30
|
|
120
|
+
# step FetchData, timeout: 10
|
|
121
|
+
# step ProcessData, timeout: 10
|
|
122
|
+
# end
|
|
123
|
+
#
|
|
124
|
+
# @param seconds [Integer, nil] timeout in seconds, or nil to clear
|
|
125
|
+
# @return [Integer, nil] the current workflow timeout for this graph
|
|
126
|
+
def workflow_timeout(seconds = :not_set)
|
|
91
127
|
if seconds == :not_set
|
|
92
|
-
if instance_variable_defined?(:@
|
|
93
|
-
@
|
|
94
|
-
elsif superclass.respond_to?(:
|
|
95
|
-
superclass.
|
|
128
|
+
if instance_variable_defined?(:@workflow_timeout)
|
|
129
|
+
@workflow_timeout
|
|
130
|
+
elsif superclass.respond_to?(:workflow_timeout)
|
|
131
|
+
superclass.workflow_timeout
|
|
96
132
|
end
|
|
97
133
|
else
|
|
98
|
-
@
|
|
134
|
+
@workflow_timeout = seconds
|
|
99
135
|
end
|
|
100
136
|
end
|
|
137
|
+
alias default_workflow_timeout workflow_timeout
|
|
101
138
|
|
|
102
139
|
# --- Storage ---
|
|
103
140
|
|
|
@@ -203,12 +240,12 @@ module Ask
|
|
|
203
240
|
# are merged back into the original context automatically.
|
|
204
241
|
#
|
|
205
242
|
# @example Standalone — pass data
|
|
206
|
-
#
|
|
243
|
+
# OrderFulfillment::Workflow.call({ user: "alice" })
|
|
207
244
|
#
|
|
208
245
|
# @example Inside a step — pass the outer context
|
|
209
|
-
# class
|
|
246
|
+
# class ValidatePayment
|
|
210
247
|
# def call(context)
|
|
211
|
-
#
|
|
248
|
+
# PaymentGateway::Workflow.call(context)
|
|
212
249
|
# end
|
|
213
250
|
# end
|
|
214
251
|
def call(input = nil, storage: nil)
|
|
@@ -246,7 +283,8 @@ module Ask
|
|
|
246
283
|
storage: store,
|
|
247
284
|
hooks: hooks,
|
|
248
285
|
graph_instance: self,
|
|
249
|
-
|
|
286
|
+
step_timeout: self.class.step_timeout,
|
|
287
|
+
workflow_timeout: self.class.workflow_timeout)
|
|
250
288
|
@context = nil
|
|
251
289
|
end
|
|
252
290
|
|