fractor 0.1.6 → 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.
Files changed (172) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop_todo.yml +227 -102
  3. data/README.adoc +113 -1940
  4. data/docs/.lycheeignore +16 -0
  5. data/docs/Gemfile +24 -0
  6. data/docs/README.md +157 -0
  7. data/docs/_config.yml +151 -0
  8. data/docs/_features/error-handling.adoc +1192 -0
  9. data/docs/_features/index.adoc +80 -0
  10. data/docs/_features/monitoring.adoc +589 -0
  11. data/docs/_features/signal-handling.adoc +202 -0
  12. data/docs/_features/workflows.adoc +1235 -0
  13. data/docs/_guides/continuous-mode.adoc +736 -0
  14. data/docs/_guides/cookbook.adoc +1133 -0
  15. data/docs/_guides/index.adoc +55 -0
  16. data/docs/_guides/pipeline-mode.adoc +730 -0
  17. data/docs/_guides/troubleshooting.adoc +358 -0
  18. data/docs/_pages/architecture.adoc +1390 -0
  19. data/docs/_pages/core-concepts.adoc +1392 -0
  20. data/docs/_pages/design-principles.adoc +862 -0
  21. data/docs/_pages/getting-started.adoc +290 -0
  22. data/docs/_pages/installation.adoc +143 -0
  23. data/docs/_reference/api.adoc +1080 -0
  24. data/docs/_reference/error-reporting.adoc +670 -0
  25. data/docs/_reference/examples.adoc +181 -0
  26. data/docs/_reference/index.adoc +96 -0
  27. data/docs/_reference/troubleshooting.adoc +862 -0
  28. data/docs/_tutorials/complex-workflows.adoc +1022 -0
  29. data/docs/_tutorials/data-processing-pipeline.adoc +740 -0
  30. data/docs/_tutorials/first-application.adoc +384 -0
  31. data/docs/_tutorials/index.adoc +48 -0
  32. data/docs/_tutorials/long-running-services.adoc +931 -0
  33. data/docs/assets/images/favicon-16.png +0 -0
  34. data/docs/assets/images/favicon-32.png +0 -0
  35. data/docs/assets/images/favicon-48.png +0 -0
  36. data/docs/assets/images/favicon.ico +0 -0
  37. data/docs/assets/images/favicon.png +0 -0
  38. data/docs/assets/images/favicon.svg +45 -0
  39. data/docs/assets/images/fractor-icon.svg +49 -0
  40. data/docs/assets/images/fractor-logo.svg +61 -0
  41. data/docs/index.adoc +131 -0
  42. data/docs/lychee.toml +39 -0
  43. data/examples/api_aggregator/README.adoc +627 -0
  44. data/examples/api_aggregator/api_aggregator.rb +376 -0
  45. data/examples/auto_detection/README.adoc +407 -29
  46. data/examples/continuous_chat_common/message_protocol.rb +1 -1
  47. data/examples/error_reporting.rb +207 -0
  48. data/examples/file_processor/README.adoc +170 -0
  49. data/examples/file_processor/file_processor.rb +615 -0
  50. data/examples/file_processor/sample_files/invalid.csv +1 -0
  51. data/examples/file_processor/sample_files/orders.xml +24 -0
  52. data/examples/file_processor/sample_files/products.json +23 -0
  53. data/examples/file_processor/sample_files/users.csv +6 -0
  54. data/examples/hierarchical_hasher/README.adoc +629 -41
  55. data/examples/image_processor/README.adoc +610 -0
  56. data/examples/image_processor/image_processor.rb +349 -0
  57. data/examples/image_processor/processed_images/sample_10_processed.jpg.json +12 -0
  58. data/examples/image_processor/processed_images/sample_1_processed.jpg.json +12 -0
  59. data/examples/image_processor/processed_images/sample_2_processed.jpg.json +12 -0
  60. data/examples/image_processor/processed_images/sample_3_processed.jpg.json +12 -0
  61. data/examples/image_processor/processed_images/sample_4_processed.jpg.json +12 -0
  62. data/examples/image_processor/processed_images/sample_5_processed.jpg.json +12 -0
  63. data/examples/image_processor/processed_images/sample_6_processed.jpg.json +12 -0
  64. data/examples/image_processor/processed_images/sample_7_processed.jpg.json +12 -0
  65. data/examples/image_processor/processed_images/sample_8_processed.jpg.json +12 -0
  66. data/examples/image_processor/processed_images/sample_9_processed.jpg.json +12 -0
  67. data/examples/image_processor/test_images/sample_1.png +1 -0
  68. data/examples/image_processor/test_images/sample_10.png +1 -0
  69. data/examples/image_processor/test_images/sample_2.png +1 -0
  70. data/examples/image_processor/test_images/sample_3.png +1 -0
  71. data/examples/image_processor/test_images/sample_4.png +1 -0
  72. data/examples/image_processor/test_images/sample_5.png +1 -0
  73. data/examples/image_processor/test_images/sample_6.png +1 -0
  74. data/examples/image_processor/test_images/sample_7.png +1 -0
  75. data/examples/image_processor/test_images/sample_8.png +1 -0
  76. data/examples/image_processor/test_images/sample_9.png +1 -0
  77. data/examples/log_analyzer/README.adoc +662 -0
  78. data/examples/log_analyzer/log_analyzer.rb +579 -0
  79. data/examples/log_analyzer/sample_logs/apache.log +20 -0
  80. data/examples/log_analyzer/sample_logs/json.log +15 -0
  81. data/examples/log_analyzer/sample_logs/nginx.log +15 -0
  82. data/examples/log_analyzer/sample_logs/rails.log +29 -0
  83. data/examples/multi_work_type/README.adoc +576 -26
  84. data/examples/performance_monitoring.rb +120 -0
  85. data/examples/pipeline_processing/README.adoc +740 -26
  86. data/examples/pipeline_processing/pipeline_processing.rb +2 -2
  87. data/examples/priority_work_example.rb +155 -0
  88. data/examples/producer_subscriber/README.adoc +889 -46
  89. data/examples/scatter_gather/README.adoc +829 -27
  90. data/examples/simple/README.adoc +347 -0
  91. data/examples/specialized_workers/README.adoc +622 -26
  92. data/examples/specialized_workers/specialized_workers.rb +44 -8
  93. data/examples/stream_processor/README.adoc +206 -0
  94. data/examples/stream_processor/stream_processor.rb +284 -0
  95. data/examples/web_scraper/README.adoc +625 -0
  96. data/examples/web_scraper/web_scraper.rb +285 -0
  97. data/examples/workflow/README.adoc +406 -0
  98. data/examples/workflow/circuit_breaker/README.adoc +360 -0
  99. data/examples/workflow/circuit_breaker/circuit_breaker_workflow.rb +225 -0
  100. data/examples/workflow/conditional/README.adoc +483 -0
  101. data/examples/workflow/conditional/conditional_workflow.rb +215 -0
  102. data/examples/workflow/dead_letter_queue/README.adoc +374 -0
  103. data/examples/workflow/dead_letter_queue/dead_letter_queue_workflow.rb +217 -0
  104. data/examples/workflow/fan_out/README.adoc +381 -0
  105. data/examples/workflow/fan_out/fan_out_workflow.rb +202 -0
  106. data/examples/workflow/retry/README.adoc +248 -0
  107. data/examples/workflow/retry/retry_workflow.rb +195 -0
  108. data/examples/workflow/simple_linear/README.adoc +267 -0
  109. data/examples/workflow/simple_linear/simple_linear_workflow.rb +175 -0
  110. data/examples/workflow/simplified/README.adoc +329 -0
  111. data/examples/workflow/simplified/simplified_workflow.rb +222 -0
  112. data/exe/fractor +10 -0
  113. data/lib/fractor/cli.rb +288 -0
  114. data/lib/fractor/configuration.rb +307 -0
  115. data/lib/fractor/continuous_server.rb +60 -65
  116. data/lib/fractor/error_formatter.rb +72 -0
  117. data/lib/fractor/error_report_generator.rb +152 -0
  118. data/lib/fractor/error_reporter.rb +244 -0
  119. data/lib/fractor/error_statistics.rb +147 -0
  120. data/lib/fractor/execution_tracer.rb +162 -0
  121. data/lib/fractor/logger.rb +230 -0
  122. data/lib/fractor/main_loop_handler.rb +406 -0
  123. data/lib/fractor/main_loop_handler3.rb +135 -0
  124. data/lib/fractor/main_loop_handler4.rb +299 -0
  125. data/lib/fractor/performance_metrics_collector.rb +181 -0
  126. data/lib/fractor/performance_monitor.rb +215 -0
  127. data/lib/fractor/performance_report_generator.rb +202 -0
  128. data/lib/fractor/priority_work.rb +93 -0
  129. data/lib/fractor/priority_work_queue.rb +189 -0
  130. data/lib/fractor/result_aggregator.rb +32 -0
  131. data/lib/fractor/shutdown_handler.rb +168 -0
  132. data/lib/fractor/signal_handler.rb +80 -0
  133. data/lib/fractor/supervisor.rb +382 -269
  134. data/lib/fractor/supervisor_logger.rb +88 -0
  135. data/lib/fractor/version.rb +1 -1
  136. data/lib/fractor/work.rb +12 -0
  137. data/lib/fractor/work_distribution_manager.rb +151 -0
  138. data/lib/fractor/work_queue.rb +20 -0
  139. data/lib/fractor/work_result.rb +181 -9
  140. data/lib/fractor/worker.rb +73 -0
  141. data/lib/fractor/workflow/builder.rb +210 -0
  142. data/lib/fractor/workflow/chain_builder.rb +169 -0
  143. data/lib/fractor/workflow/circuit_breaker.rb +183 -0
  144. data/lib/fractor/workflow/circuit_breaker_orchestrator.rb +208 -0
  145. data/lib/fractor/workflow/circuit_breaker_registry.rb +112 -0
  146. data/lib/fractor/workflow/dead_letter_queue.rb +334 -0
  147. data/lib/fractor/workflow/execution_hooks.rb +39 -0
  148. data/lib/fractor/workflow/execution_strategy.rb +225 -0
  149. data/lib/fractor/workflow/execution_trace.rb +134 -0
  150. data/lib/fractor/workflow/helpers.rb +191 -0
  151. data/lib/fractor/workflow/job.rb +290 -0
  152. data/lib/fractor/workflow/job_dependency_validator.rb +120 -0
  153. data/lib/fractor/workflow/logger.rb +110 -0
  154. data/lib/fractor/workflow/pre_execution_context.rb +193 -0
  155. data/lib/fractor/workflow/retry_config.rb +156 -0
  156. data/lib/fractor/workflow/retry_orchestrator.rb +184 -0
  157. data/lib/fractor/workflow/retry_strategy.rb +93 -0
  158. data/lib/fractor/workflow/structured_logger.rb +30 -0
  159. data/lib/fractor/workflow/type_compatibility_validator.rb +222 -0
  160. data/lib/fractor/workflow/visualizer.rb +211 -0
  161. data/lib/fractor/workflow/workflow_context.rb +132 -0
  162. data/lib/fractor/workflow/workflow_executor.rb +669 -0
  163. data/lib/fractor/workflow/workflow_result.rb +55 -0
  164. data/lib/fractor/workflow/workflow_validator.rb +295 -0
  165. data/lib/fractor/workflow.rb +333 -0
  166. data/lib/fractor/wrapped_ractor.rb +66 -101
  167. data/lib/fractor/wrapped_ractor3.rb +161 -0
  168. data/lib/fractor/wrapped_ractor4.rb +242 -0
  169. data/lib/fractor.rb +92 -4
  170. metadata +179 -6
  171. data/tests/sample.rb.bak +0 -309
  172. 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/]