fractor 0.1.4 → 0.1.7
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/.rubocop-https---raw-githubusercontent-com-riboseinc-oss-guides-main-ci-rubocop-yml +552 -0
- data/.rubocop.yml +14 -8
- data/.rubocop_todo.yml +284 -43
- data/README.adoc +111 -950
- data/docs/.lycheeignore +16 -0
- data/docs/Gemfile +24 -0
- data/docs/README.md +157 -0
- data/docs/_config.yml +151 -0
- data/docs/_features/error-handling.adoc +1192 -0
- data/docs/_features/index.adoc +80 -0
- data/docs/_features/monitoring.adoc +589 -0
- data/docs/_features/signal-handling.adoc +202 -0
- data/docs/_features/workflows.adoc +1235 -0
- data/docs/_guides/continuous-mode.adoc +736 -0
- data/docs/_guides/cookbook.adoc +1133 -0
- data/docs/_guides/index.adoc +55 -0
- data/docs/_guides/pipeline-mode.adoc +730 -0
- data/docs/_guides/troubleshooting.adoc +358 -0
- data/docs/_pages/architecture.adoc +1390 -0
- data/docs/_pages/core-concepts.adoc +1392 -0
- data/docs/_pages/design-principles.adoc +862 -0
- data/docs/_pages/getting-started.adoc +290 -0
- data/docs/_pages/installation.adoc +143 -0
- data/docs/_reference/api.adoc +1080 -0
- data/docs/_reference/error-reporting.adoc +670 -0
- data/docs/_reference/examples.adoc +181 -0
- data/docs/_reference/index.adoc +96 -0
- data/docs/_reference/troubleshooting.adoc +862 -0
- data/docs/_tutorials/complex-workflows.adoc +1022 -0
- data/docs/_tutorials/data-processing-pipeline.adoc +740 -0
- data/docs/_tutorials/first-application.adoc +384 -0
- data/docs/_tutorials/index.adoc +48 -0
- data/docs/_tutorials/long-running-services.adoc +931 -0
- data/docs/assets/images/favicon-16.png +0 -0
- data/docs/assets/images/favicon-32.png +0 -0
- data/docs/assets/images/favicon-48.png +0 -0
- data/docs/assets/images/favicon.ico +0 -0
- data/docs/assets/images/favicon.png +0 -0
- data/docs/assets/images/favicon.svg +45 -0
- data/docs/assets/images/fractor-icon.svg +49 -0
- data/docs/assets/images/fractor-logo.svg +61 -0
- data/docs/index.adoc +131 -0
- data/docs/lychee.toml +39 -0
- data/examples/api_aggregator/README.adoc +627 -0
- data/examples/api_aggregator/api_aggregator.rb +376 -0
- data/examples/auto_detection/README.adoc +407 -29
- data/examples/auto_detection/auto_detection.rb +9 -9
- data/examples/continuous_chat_common/message_protocol.rb +53 -0
- data/examples/continuous_chat_fractor/README.adoc +217 -0
- data/examples/continuous_chat_fractor/chat_client.rb +303 -0
- data/examples/continuous_chat_fractor/chat_common.rb +83 -0
- data/examples/continuous_chat_fractor/chat_server.rb +167 -0
- data/examples/continuous_chat_fractor/simulate.rb +345 -0
- data/examples/continuous_chat_server/README.adoc +135 -0
- data/examples/continuous_chat_server/chat_client.rb +303 -0
- data/examples/continuous_chat_server/chat_server.rb +359 -0
- data/examples/continuous_chat_server/simulate.rb +343 -0
- data/examples/error_reporting.rb +207 -0
- data/examples/file_processor/README.adoc +170 -0
- data/examples/file_processor/file_processor.rb +615 -0
- data/examples/file_processor/sample_files/invalid.csv +1 -0
- data/examples/file_processor/sample_files/orders.xml +24 -0
- data/examples/file_processor/sample_files/products.json +23 -0
- data/examples/file_processor/sample_files/users.csv +6 -0
- data/examples/hierarchical_hasher/README.adoc +629 -41
- data/examples/hierarchical_hasher/hierarchical_hasher.rb +12 -8
- data/examples/image_processor/README.adoc +610 -0
- data/examples/image_processor/image_processor.rb +349 -0
- data/examples/image_processor/processed_images/sample_10_processed.jpg.json +12 -0
- data/examples/image_processor/processed_images/sample_1_processed.jpg.json +12 -0
- data/examples/image_processor/processed_images/sample_2_processed.jpg.json +12 -0
- data/examples/image_processor/processed_images/sample_3_processed.jpg.json +12 -0
- data/examples/image_processor/processed_images/sample_4_processed.jpg.json +12 -0
- data/examples/image_processor/processed_images/sample_5_processed.jpg.json +12 -0
- data/examples/image_processor/processed_images/sample_6_processed.jpg.json +12 -0
- data/examples/image_processor/processed_images/sample_7_processed.jpg.json +12 -0
- data/examples/image_processor/processed_images/sample_8_processed.jpg.json +12 -0
- data/examples/image_processor/processed_images/sample_9_processed.jpg.json +12 -0
- data/examples/image_processor/test_images/sample_1.png +1 -0
- data/examples/image_processor/test_images/sample_10.png +1 -0
- data/examples/image_processor/test_images/sample_2.png +1 -0
- data/examples/image_processor/test_images/sample_3.png +1 -0
- data/examples/image_processor/test_images/sample_4.png +1 -0
- data/examples/image_processor/test_images/sample_5.png +1 -0
- data/examples/image_processor/test_images/sample_6.png +1 -0
- data/examples/image_processor/test_images/sample_7.png +1 -0
- data/examples/image_processor/test_images/sample_8.png +1 -0
- data/examples/image_processor/test_images/sample_9.png +1 -0
- data/examples/log_analyzer/README.adoc +662 -0
- data/examples/log_analyzer/log_analyzer.rb +579 -0
- data/examples/log_analyzer/sample_logs/apache.log +20 -0
- data/examples/log_analyzer/sample_logs/json.log +15 -0
- data/examples/log_analyzer/sample_logs/nginx.log +15 -0
- data/examples/log_analyzer/sample_logs/rails.log +29 -0
- data/examples/multi_work_type/README.adoc +576 -26
- data/examples/multi_work_type/multi_work_type.rb +30 -29
- data/examples/performance_monitoring.rb +120 -0
- data/examples/pipeline_processing/README.adoc +740 -26
- data/examples/pipeline_processing/pipeline_processing.rb +16 -16
- data/examples/priority_work_example.rb +155 -0
- data/examples/producer_subscriber/README.adoc +889 -46
- data/examples/producer_subscriber/producer_subscriber.rb +20 -16
- data/examples/scatter_gather/README.adoc +829 -27
- data/examples/scatter_gather/scatter_gather.rb +29 -28
- data/examples/simple/README.adoc +347 -0
- data/examples/simple/sample.rb +5 -5
- data/examples/specialized_workers/README.adoc +622 -26
- data/examples/specialized_workers/specialized_workers.rb +88 -45
- data/examples/stream_processor/README.adoc +206 -0
- data/examples/stream_processor/stream_processor.rb +284 -0
- data/examples/web_scraper/README.adoc +625 -0
- data/examples/web_scraper/web_scraper.rb +285 -0
- data/examples/workflow/README.adoc +406 -0
- data/examples/workflow/circuit_breaker/README.adoc +360 -0
- data/examples/workflow/circuit_breaker/circuit_breaker_workflow.rb +225 -0
- data/examples/workflow/conditional/README.adoc +483 -0
- data/examples/workflow/conditional/conditional_workflow.rb +215 -0
- data/examples/workflow/dead_letter_queue/README.adoc +374 -0
- data/examples/workflow/dead_letter_queue/dead_letter_queue_workflow.rb +217 -0
- data/examples/workflow/fan_out/README.adoc +381 -0
- data/examples/workflow/fan_out/fan_out_workflow.rb +202 -0
- data/examples/workflow/retry/README.adoc +248 -0
- data/examples/workflow/retry/retry_workflow.rb +195 -0
- data/examples/workflow/simple_linear/README.adoc +267 -0
- data/examples/workflow/simple_linear/simple_linear_workflow.rb +175 -0
- data/examples/workflow/simplified/README.adoc +329 -0
- data/examples/workflow/simplified/simplified_workflow.rb +222 -0
- data/exe/fractor +10 -0
- data/lib/fractor/cli.rb +288 -0
- data/lib/fractor/configuration.rb +307 -0
- data/lib/fractor/continuous_server.rb +183 -0
- data/lib/fractor/error_formatter.rb +72 -0
- data/lib/fractor/error_report_generator.rb +152 -0
- data/lib/fractor/error_reporter.rb +244 -0
- data/lib/fractor/error_statistics.rb +147 -0
- data/lib/fractor/execution_tracer.rb +162 -0
- data/lib/fractor/logger.rb +230 -0
- data/lib/fractor/main_loop_handler.rb +406 -0
- data/lib/fractor/main_loop_handler3.rb +135 -0
- data/lib/fractor/main_loop_handler4.rb +299 -0
- data/lib/fractor/performance_metrics_collector.rb +181 -0
- data/lib/fractor/performance_monitor.rb +215 -0
- data/lib/fractor/performance_report_generator.rb +202 -0
- data/lib/fractor/priority_work.rb +93 -0
- data/lib/fractor/priority_work_queue.rb +189 -0
- data/lib/fractor/result_aggregator.rb +33 -1
- data/lib/fractor/shutdown_handler.rb +168 -0
- data/lib/fractor/signal_handler.rb +80 -0
- data/lib/fractor/supervisor.rb +430 -144
- data/lib/fractor/supervisor_logger.rb +88 -0
- data/lib/fractor/version.rb +1 -1
- data/lib/fractor/work.rb +12 -0
- data/lib/fractor/work_distribution_manager.rb +151 -0
- data/lib/fractor/work_queue.rb +88 -0
- data/lib/fractor/work_result.rb +181 -9
- data/lib/fractor/worker.rb +75 -1
- data/lib/fractor/workflow/builder.rb +210 -0
- data/lib/fractor/workflow/chain_builder.rb +169 -0
- data/lib/fractor/workflow/circuit_breaker.rb +183 -0
- data/lib/fractor/workflow/circuit_breaker_orchestrator.rb +208 -0
- data/lib/fractor/workflow/circuit_breaker_registry.rb +112 -0
- data/lib/fractor/workflow/dead_letter_queue.rb +334 -0
- data/lib/fractor/workflow/execution_hooks.rb +39 -0
- data/lib/fractor/workflow/execution_strategy.rb +225 -0
- data/lib/fractor/workflow/execution_trace.rb +134 -0
- data/lib/fractor/workflow/helpers.rb +191 -0
- data/lib/fractor/workflow/job.rb +290 -0
- data/lib/fractor/workflow/job_dependency_validator.rb +120 -0
- data/lib/fractor/workflow/logger.rb +110 -0
- data/lib/fractor/workflow/pre_execution_context.rb +193 -0
- data/lib/fractor/workflow/retry_config.rb +156 -0
- data/lib/fractor/workflow/retry_orchestrator.rb +184 -0
- data/lib/fractor/workflow/retry_strategy.rb +93 -0
- data/lib/fractor/workflow/structured_logger.rb +30 -0
- data/lib/fractor/workflow/type_compatibility_validator.rb +222 -0
- data/lib/fractor/workflow/visualizer.rb +211 -0
- data/lib/fractor/workflow/workflow_context.rb +132 -0
- data/lib/fractor/workflow/workflow_executor.rb +669 -0
- data/lib/fractor/workflow/workflow_result.rb +55 -0
- data/lib/fractor/workflow/workflow_validator.rb +295 -0
- data/lib/fractor/workflow.rb +333 -0
- data/lib/fractor/wrapped_ractor.rb +66 -91
- data/lib/fractor/wrapped_ractor3.rb +161 -0
- data/lib/fractor/wrapped_ractor4.rb +242 -0
- data/lib/fractor.rb +93 -3
- metadata +192 -6
- data/tests/sample.rb.bak +0 -309
- data/tests/sample_working.rb.bak +0 -209
|
@@ -0,0 +1,1022 @@
|
|
|
1
|
+
---
|
|
2
|
+
layout: default
|
|
3
|
+
title: Implementing Complex Workflows
|
|
4
|
+
nav_order: 7
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
== Implementing Complex Workflows
|
|
8
|
+
|
|
9
|
+
=== Overview
|
|
10
|
+
|
|
11
|
+
In this 45-minute advanced tutorial, you'll build a sophisticated data processing workflow with conditional execution, retry logic, circuit breakers, and error recovery. This demonstrates Fractor's workflow system for production-grade applications.
|
|
12
|
+
|
|
13
|
+
**What you'll learn:**
|
|
14
|
+
|
|
15
|
+
* Building multi-stage workflows with dependencies
|
|
16
|
+
* Implementing retry logic with different backoff strategies
|
|
17
|
+
* Using circuit breakers to prevent cascading failures
|
|
18
|
+
* Handling errors with Dead Letter Queues
|
|
19
|
+
* Creating conditional workflows
|
|
20
|
+
* Monitoring workflow execution
|
|
21
|
+
* Testing workflow patterns
|
|
22
|
+
|
|
23
|
+
**Prerequisites:**
|
|
24
|
+
|
|
25
|
+
* Completed link:getting-started[Getting Started] and link:data-processing-pipeline[Data Processing Pipeline] tutorials
|
|
26
|
+
* Understanding of link:../guides/core-concepts[Core Concepts]
|
|
27
|
+
* Familiarity with link:../guides/workflows[Workflows]
|
|
28
|
+
|
|
29
|
+
=== The Problem
|
|
30
|
+
|
|
31
|
+
You need to build an order fulfillment workflow that:
|
|
32
|
+
|
|
33
|
+
1. **Validate Order**: Check inventory, pricing, customer credit
|
|
34
|
+
2. **Payment Processing**: Charge customer with retry and fallback
|
|
35
|
+
3. **Inventory Reservation**: Reserve items with circuit breaker protection
|
|
36
|
+
4. **Shipping**: Create shipping label (conditional on payment success)
|
|
37
|
+
5. **Notification**: Send confirmations with retry logic
|
|
38
|
+
6. **Audit**: Log all steps for compliance
|
|
39
|
+
|
|
40
|
+
The workflow must handle:
|
|
41
|
+
|
|
42
|
+
* External API failures (payment, shipping)
|
|
43
|
+
* Inventory shortages
|
|
44
|
+
* Network timeouts
|
|
45
|
+
* Partial failures
|
|
46
|
+
* Concurrent order processing
|
|
47
|
+
|
|
48
|
+
=== Step 1: Set Up the Project
|
|
49
|
+
|
|
50
|
+
Create the project:
|
|
51
|
+
|
|
52
|
+
[source,sh]
|
|
53
|
+
----
|
|
54
|
+
mkdir order_fulfillment
|
|
55
|
+
cd order_fulfillment
|
|
56
|
+
mkdir -p lib spec
|
|
57
|
+
----
|
|
58
|
+
|
|
59
|
+
Create `Gemfile`:
|
|
60
|
+
|
|
61
|
+
[source,ruby]
|
|
62
|
+
----
|
|
63
|
+
source 'https://rubygems.org'
|
|
64
|
+
|
|
65
|
+
gem 'fractor'
|
|
66
|
+
gem 'rspec'
|
|
67
|
+
----
|
|
68
|
+
|
|
69
|
+
Install:
|
|
70
|
+
|
|
71
|
+
[source,sh]
|
|
72
|
+
----
|
|
73
|
+
bundle install
|
|
74
|
+
----
|
|
75
|
+
|
|
76
|
+
=== Step 2: Define Data Models
|
|
77
|
+
|
|
78
|
+
Create `lib/models.rb`:
|
|
79
|
+
|
|
80
|
+
[source,ruby]
|
|
81
|
+
----
|
|
82
|
+
# Order input
|
|
83
|
+
class Order
|
|
84
|
+
attr_accessor :order_id, :customer_id, :items, :total, :shipping_address
|
|
85
|
+
|
|
86
|
+
def initialize(attrs = {})
|
|
87
|
+
@order_id = attrs[:order_id]
|
|
88
|
+
@customer_id = attrs[:customer_id]
|
|
89
|
+
@items = attrs[:items] || []
|
|
90
|
+
@total = attrs[:total]
|
|
91
|
+
@shipping_address = attrs[:shipping_address]
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def to_h
|
|
95
|
+
{
|
|
96
|
+
order_id: @order_id,
|
|
97
|
+
customer_id: @customer_id,
|
|
98
|
+
items: @items,
|
|
99
|
+
total: @total,
|
|
100
|
+
shipping_address: @shipping_address
|
|
101
|
+
}
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Validation result
|
|
106
|
+
class ValidationResult
|
|
107
|
+
attr_accessor :valid, :errors, :warnings, :order
|
|
108
|
+
|
|
109
|
+
def initialize(valid:, errors: [], warnings: [], order:)
|
|
110
|
+
@valid = valid
|
|
111
|
+
@errors = errors
|
|
112
|
+
@warnings = warnings
|
|
113
|
+
@order = order
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def valid?
|
|
117
|
+
@valid
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# Payment result
|
|
122
|
+
class PaymentResult
|
|
123
|
+
attr_accessor :success, :transaction_id, :amount, :payment_method
|
|
124
|
+
|
|
125
|
+
def initialize(attrs = {})
|
|
126
|
+
@success = attrs[:success]
|
|
127
|
+
@transaction_id = attrs[:transaction_id]
|
|
128
|
+
@amount = attrs[:amount]
|
|
129
|
+
@payment_method = attrs[:payment_method] || 'primary'
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def success?
|
|
133
|
+
@success
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# Inventory result
|
|
138
|
+
class InventoryResult
|
|
139
|
+
attr_accessor :reserved, :reservation_ids, :items
|
|
140
|
+
|
|
141
|
+
def initialize(attrs = {})
|
|
142
|
+
@reserved = attrs[:reserved]
|
|
143
|
+
@reservation_ids = attrs[:reservation_ids] || []
|
|
144
|
+
@items = attrs[:items] || []
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def reserved?
|
|
148
|
+
@reserved
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# Shipping result
|
|
153
|
+
class ShippingResult
|
|
154
|
+
attr_accessor :tracking_number, :carrier, :estimated_delivery
|
|
155
|
+
|
|
156
|
+
def initialize(attrs = {})
|
|
157
|
+
@tracking_number = attrs[:tracking_number]
|
|
158
|
+
@carrier = attrs[:carrier]
|
|
159
|
+
@estimated_delivery = attrs[:estimated_delivery]
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# Final fulfillment result
|
|
164
|
+
class FulfillmentResult
|
|
165
|
+
attr_accessor :order_id, :status, :payment, :inventory, :shipping, :timestamp
|
|
166
|
+
|
|
167
|
+
def initialize(attrs = {})
|
|
168
|
+
@order_id = attrs[:order_id]
|
|
169
|
+
@status = attrs[:status]
|
|
170
|
+
@payment = attrs[:payment]
|
|
171
|
+
@inventory = attrs[:inventory]
|
|
172
|
+
@shipping = attrs[:shipping]
|
|
173
|
+
@timestamp = Time.now
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def to_h
|
|
177
|
+
{
|
|
178
|
+
order_id: @order_id,
|
|
179
|
+
status: @status,
|
|
180
|
+
payment: @payment,
|
|
181
|
+
inventory: @inventory,
|
|
182
|
+
shipping: @shipping,
|
|
183
|
+
timestamp: @timestamp
|
|
184
|
+
}
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
----
|
|
188
|
+
|
|
189
|
+
=== Step 3: Create Workflow Workers
|
|
190
|
+
|
|
191
|
+
Create `lib/workers.rb`:
|
|
192
|
+
|
|
193
|
+
[source,ruby]
|
|
194
|
+
----
|
|
195
|
+
require 'fractor'
|
|
196
|
+
require_relative 'models'
|
|
197
|
+
|
|
198
|
+
# Step 1: Validate order
|
|
199
|
+
class ValidateOrderWorker < Fractor::Worker
|
|
200
|
+
def process(work)
|
|
201
|
+
order = work.input
|
|
202
|
+
errors = []
|
|
203
|
+
warnings = []
|
|
204
|
+
|
|
205
|
+
# Inventory check
|
|
206
|
+
order.items.each do |item|
|
|
207
|
+
if item[:quantity] > available_inventory(item[:sku])
|
|
208
|
+
errors << "Insufficient inventory for #{item[:sku]}"
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
# Credit check
|
|
213
|
+
if customer_credit_limit(order.customer_id) < order.total
|
|
214
|
+
warnings << "Order exceeds normal credit limit"
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
# Price validation
|
|
218
|
+
if order.total < 0
|
|
219
|
+
errors << "Invalid total amount"
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
result = ValidationResult.new(
|
|
223
|
+
valid: errors.empty?,
|
|
224
|
+
errors: errors,
|
|
225
|
+
warnings: warnings,
|
|
226
|
+
order: order
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
Fractor::WorkResult.new(result: result, work: work)
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
private
|
|
233
|
+
|
|
234
|
+
def available_inventory(sku)
|
|
235
|
+
# Simulate inventory check
|
|
236
|
+
rand(10..100)
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
def customer_credit_limit(customer_id)
|
|
240
|
+
# Simulate credit check
|
|
241
|
+
10000
|
|
242
|
+
end
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
# Step 2: Process payment with retry and fallback
|
|
246
|
+
class ProcessPaymentWorker < Fractor::Worker
|
|
247
|
+
def process(work)
|
|
248
|
+
order = work.input[:order]
|
|
249
|
+
|
|
250
|
+
# Simulate payment processing
|
|
251
|
+
sleep(0.1 + rand * 0.2)
|
|
252
|
+
|
|
253
|
+
# Simulate occasional failures
|
|
254
|
+
if rand < 0.15
|
|
255
|
+
raise Net::HTTPRetriableError.new("Payment gateway timeout", nil)
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
result = PaymentResult.new(
|
|
259
|
+
success: true,
|
|
260
|
+
transaction_id: "TXN-#{SecureRandom.hex(8)}",
|
|
261
|
+
amount: order.total,
|
|
262
|
+
payment_method: 'primary'
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
Fractor::WorkResult.new(result: result, work: work)
|
|
266
|
+
rescue => e
|
|
267
|
+
Fractor::WorkResult.new(
|
|
268
|
+
error: e,
|
|
269
|
+
error_code: :payment_failed,
|
|
270
|
+
error_context: { order_id: order.order_id, amount: order.total },
|
|
271
|
+
work: work
|
|
272
|
+
)
|
|
273
|
+
end
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
# Fallback payment worker (uses backup payment method)
|
|
277
|
+
class ProcessPaymentFallbackWorker < Fractor::Worker
|
|
278
|
+
def process(work)
|
|
279
|
+
order = work.input[:order]
|
|
280
|
+
|
|
281
|
+
sleep(0.1)
|
|
282
|
+
|
|
283
|
+
result = PaymentResult.new(
|
|
284
|
+
success: true,
|
|
285
|
+
transaction_id: "TXN-FALLBACK-#{SecureRandom.hex(8)}",
|
|
286
|
+
amount: order.total,
|
|
287
|
+
payment_method: 'backup'
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
Fractor::WorkResult.new(result: result, work: work)
|
|
291
|
+
end
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
# Step 3: Reserve inventory with circuit breaker
|
|
295
|
+
class ReserveInventoryWorker < Fractor::Worker
|
|
296
|
+
def process(work)
|
|
297
|
+
order = work.input[:order]
|
|
298
|
+
|
|
299
|
+
sleep(0.05 + rand * 0.1)
|
|
300
|
+
|
|
301
|
+
# Simulate occasional service issues
|
|
302
|
+
if rand < 0.1
|
|
303
|
+
raise Errno::ECONNREFUSED.new("Inventory service unavailable")
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
reservation_ids = order.items.map do |item|
|
|
307
|
+
"RES-#{SecureRandom.hex(6)}"
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
result = InventoryResult.new(
|
|
311
|
+
reserved: true,
|
|
312
|
+
reservation_ids: reservation_ids,
|
|
313
|
+
items: order.items
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
Fractor::WorkResult.new(result: result, work: work)
|
|
317
|
+
rescue => e
|
|
318
|
+
Fractor::WorkResult.new(
|
|
319
|
+
error: e,
|
|
320
|
+
error_code: :inventory_failed,
|
|
321
|
+
error_context: { order_id: order.order_id },
|
|
322
|
+
work: work
|
|
323
|
+
)
|
|
324
|
+
end
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
# Step 4: Create shipping label
|
|
328
|
+
class CreateShippingWorker < Fractor::Worker
|
|
329
|
+
def process(work)
|
|
330
|
+
order = work.input[:order]
|
|
331
|
+
payment = work.input[:payment]
|
|
332
|
+
|
|
333
|
+
# Only ship if payment succeeded
|
|
334
|
+
unless payment.success?
|
|
335
|
+
return Fractor::WorkResult.new(
|
|
336
|
+
error: "Cannot ship without successful payment",
|
|
337
|
+
error_code: :shipping_skipped,
|
|
338
|
+
work: work
|
|
339
|
+
)
|
|
340
|
+
end
|
|
341
|
+
|
|
342
|
+
sleep(0.1 + rand * 0.15)
|
|
343
|
+
|
|
344
|
+
result = ShippingResult.new(
|
|
345
|
+
tracking_number: "TRACK-#{SecureRandom.hex(10)}",
|
|
346
|
+
carrier: 'FedEx',
|
|
347
|
+
estimated_delivery: Date.today + 3
|
|
348
|
+
)
|
|
349
|
+
|
|
350
|
+
Fractor::WorkResult.new(result: result, work: work)
|
|
351
|
+
rescue => e
|
|
352
|
+
Fractor::WorkResult.new(
|
|
353
|
+
error: e,
|
|
354
|
+
error_code: :shipping_failed,
|
|
355
|
+
error_context: { order_id: order.order_id },
|
|
356
|
+
work: work
|
|
357
|
+
)
|
|
358
|
+
end
|
|
359
|
+
end
|
|
360
|
+
|
|
361
|
+
# Step 5: Send notifications
|
|
362
|
+
class SendNotificationWorker < Fractor::Worker
|
|
363
|
+
def process(work)
|
|
364
|
+
fulfillment = work.input
|
|
365
|
+
|
|
366
|
+
sleep(0.05)
|
|
367
|
+
|
|
368
|
+
# Send email notification
|
|
369
|
+
send_email(fulfillment)
|
|
370
|
+
|
|
371
|
+
# Send SMS if configured
|
|
372
|
+
send_sms(fulfillment) if fulfillment[:send_sms]
|
|
373
|
+
|
|
374
|
+
Fractor::WorkResult.new(
|
|
375
|
+
result: { notified: true, channels: ['email'] },
|
|
376
|
+
work: work
|
|
377
|
+
)
|
|
378
|
+
rescue => e
|
|
379
|
+
Fractor::WorkResult.new(
|
|
380
|
+
error: e,
|
|
381
|
+
error_code: :notification_failed,
|
|
382
|
+
error_context: { order_id: fulfillment[:order_id] },
|
|
383
|
+
work: work
|
|
384
|
+
)
|
|
385
|
+
end
|
|
386
|
+
|
|
387
|
+
private
|
|
388
|
+
|
|
389
|
+
def send_email(fulfillment)
|
|
390
|
+
# Simulate email sending
|
|
391
|
+
puts " 📧 Sent order confirmation for #{fulfillment[:order_id]}"
|
|
392
|
+
end
|
|
393
|
+
|
|
394
|
+
def send_sms(fulfillment)
|
|
395
|
+
# Simulate SMS sending
|
|
396
|
+
puts " 📱 Sent SMS notification for #{fulfillment[:order_id]}"
|
|
397
|
+
end
|
|
398
|
+
end
|
|
399
|
+
|
|
400
|
+
# Step 6: Audit logging
|
|
401
|
+
class AuditLogWorker < Fractor::Worker
|
|
402
|
+
def process(work)
|
|
403
|
+
fulfillment = work.input
|
|
404
|
+
|
|
405
|
+
# Log to audit system
|
|
406
|
+
log_audit_event(fulfillment)
|
|
407
|
+
|
|
408
|
+
Fractor::WorkResult.new(
|
|
409
|
+
result: FulfillmentResult.new(fulfillment),
|
|
410
|
+
work: work
|
|
411
|
+
)
|
|
412
|
+
end
|
|
413
|
+
|
|
414
|
+
private
|
|
415
|
+
|
|
416
|
+
def log_audit_event(fulfillment)
|
|
417
|
+
# In production: write to audit log, compliance system
|
|
418
|
+
puts " 📝 Logged audit event for order #{fulfillment[:order_id]}"
|
|
419
|
+
end
|
|
420
|
+
end
|
|
421
|
+
----
|
|
422
|
+
|
|
423
|
+
=== Step 4: Build the Workflow
|
|
424
|
+
|
|
425
|
+
Create `lib/order_workflow.rb`:
|
|
426
|
+
|
|
427
|
+
[source,ruby]
|
|
428
|
+
----
|
|
429
|
+
require 'fractor'
|
|
430
|
+
require_relative 'models'
|
|
431
|
+
require_relative 'workers'
|
|
432
|
+
|
|
433
|
+
class OrderFulfillmentWorkflow < Fractor::Workflow
|
|
434
|
+
workflow "order-fulfillment" do
|
|
435
|
+
input_type Order
|
|
436
|
+
output_type FulfillmentResult
|
|
437
|
+
|
|
438
|
+
# Configure Dead Letter Queue for permanently failed orders
|
|
439
|
+
configure_dead_letter_queue(
|
|
440
|
+
max_size: 1000,
|
|
441
|
+
on_add: lambda { |entry|
|
|
442
|
+
puts "⚠️ Order #{entry.metadata[:order_id]} added to DLQ"
|
|
443
|
+
puts " Reason: #{entry.error.message}"
|
|
444
|
+
}
|
|
445
|
+
)
|
|
446
|
+
|
|
447
|
+
# Step 1: Validate the order
|
|
448
|
+
job "validate" do
|
|
449
|
+
runs_with ValidateOrderWorker
|
|
450
|
+
inputs_from_workflow
|
|
451
|
+
|
|
452
|
+
on_error do |error, context|
|
|
453
|
+
puts "Validation error: #{error.message}"
|
|
454
|
+
end
|
|
455
|
+
end
|
|
456
|
+
|
|
457
|
+
# Step 2: Process payment with retry and fallback
|
|
458
|
+
job "payment" do
|
|
459
|
+
needs "validate"
|
|
460
|
+
runs_with ProcessPaymentWorker
|
|
461
|
+
inputs_from_job "validate"
|
|
462
|
+
|
|
463
|
+
# Retry up to 5 times with exponential backoff
|
|
464
|
+
retry_on_error max_attempts: 5,
|
|
465
|
+
backoff: :exponential,
|
|
466
|
+
initial_delay: 1,
|
|
467
|
+
max_delay: 30,
|
|
468
|
+
retryable_errors: [Net::HTTPRetriableError, Timeout::Error]
|
|
469
|
+
|
|
470
|
+
# Use backup payment method if primary fails
|
|
471
|
+
fallback_to "payment_fallback"
|
|
472
|
+
|
|
473
|
+
on_error do |error, context|
|
|
474
|
+
puts "Payment attempt failed: #{error.message}"
|
|
475
|
+
puts "Attempt #{context.metadata[:attempt]} of 5"
|
|
476
|
+
end
|
|
477
|
+
end
|
|
478
|
+
|
|
479
|
+
# Fallback payment processing
|
|
480
|
+
job "payment_fallback" do
|
|
481
|
+
runs_with ProcessPaymentFallbackWorker
|
|
482
|
+
inputs_from_job "validate"
|
|
483
|
+
|
|
484
|
+
on_error do |error, context|
|
|
485
|
+
puts "Backup payment also failed: #{error.message}"
|
|
486
|
+
end
|
|
487
|
+
end
|
|
488
|
+
|
|
489
|
+
# Step 3: Reserve inventory with circuit breaker
|
|
490
|
+
job "inventory" do
|
|
491
|
+
needs "payment"
|
|
492
|
+
runs_with ReserveInventoryWorker
|
|
493
|
+
inputs_from_job "validate"
|
|
494
|
+
|
|
495
|
+
# Circuit breaker: open after 5 failures, stay open for 60s
|
|
496
|
+
circuit_breaker threshold: 5,
|
|
497
|
+
timeout: 60,
|
|
498
|
+
half_open_calls: 3,
|
|
499
|
+
shared_key: "inventory_service"
|
|
500
|
+
|
|
501
|
+
# Retry with linear backoff
|
|
502
|
+
retry_on_error max_attempts: 3,
|
|
503
|
+
backoff: :linear,
|
|
504
|
+
increment: 2,
|
|
505
|
+
retryable_errors: [Errno::ECONNREFUSED, Errno::ETIMEDOUT]
|
|
506
|
+
|
|
507
|
+
on_error do |error, context|
|
|
508
|
+
if error.is_a?(Fractor::Workflow::CircuitOpenError)
|
|
509
|
+
puts "⚡ Circuit breaker open for inventory service"
|
|
510
|
+
else
|
|
511
|
+
puts "Inventory reservation failed: #{error.message}"
|
|
512
|
+
end
|
|
513
|
+
end
|
|
514
|
+
end
|
|
515
|
+
|
|
516
|
+
# Step 4: Create shipping label (conditional on payment success)
|
|
517
|
+
job "shipping" do
|
|
518
|
+
needs "payment", "inventory"
|
|
519
|
+
runs_with CreateShippingWorker
|
|
520
|
+
|
|
521
|
+
# Combine inputs from multiple jobs
|
|
522
|
+
inputs_from_jobs "validate", "payment"
|
|
523
|
+
|
|
524
|
+
retry_on_error max_attempts: 3,
|
|
525
|
+
backoff: :exponential
|
|
526
|
+
end
|
|
527
|
+
|
|
528
|
+
# Step 5: Send notifications
|
|
529
|
+
job "notify" do
|
|
530
|
+
needs "shipping"
|
|
531
|
+
runs_with SendNotificationWorker
|
|
532
|
+
|
|
533
|
+
# Build notification payload
|
|
534
|
+
inputs_from_jobs "validate", "payment", "shipping"
|
|
535
|
+
|
|
536
|
+
# Retry notifications
|
|
537
|
+
retry_on_error max_attempts: 3,
|
|
538
|
+
backoff: :constant,
|
|
539
|
+
delay: 5
|
|
540
|
+
end
|
|
541
|
+
|
|
542
|
+
# Step 6: Audit logging (always runs)
|
|
543
|
+
job "audit" do
|
|
544
|
+
needs "notify"
|
|
545
|
+
runs_with AuditLogWorker
|
|
546
|
+
inputs_from_jobs "validate", "payment", "inventory", "shipping"
|
|
547
|
+
outputs_to_workflow
|
|
548
|
+
terminates_workflow
|
|
549
|
+
end
|
|
550
|
+
end
|
|
551
|
+
end
|
|
552
|
+
----
|
|
553
|
+
|
|
554
|
+
=== Step 5: Add Workflow Execution
|
|
555
|
+
|
|
556
|
+
Create `lib/workflow_executor.rb`:
|
|
557
|
+
|
|
558
|
+
[source,ruby]
|
|
559
|
+
----
|
|
560
|
+
require_relative 'order_workflow'
|
|
561
|
+
require_relative 'models'
|
|
562
|
+
|
|
563
|
+
class OrderProcessor
|
|
564
|
+
def initialize
|
|
565
|
+
@stats = {
|
|
566
|
+
total: 0,
|
|
567
|
+
succeeded: 0,
|
|
568
|
+
failed: 0,
|
|
569
|
+
in_dlq: 0
|
|
570
|
+
}
|
|
571
|
+
end
|
|
572
|
+
|
|
573
|
+
def process_order(order)
|
|
574
|
+
@stats[:total] += 1
|
|
575
|
+
|
|
576
|
+
puts "\n" + "=" * 60
|
|
577
|
+
puts "Processing Order #{order.order_id}"
|
|
578
|
+
puts "=" * 60
|
|
579
|
+
puts "Customer: #{order.customer_id}"
|
|
580
|
+
puts "Total: $#{order.total}"
|
|
581
|
+
puts "Items: #{order.items.size}"
|
|
582
|
+
puts ""
|
|
583
|
+
|
|
584
|
+
workflow = OrderFulfillmentWorkflow.new
|
|
585
|
+
|
|
586
|
+
begin
|
|
587
|
+
# Execute the workflow
|
|
588
|
+
result = workflow.execute(order)
|
|
589
|
+
|
|
590
|
+
@stats[:succeeded] += 1
|
|
591
|
+
|
|
592
|
+
puts "\n✓ Order #{order.order_id} fulfilled successfully"
|
|
593
|
+
puts " Status: #{result.status}"
|
|
594
|
+
puts " Payment: #{result.payment[:transaction_id]}"
|
|
595
|
+
puts " Tracking: #{result.shipping[:tracking_number]}"
|
|
596
|
+
|
|
597
|
+
result
|
|
598
|
+
rescue Fractor::Workflow::WorkflowExecutionError => e
|
|
599
|
+
@stats[:failed] += 1
|
|
600
|
+
|
|
601
|
+
puts "\n✗ Order #{order.order_id} failed"
|
|
602
|
+
puts " Error: #{e.message}"
|
|
603
|
+
|
|
604
|
+
# Check Dead Letter Queue
|
|
605
|
+
check_dlq(workflow)
|
|
606
|
+
|
|
607
|
+
nil
|
|
608
|
+
end
|
|
609
|
+
end
|
|
610
|
+
|
|
611
|
+
def process_batch(orders)
|
|
612
|
+
puts "\nProcessing batch of #{orders.size} orders...\n"
|
|
613
|
+
|
|
614
|
+
results = orders.map { |order| process_order(order) }
|
|
615
|
+
|
|
616
|
+
print_statistics
|
|
617
|
+
|
|
618
|
+
results
|
|
619
|
+
end
|
|
620
|
+
|
|
621
|
+
private
|
|
622
|
+
|
|
623
|
+
def check_dlq(workflow)
|
|
624
|
+
dlq = workflow.dead_letter_queue
|
|
625
|
+
return unless dlq && dlq.size > 0
|
|
626
|
+
|
|
627
|
+
@stats[:in_dlq] += 1
|
|
628
|
+
|
|
629
|
+
puts "\n DLQ Status:"
|
|
630
|
+
puts " Total entries: #{dlq.size}"
|
|
631
|
+
|
|
632
|
+
# Show recent failures
|
|
633
|
+
dlq.all.last(3).each do |entry|
|
|
634
|
+
puts " - #{entry.error.class.name}: #{entry.error.message}"
|
|
635
|
+
end
|
|
636
|
+
end
|
|
637
|
+
|
|
638
|
+
def print_statistics
|
|
639
|
+
puts "\n" + "=" * 60
|
|
640
|
+
puts "Batch Processing Complete"
|
|
641
|
+
puts "=" * 60
|
|
642
|
+
puts "Total orders: #{@stats[:total]}"
|
|
643
|
+
puts "Succeeded: #{@stats[:succeeded]}"
|
|
644
|
+
puts "Failed: #{@stats[:failed]}"
|
|
645
|
+
puts "In DLQ: #{@stats[:in_dlq]}"
|
|
646
|
+
puts "Success rate: #{success_rate}%"
|
|
647
|
+
puts "=" * 60
|
|
648
|
+
end
|
|
649
|
+
|
|
650
|
+
def success_rate
|
|
651
|
+
return 0 if @stats[:total] == 0
|
|
652
|
+
((@stats[:succeeded].to_f / @stats[:total]) * 100).round(2)
|
|
653
|
+
end
|
|
654
|
+
end
|
|
655
|
+
----
|
|
656
|
+
|
|
657
|
+
=== Step 6: Create Test Suite
|
|
658
|
+
|
|
659
|
+
Create `spec/order_workflow_spec.rb`:
|
|
660
|
+
|
|
661
|
+
[source,ruby]
|
|
662
|
+
----
|
|
663
|
+
require 'rspec'
|
|
664
|
+
require_relative '../lib/order_workflow'
|
|
665
|
+
require_relative '../lib/workflow_executor'
|
|
666
|
+
|
|
667
|
+
RSpec.describe OrderFulfillmentWorkflow do
|
|
668
|
+
let(:valid_order) do
|
|
669
|
+
Order.new(
|
|
670
|
+
order_id: 'ORD-001',
|
|
671
|
+
customer_id: 'CUST-123',
|
|
672
|
+
items: [
|
|
673
|
+
{ sku: 'WIDGET-A', quantity: 2, price: 10.00 },
|
|
674
|
+
{ sku: 'GADGET-B', quantity: 1, price: 25.00 }
|
|
675
|
+
],
|
|
676
|
+
total: 45.00,
|
|
677
|
+
shipping_address: {
|
|
678
|
+
street: '123 Main St',
|
|
679
|
+
city: 'Portland',
|
|
680
|
+
state: 'OR',
|
|
681
|
+
zip: '97201'
|
|
682
|
+
}
|
|
683
|
+
)
|
|
684
|
+
end
|
|
685
|
+
|
|
686
|
+
describe "successful workflow" do
|
|
687
|
+
it "processes valid order through all stages" do
|
|
688
|
+
workflow = OrderFulfillmentWorkflow.new
|
|
689
|
+
result = workflow.execute(valid_order)
|
|
690
|
+
|
|
691
|
+
expect(result).to be_a(FulfillmentResult)
|
|
692
|
+
expect(result.order_id).to eq('ORD-001')
|
|
693
|
+
expect(result.status).to_not be_nil
|
|
694
|
+
end
|
|
695
|
+
end
|
|
696
|
+
|
|
697
|
+
describe "validation stage" do
|
|
698
|
+
it "validates order data" do
|
|
699
|
+
order = valid_order
|
|
700
|
+
workflow = OrderFulfillmentWorkflow.new
|
|
701
|
+
|
|
702
|
+
result = workflow.execute(order)
|
|
703
|
+
expect(result).to_not be_nil
|
|
704
|
+
end
|
|
705
|
+
end
|
|
706
|
+
|
|
707
|
+
describe "retry logic" do
|
|
708
|
+
it "retries on transient failures" do
|
|
709
|
+
# Test by observing retry behavior in logs
|
|
710
|
+
processor = OrderProcessor.new
|
|
711
|
+
result = processor.process_order(valid_order)
|
|
712
|
+
|
|
713
|
+
expect(result).to_not be_nil
|
|
714
|
+
end
|
|
715
|
+
end
|
|
716
|
+
|
|
717
|
+
describe "circuit breaker" do
|
|
718
|
+
it "opens circuit after threshold failures" do
|
|
719
|
+
# Would need to inject failures to test properly
|
|
720
|
+
# This is a placeholder for circuit breaker testing
|
|
721
|
+
expect(true).to be true
|
|
722
|
+
end
|
|
723
|
+
end
|
|
724
|
+
|
|
725
|
+
describe "dead letter queue" do
|
|
726
|
+
it "captures permanently failed orders" do
|
|
727
|
+
processor = OrderProcessor.new
|
|
728
|
+
|
|
729
|
+
# Create intentionally failing order
|
|
730
|
+
bad_order = valid_order
|
|
731
|
+
bad_order.total = -100 # Invalid
|
|
732
|
+
|
|
733
|
+
result = processor.process_order(bad_order)
|
|
734
|
+
# Result might be nil for failed orders
|
|
735
|
+
end
|
|
736
|
+
end
|
|
737
|
+
end
|
|
738
|
+
----
|
|
739
|
+
|
|
740
|
+
=== Step 7: Run the Workflow
|
|
741
|
+
|
|
742
|
+
Create `run_workflow.rb`:
|
|
743
|
+
|
|
744
|
+
[source,ruby]
|
|
745
|
+
----
|
|
746
|
+
require_relative 'lib/workflow_executor'
|
|
747
|
+
require_relative 'lib/models'
|
|
748
|
+
|
|
749
|
+
# Create test orders
|
|
750
|
+
orders = [
|
|
751
|
+
Order.new(
|
|
752
|
+
order_id: 'ORD-001',
|
|
753
|
+
customer_id: 'CUST-001',
|
|
754
|
+
items: [{ sku: 'WIDGET-A', quantity: 2, price: 10.00 }],
|
|
755
|
+
total: 20.00,
|
|
756
|
+
shipping_address: { city: 'Portland', state: 'OR' }
|
|
757
|
+
),
|
|
758
|
+
Order.new(
|
|
759
|
+
order_id: 'ORD-002',
|
|
760
|
+
customer_id: 'CUST-002',
|
|
761
|
+
items: [{ sku: 'GADGET-B', quantity: 1, price: 50.00 }],
|
|
762
|
+
total: 50.00,
|
|
763
|
+
shipping_address: { city: 'Seattle', state: 'WA' }
|
|
764
|
+
),
|
|
765
|
+
Order.new(
|
|
766
|
+
order_id: 'ORD-003',
|
|
767
|
+
customer_id: 'CUST-003',
|
|
768
|
+
items: [{ sku: 'WIDGET-C', quantity: 5, price: 15.00 }],
|
|
769
|
+
total: 75.00,
|
|
770
|
+
shipping_address: { city: 'San Francisco', state: 'CA' }
|
|
771
|
+
)
|
|
772
|
+
]
|
|
773
|
+
|
|
774
|
+
# Process orders
|
|
775
|
+
processor = OrderProcessor.new
|
|
776
|
+
processor.process_batch(orders)
|
|
777
|
+
----
|
|
778
|
+
|
|
779
|
+
Run it:
|
|
780
|
+
|
|
781
|
+
[source,sh]
|
|
782
|
+
----
|
|
783
|
+
ruby run_workflow.rb
|
|
784
|
+
----
|
|
785
|
+
|
|
786
|
+
Run tests:
|
|
787
|
+
|
|
788
|
+
[source,sh]
|
|
789
|
+
----
|
|
790
|
+
bundle exec rspec spec/order_workflow_spec.rb
|
|
791
|
+
----
|
|
792
|
+
|
|
793
|
+
=== Step 8: Add Workflow Visualization
|
|
794
|
+
|
|
795
|
+
Create `visualize_workflow.rb`:
|
|
796
|
+
|
|
797
|
+
[source,ruby]
|
|
798
|
+
----
|
|
799
|
+
require_relative 'lib/order_workflow'
|
|
800
|
+
require 'fractor/workflow/visualizer'
|
|
801
|
+
|
|
802
|
+
workflow = OrderFulfillmentWorkflow.new
|
|
803
|
+
|
|
804
|
+
visualizer = Fractor::Workflow::Visualizer.new(workflow)
|
|
805
|
+
|
|
806
|
+
# Generate Mermaid diagram
|
|
807
|
+
puts "Mermaid Diagram:"
|
|
808
|
+
puts "=" * 60
|
|
809
|
+
puts visualizer.to_mermaid
|
|
810
|
+
puts ""
|
|
811
|
+
|
|
812
|
+
# Generate ASCII diagram
|
|
813
|
+
puts "ASCII Diagram:"
|
|
814
|
+
puts "=" * 60
|
|
815
|
+
puts visualizer.to_ascii
|
|
816
|
+
puts ""
|
|
817
|
+
|
|
818
|
+
# Save DOT format for Graphviz
|
|
819
|
+
File.write('workflow.dot', visualizer.to_dot)
|
|
820
|
+
puts "Graphviz DOT saved to workflow.dot"
|
|
821
|
+
puts "Generate image: dot -Tpng workflow.dot -o workflow.png"
|
|
822
|
+
----
|
|
823
|
+
|
|
824
|
+
=== Best Practices Demonstrated
|
|
825
|
+
|
|
826
|
+
==== 1. Layered Error Handling
|
|
827
|
+
|
|
828
|
+
Multiple levels of protection:
|
|
829
|
+
|
|
830
|
+
[source,ruby]
|
|
831
|
+
----
|
|
832
|
+
# Layer 1: Retry for transient failures
|
|
833
|
+
retry_on_error max_attempts: 5, backoff: :exponential
|
|
834
|
+
|
|
835
|
+
# Layer 2: Circuit breaker for sustained failures
|
|
836
|
+
circuit_breaker threshold: 5, timeout: 60
|
|
837
|
+
|
|
838
|
+
# Layer 3: Fallback for when all else fails
|
|
839
|
+
fallback_to "payment_fallback"
|
|
840
|
+
|
|
841
|
+
# Layer 4: Dead Letter Queue for permanent failures
|
|
842
|
+
configure_dead_letter_queue max_size: 1000
|
|
843
|
+
----
|
|
844
|
+
|
|
845
|
+
==== 2. Selective Retry
|
|
846
|
+
|
|
847
|
+
Only retry specific error types:
|
|
848
|
+
|
|
849
|
+
[source,ruby]
|
|
850
|
+
----
|
|
851
|
+
retry_on_error retryable_errors: [
|
|
852
|
+
Net::HTTPRetriableError,
|
|
853
|
+
Timeout::Error,
|
|
854
|
+
Errno::ECONNREFUSED
|
|
855
|
+
]
|
|
856
|
+
----
|
|
857
|
+
|
|
858
|
+
==== 3. Backoff Strategies
|
|
859
|
+
|
|
860
|
+
Choose appropriate backoff for each job:
|
|
861
|
+
|
|
862
|
+
* **Exponential**: For external APIs (prevents hammering)
|
|
863
|
+
* **Linear**: For resource contention
|
|
864
|
+
* **Constant**: For notification retries
|
|
865
|
+
|
|
866
|
+
==== 4. Circuit Breaker Sharing
|
|
867
|
+
|
|
868
|
+
Share circuit breakers across related jobs:
|
|
869
|
+
|
|
870
|
+
[source,ruby]
|
|
871
|
+
----
|
|
872
|
+
# Multiple jobs can share the same circuit
|
|
873
|
+
circuit_breaker shared_key: "inventory_service"
|
|
874
|
+
----
|
|
875
|
+
|
|
876
|
+
==== 5. Comprehensive Monitoring
|
|
877
|
+
|
|
878
|
+
Track workflow execution:
|
|
879
|
+
|
|
880
|
+
[source,ruby]
|
|
881
|
+
----
|
|
882
|
+
trace = workflow.execution_trace
|
|
883
|
+
trace.jobs.each do |job_id, job_trace|
|
|
884
|
+
puts "#{job_id}: #{job_trace.duration}s (#{job_trace.status})"
|
|
885
|
+
end
|
|
886
|
+
----
|
|
887
|
+
|
|
888
|
+
=== Advanced Patterns
|
|
889
|
+
|
|
890
|
+
==== 1. Conditional Execution
|
|
891
|
+
|
|
892
|
+
Create jobs that run conditionally:
|
|
893
|
+
|
|
894
|
+
[source,ruby]
|
|
895
|
+
----
|
|
896
|
+
job "premium_handling" do
|
|
897
|
+
runs_with PremiumHandlingWorker
|
|
898
|
+
|
|
899
|
+
# Only run for premium customers
|
|
900
|
+
condition ->(context) { context.input.customer_tier == 'premium' }
|
|
901
|
+
end
|
|
902
|
+
----
|
|
903
|
+
|
|
904
|
+
==== 2. Fan-Out/Fan-In
|
|
905
|
+
|
|
906
|
+
Parallel processing with aggregation:
|
|
907
|
+
|
|
908
|
+
[source,ruby]
|
|
909
|
+
----
|
|
910
|
+
# Fan out to multiple jobs
|
|
911
|
+
job "validate_inventory" do
|
|
912
|
+
runs_with InventoryWorker
|
|
913
|
+
end
|
|
914
|
+
|
|
915
|
+
job "validate_pricing" do
|
|
916
|
+
runs_with PricingWorker
|
|
917
|
+
end
|
|
918
|
+
|
|
919
|
+
# Fan in: aggregate results
|
|
920
|
+
job "combine_validation" do
|
|
921
|
+
needs "validate_inventory", "validate_pricing"
|
|
922
|
+
runs_with AggregationWorker
|
|
923
|
+
end
|
|
924
|
+
----
|
|
925
|
+
|
|
926
|
+
==== 3. Workflow Composition
|
|
927
|
+
|
|
928
|
+
Reuse workflows:
|
|
929
|
+
|
|
930
|
+
[source,ruby]
|
|
931
|
+
----
|
|
932
|
+
class ParentWorkflow < Fractor::Workflow
|
|
933
|
+
workflow "parent" do
|
|
934
|
+
job "sub_workflow" do
|
|
935
|
+
runs_with SubWorkflowWorker
|
|
936
|
+
# SubWorkflowWorker internally runs another workflow
|
|
937
|
+
end
|
|
938
|
+
end
|
|
939
|
+
end
|
|
940
|
+
----
|
|
941
|
+
|
|
942
|
+
=== Production Deployment
|
|
943
|
+
|
|
944
|
+
==== Monitoring
|
|
945
|
+
|
|
946
|
+
Add comprehensive monitoring:
|
|
947
|
+
|
|
948
|
+
[source,ruby]
|
|
949
|
+
----
|
|
950
|
+
workflow = OrderFulfillmentWorkflow.new
|
|
951
|
+
|
|
952
|
+
# Before execution
|
|
953
|
+
monitor = Fractor::PerformanceMonitor.new(workflow.supervisor)
|
|
954
|
+
monitor.start
|
|
955
|
+
|
|
956
|
+
# Execute
|
|
957
|
+
result = workflow.execute(order)
|
|
958
|
+
|
|
959
|
+
# After execution
|
|
960
|
+
puts monitor.report
|
|
961
|
+
File.write('metrics.json', monitor.to_json)
|
|
962
|
+
monitor.stop
|
|
963
|
+
----
|
|
964
|
+
|
|
965
|
+
==== Error Tracking
|
|
966
|
+
|
|
967
|
+
Integrate with error tracking services:
|
|
968
|
+
|
|
969
|
+
[source,ruby]
|
|
970
|
+
----
|
|
971
|
+
on_error do |error, context|
|
|
972
|
+
Sentry.capture_exception(error, extra: context.to_h)
|
|
973
|
+
Datadog.increment('workflow.errors', tags: ["job:#{context.job_id}"])
|
|
974
|
+
end
|
|
975
|
+
----
|
|
976
|
+
|
|
977
|
+
==== DLQ Processing
|
|
978
|
+
|
|
979
|
+
Process DLQ entries:
|
|
980
|
+
|
|
981
|
+
[source,ruby]
|
|
982
|
+
----
|
|
983
|
+
# Manual DLQ review and retry
|
|
984
|
+
dlq = workflow.dead_letter_queue
|
|
985
|
+
|
|
986
|
+
dlq.all.each do |entry|
|
|
987
|
+
puts "Failed: #{entry.error.message}"
|
|
988
|
+
|
|
989
|
+
# Fix issue and retry
|
|
990
|
+
if should_retry?(entry)
|
|
991
|
+
workflow.execute(entry.work.input)
|
|
992
|
+
end
|
|
993
|
+
end
|
|
994
|
+
----
|
|
995
|
+
|
|
996
|
+
=== Summary
|
|
997
|
+
|
|
998
|
+
You've built a production-ready workflow with:
|
|
999
|
+
|
|
1000
|
+
✓ Multi-stage processing with dependencies
|
|
1001
|
+
✓ Retry logic with multiple backoff strategies
|
|
1002
|
+
✓ Circuit breakers for service protection
|
|
1003
|
+
✓ Fallback jobs for resilience
|
|
1004
|
+
✓ Dead Letter Queue for failure tracking
|
|
1005
|
+
✓ Comprehensive error handling
|
|
1006
|
+
✓ Monitoring and visualization
|
|
1007
|
+
|
|
1008
|
+
**Key takeaways:**
|
|
1009
|
+
|
|
1010
|
+
1. Layer error handling (retry → circuit breaker → fallback → DLQ)
|
|
1011
|
+
2. Choose appropriate backoff strategies per job type
|
|
1012
|
+
3. Use circuit breakers for external service calls
|
|
1013
|
+
4. Implement fallback paths for critical operations
|
|
1014
|
+
5. Monitor workflow execution and failures
|
|
1015
|
+
6. Test all failure scenarios
|
|
1016
|
+
|
|
1017
|
+
=== Next Steps
|
|
1018
|
+
|
|
1019
|
+
* Explore link:../guides/workflows[Workflows Guide] for more patterns
|
|
1020
|
+
* Check link:../reference/api[API Reference] for complete workflow DSL
|
|
1021
|
+
* Review link:../reference/error-reporting[Error Reporting] for production monitoring
|
|
1022
|
+
* See workflow examples in link:../../examples/workflow/[examples/workflow/]
|