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.
Files changed (189) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop-https---raw-githubusercontent-com-riboseinc-oss-guides-main-ci-rubocop-yml +552 -0
  3. data/.rubocop.yml +14 -8
  4. data/.rubocop_todo.yml +284 -43
  5. data/README.adoc +111 -950
  6. data/docs/.lycheeignore +16 -0
  7. data/docs/Gemfile +24 -0
  8. data/docs/README.md +157 -0
  9. data/docs/_config.yml +151 -0
  10. data/docs/_features/error-handling.adoc +1192 -0
  11. data/docs/_features/index.adoc +80 -0
  12. data/docs/_features/monitoring.adoc +589 -0
  13. data/docs/_features/signal-handling.adoc +202 -0
  14. data/docs/_features/workflows.adoc +1235 -0
  15. data/docs/_guides/continuous-mode.adoc +736 -0
  16. data/docs/_guides/cookbook.adoc +1133 -0
  17. data/docs/_guides/index.adoc +55 -0
  18. data/docs/_guides/pipeline-mode.adoc +730 -0
  19. data/docs/_guides/troubleshooting.adoc +358 -0
  20. data/docs/_pages/architecture.adoc +1390 -0
  21. data/docs/_pages/core-concepts.adoc +1392 -0
  22. data/docs/_pages/design-principles.adoc +862 -0
  23. data/docs/_pages/getting-started.adoc +290 -0
  24. data/docs/_pages/installation.adoc +143 -0
  25. data/docs/_reference/api.adoc +1080 -0
  26. data/docs/_reference/error-reporting.adoc +670 -0
  27. data/docs/_reference/examples.adoc +181 -0
  28. data/docs/_reference/index.adoc +96 -0
  29. data/docs/_reference/troubleshooting.adoc +862 -0
  30. data/docs/_tutorials/complex-workflows.adoc +1022 -0
  31. data/docs/_tutorials/data-processing-pipeline.adoc +740 -0
  32. data/docs/_tutorials/first-application.adoc +384 -0
  33. data/docs/_tutorials/index.adoc +48 -0
  34. data/docs/_tutorials/long-running-services.adoc +931 -0
  35. data/docs/assets/images/favicon-16.png +0 -0
  36. data/docs/assets/images/favicon-32.png +0 -0
  37. data/docs/assets/images/favicon-48.png +0 -0
  38. data/docs/assets/images/favicon.ico +0 -0
  39. data/docs/assets/images/favicon.png +0 -0
  40. data/docs/assets/images/favicon.svg +45 -0
  41. data/docs/assets/images/fractor-icon.svg +49 -0
  42. data/docs/assets/images/fractor-logo.svg +61 -0
  43. data/docs/index.adoc +131 -0
  44. data/docs/lychee.toml +39 -0
  45. data/examples/api_aggregator/README.adoc +627 -0
  46. data/examples/api_aggregator/api_aggregator.rb +376 -0
  47. data/examples/auto_detection/README.adoc +407 -29
  48. data/examples/auto_detection/auto_detection.rb +9 -9
  49. data/examples/continuous_chat_common/message_protocol.rb +53 -0
  50. data/examples/continuous_chat_fractor/README.adoc +217 -0
  51. data/examples/continuous_chat_fractor/chat_client.rb +303 -0
  52. data/examples/continuous_chat_fractor/chat_common.rb +83 -0
  53. data/examples/continuous_chat_fractor/chat_server.rb +167 -0
  54. data/examples/continuous_chat_fractor/simulate.rb +345 -0
  55. data/examples/continuous_chat_server/README.adoc +135 -0
  56. data/examples/continuous_chat_server/chat_client.rb +303 -0
  57. data/examples/continuous_chat_server/chat_server.rb +359 -0
  58. data/examples/continuous_chat_server/simulate.rb +343 -0
  59. data/examples/error_reporting.rb +207 -0
  60. data/examples/file_processor/README.adoc +170 -0
  61. data/examples/file_processor/file_processor.rb +615 -0
  62. data/examples/file_processor/sample_files/invalid.csv +1 -0
  63. data/examples/file_processor/sample_files/orders.xml +24 -0
  64. data/examples/file_processor/sample_files/products.json +23 -0
  65. data/examples/file_processor/sample_files/users.csv +6 -0
  66. data/examples/hierarchical_hasher/README.adoc +629 -41
  67. data/examples/hierarchical_hasher/hierarchical_hasher.rb +12 -8
  68. data/examples/image_processor/README.adoc +610 -0
  69. data/examples/image_processor/image_processor.rb +349 -0
  70. data/examples/image_processor/processed_images/sample_10_processed.jpg.json +12 -0
  71. data/examples/image_processor/processed_images/sample_1_processed.jpg.json +12 -0
  72. data/examples/image_processor/processed_images/sample_2_processed.jpg.json +12 -0
  73. data/examples/image_processor/processed_images/sample_3_processed.jpg.json +12 -0
  74. data/examples/image_processor/processed_images/sample_4_processed.jpg.json +12 -0
  75. data/examples/image_processor/processed_images/sample_5_processed.jpg.json +12 -0
  76. data/examples/image_processor/processed_images/sample_6_processed.jpg.json +12 -0
  77. data/examples/image_processor/processed_images/sample_7_processed.jpg.json +12 -0
  78. data/examples/image_processor/processed_images/sample_8_processed.jpg.json +12 -0
  79. data/examples/image_processor/processed_images/sample_9_processed.jpg.json +12 -0
  80. data/examples/image_processor/test_images/sample_1.png +1 -0
  81. data/examples/image_processor/test_images/sample_10.png +1 -0
  82. data/examples/image_processor/test_images/sample_2.png +1 -0
  83. data/examples/image_processor/test_images/sample_3.png +1 -0
  84. data/examples/image_processor/test_images/sample_4.png +1 -0
  85. data/examples/image_processor/test_images/sample_5.png +1 -0
  86. data/examples/image_processor/test_images/sample_6.png +1 -0
  87. data/examples/image_processor/test_images/sample_7.png +1 -0
  88. data/examples/image_processor/test_images/sample_8.png +1 -0
  89. data/examples/image_processor/test_images/sample_9.png +1 -0
  90. data/examples/log_analyzer/README.adoc +662 -0
  91. data/examples/log_analyzer/log_analyzer.rb +579 -0
  92. data/examples/log_analyzer/sample_logs/apache.log +20 -0
  93. data/examples/log_analyzer/sample_logs/json.log +15 -0
  94. data/examples/log_analyzer/sample_logs/nginx.log +15 -0
  95. data/examples/log_analyzer/sample_logs/rails.log +29 -0
  96. data/examples/multi_work_type/README.adoc +576 -26
  97. data/examples/multi_work_type/multi_work_type.rb +30 -29
  98. data/examples/performance_monitoring.rb +120 -0
  99. data/examples/pipeline_processing/README.adoc +740 -26
  100. data/examples/pipeline_processing/pipeline_processing.rb +16 -16
  101. data/examples/priority_work_example.rb +155 -0
  102. data/examples/producer_subscriber/README.adoc +889 -46
  103. data/examples/producer_subscriber/producer_subscriber.rb +20 -16
  104. data/examples/scatter_gather/README.adoc +829 -27
  105. data/examples/scatter_gather/scatter_gather.rb +29 -28
  106. data/examples/simple/README.adoc +347 -0
  107. data/examples/simple/sample.rb +5 -5
  108. data/examples/specialized_workers/README.adoc +622 -26
  109. data/examples/specialized_workers/specialized_workers.rb +88 -45
  110. data/examples/stream_processor/README.adoc +206 -0
  111. data/examples/stream_processor/stream_processor.rb +284 -0
  112. data/examples/web_scraper/README.adoc +625 -0
  113. data/examples/web_scraper/web_scraper.rb +285 -0
  114. data/examples/workflow/README.adoc +406 -0
  115. data/examples/workflow/circuit_breaker/README.adoc +360 -0
  116. data/examples/workflow/circuit_breaker/circuit_breaker_workflow.rb +225 -0
  117. data/examples/workflow/conditional/README.adoc +483 -0
  118. data/examples/workflow/conditional/conditional_workflow.rb +215 -0
  119. data/examples/workflow/dead_letter_queue/README.adoc +374 -0
  120. data/examples/workflow/dead_letter_queue/dead_letter_queue_workflow.rb +217 -0
  121. data/examples/workflow/fan_out/README.adoc +381 -0
  122. data/examples/workflow/fan_out/fan_out_workflow.rb +202 -0
  123. data/examples/workflow/retry/README.adoc +248 -0
  124. data/examples/workflow/retry/retry_workflow.rb +195 -0
  125. data/examples/workflow/simple_linear/README.adoc +267 -0
  126. data/examples/workflow/simple_linear/simple_linear_workflow.rb +175 -0
  127. data/examples/workflow/simplified/README.adoc +329 -0
  128. data/examples/workflow/simplified/simplified_workflow.rb +222 -0
  129. data/exe/fractor +10 -0
  130. data/lib/fractor/cli.rb +288 -0
  131. data/lib/fractor/configuration.rb +307 -0
  132. data/lib/fractor/continuous_server.rb +183 -0
  133. data/lib/fractor/error_formatter.rb +72 -0
  134. data/lib/fractor/error_report_generator.rb +152 -0
  135. data/lib/fractor/error_reporter.rb +244 -0
  136. data/lib/fractor/error_statistics.rb +147 -0
  137. data/lib/fractor/execution_tracer.rb +162 -0
  138. data/lib/fractor/logger.rb +230 -0
  139. data/lib/fractor/main_loop_handler.rb +406 -0
  140. data/lib/fractor/main_loop_handler3.rb +135 -0
  141. data/lib/fractor/main_loop_handler4.rb +299 -0
  142. data/lib/fractor/performance_metrics_collector.rb +181 -0
  143. data/lib/fractor/performance_monitor.rb +215 -0
  144. data/lib/fractor/performance_report_generator.rb +202 -0
  145. data/lib/fractor/priority_work.rb +93 -0
  146. data/lib/fractor/priority_work_queue.rb +189 -0
  147. data/lib/fractor/result_aggregator.rb +33 -1
  148. data/lib/fractor/shutdown_handler.rb +168 -0
  149. data/lib/fractor/signal_handler.rb +80 -0
  150. data/lib/fractor/supervisor.rb +430 -144
  151. data/lib/fractor/supervisor_logger.rb +88 -0
  152. data/lib/fractor/version.rb +1 -1
  153. data/lib/fractor/work.rb +12 -0
  154. data/lib/fractor/work_distribution_manager.rb +151 -0
  155. data/lib/fractor/work_queue.rb +88 -0
  156. data/lib/fractor/work_result.rb +181 -9
  157. data/lib/fractor/worker.rb +75 -1
  158. data/lib/fractor/workflow/builder.rb +210 -0
  159. data/lib/fractor/workflow/chain_builder.rb +169 -0
  160. data/lib/fractor/workflow/circuit_breaker.rb +183 -0
  161. data/lib/fractor/workflow/circuit_breaker_orchestrator.rb +208 -0
  162. data/lib/fractor/workflow/circuit_breaker_registry.rb +112 -0
  163. data/lib/fractor/workflow/dead_letter_queue.rb +334 -0
  164. data/lib/fractor/workflow/execution_hooks.rb +39 -0
  165. data/lib/fractor/workflow/execution_strategy.rb +225 -0
  166. data/lib/fractor/workflow/execution_trace.rb +134 -0
  167. data/lib/fractor/workflow/helpers.rb +191 -0
  168. data/lib/fractor/workflow/job.rb +290 -0
  169. data/lib/fractor/workflow/job_dependency_validator.rb +120 -0
  170. data/lib/fractor/workflow/logger.rb +110 -0
  171. data/lib/fractor/workflow/pre_execution_context.rb +193 -0
  172. data/lib/fractor/workflow/retry_config.rb +156 -0
  173. data/lib/fractor/workflow/retry_orchestrator.rb +184 -0
  174. data/lib/fractor/workflow/retry_strategy.rb +93 -0
  175. data/lib/fractor/workflow/structured_logger.rb +30 -0
  176. data/lib/fractor/workflow/type_compatibility_validator.rb +222 -0
  177. data/lib/fractor/workflow/visualizer.rb +211 -0
  178. data/lib/fractor/workflow/workflow_context.rb +132 -0
  179. data/lib/fractor/workflow/workflow_executor.rb +669 -0
  180. data/lib/fractor/workflow/workflow_result.rb +55 -0
  181. data/lib/fractor/workflow/workflow_validator.rb +295 -0
  182. data/lib/fractor/workflow.rb +333 -0
  183. data/lib/fractor/wrapped_ractor.rb +66 -91
  184. data/lib/fractor/wrapped_ractor3.rb +161 -0
  185. data/lib/fractor/wrapped_ractor4.rb +242 -0
  186. data/lib/fractor.rb +93 -3
  187. metadata +192 -6
  188. data/tests/sample.rb.bak +0 -309
  189. data/tests/sample_working.rb.bak +0 -209
@@ -0,0 +1,360 @@
1
+ = Circuit Breaker Workflow Example
2
+
3
+ This example demonstrates the circuit breaker pattern for fault tolerance in Fractor workflows. Circuit breakers prevent cascading failures by failing fast when a service is unavailable.
4
+
5
+ == Overview
6
+
7
+ The circuit breaker pattern protects your workflows from repeated failures by:
8
+
9
+ * *Detecting failures*: Tracking failure rates for external services
10
+ * *Preventing cascading failures*: Failing fast when a service is down
11
+ * *Automatic recovery*: Testing service availability and recovering automatically
12
+ * *Graceful degradation*: Using fallback strategies when primary services fail
13
+
14
+ == Circuit Breaker States
15
+
16
+ A circuit breaker has three states:
17
+
18
+ [cols="1,3"]
19
+ |===
20
+ |State |Behavior
21
+
22
+ |*Closed*
23
+ |Normal operation - requests pass through to the service
24
+
25
+ |*Open*
26
+ |Failure threshold exceeded - requests fail immediately without calling the service
27
+
28
+ |*Half-Open*
29
+ |Testing phase - limited requests allowed to check if service recovered
30
+ |===
31
+
32
+ == State Transitions
33
+
34
+ [source]
35
+ ----
36
+ success
37
+ Closed ◄─────────── Half-Open
38
+ │ ▲
39
+ │ │
40
+ │ threshold │ timeout
41
+ │ exceeded │ elapsed
42
+ │ │
43
+ ▼ │
44
+ Open ─────────────────┘
45
+
46
+ Closed → Open: After threshold failures
47
+ Open → Half-Open: After timeout period
48
+ Half-Open → Closed: After successful test calls
49
+ Half-Open → Open: On any failure during testing
50
+ ----
51
+
52
+ == Basic Circuit Breaker
53
+
54
+ === Configuration
55
+
56
+ [source,ruby]
57
+ ----
58
+ job "fetch_from_api" do
59
+ runs_with UnreliableApiWorker
60
+ inputs_from_workflow
61
+
62
+ circuit_breaker threshold: 3, # <1>
63
+ timeout: 60, # <2>
64
+ half_open_calls: 2 # <3>
65
+
66
+ fallback_to "fetch_from_cache" # <4>
67
+
68
+ outputs_to_workflow
69
+ terminates_workflow
70
+ end
71
+ ----
72
+ <1> Circuit opens after 3 consecutive failures
73
+ <2> Stay open for 60 seconds before testing recovery
74
+ <3> Allow 2 test calls in half-open state
75
+ <4> Use cache when circuit is open
76
+
77
+ === How It Works
78
+
79
+ . *Normal Operation (Closed)*
80
+ ** All requests pass through to the API
81
+ ** Successful calls reset the failure count
82
+ ** Failed calls increment the failure count
83
+
84
+ . *Circuit Opens*
85
+ ** After 3 failures, circuit opens
86
+ ** All subsequent requests fail immediately
87
+ ** No calls made to the failing service
88
+
89
+ . *Recovery Testing (Half-Open)*
90
+ ** After 60 seconds, circuit enters half-open state
91
+ ** Allows 2 test calls to check service recovery
92
+ ** If both succeed → circuit closes (back to normal)
93
+ ** If any fails → circuit reopens
94
+
95
+ . *Fallback Activation*
96
+ ** When circuit is open, fallback job executes
97
+ ** Provides degraded service using cached data
98
+ ** Maintains availability despite service failure
99
+
100
+ == Shared Circuit Breaker
101
+
102
+ Multiple jobs can share a circuit breaker using `shared_key`:
103
+
104
+ [source,ruby]
105
+ ----
106
+ job "fetch_user_data" do
107
+ runs_with ApiWorker
108
+ inputs_from_workflow
109
+
110
+ circuit_breaker threshold: 5,
111
+ timeout: 60,
112
+ half_open_calls: 3,
113
+ shared_key: "external_api" # <1>
114
+ end
115
+
116
+ job "fetch_profile_data" do
117
+ runs_with ApiWorker
118
+ inputs_from_workflow
119
+
120
+ circuit_breaker threshold: 5,
121
+ timeout: 60,
122
+ half_open_calls: 3,
123
+ shared_key: "external_api" # <1>
124
+ end
125
+ ----
126
+ <1> Same `shared_key` means both jobs share the same circuit breaker instance
127
+
128
+ === Shared Circuit Breaker Benefits
129
+
130
+ * *Coordinated protection*: Failures from any job contribute to the shared threshold
131
+ * *System-wide defense*: When one job triggers the circuit, all jobs using the same key are protected
132
+ * *Resource conservation*: Prevents multiple jobs from hammering a failing service
133
+ * *Faster detection*: Aggregate failures across jobs trigger protection sooner
134
+
135
+ == Running the Example
136
+
137
+ [source,shell]
138
+ ----
139
+ ruby examples/workflow/circuit_breaker/circuit_breaker_workflow.rb
140
+ ----
141
+
142
+ Expected output:
143
+
144
+ [source]
145
+ ----
146
+ ============================================================
147
+ Circuit Breaker Example - Basic Protection
148
+ ============================================================
149
+
150
+ 1️⃣ Successful API call:
151
+ ✅ Result: Data from users/123 (source: primary)
152
+
153
+ 2️⃣ Triggering circuit breaker (3 failures):
154
+ ❌ API call failed: API service unavailable
155
+ Attempt: 1
156
+ Attempt 1: Circuit breaker protecting...
157
+ ❌ API call failed: API service unavailable
158
+ Attempt: 1
159
+ Attempt 2: Circuit breaker protecting...
160
+ ❌ API call failed: API service unavailable
161
+ Attempt: 1
162
+ Attempt 3: Circuit breaker protecting...
163
+
164
+ 3️⃣ Circuit open - using fallback:
165
+ ✅ Result: Cached data for users/456 (source: cache)
166
+ Note: Got cached data because circuit is open
167
+
168
+ ============================================================
169
+ ----
170
+
171
+ == Configuration Options
172
+
173
+ [cols="1,1,3"]
174
+ |===
175
+ |Option |Default |Description
176
+
177
+ |`threshold`
178
+ |5
179
+ |Number of consecutive failures before opening circuit
180
+
181
+ |`timeout`
182
+ |60
183
+ |Seconds to wait in open state before testing recovery
184
+
185
+ |`half_open_calls`
186
+ |3
187
+ |Number of successful test calls needed to close circuit
188
+
189
+ |`shared_key`
190
+ |`nil`
191
+ |Optional key for sharing circuit breaker across jobs. If not provided, each job gets its own circuit breaker
192
+ |===
193
+
194
+ == Use Cases
195
+
196
+ === External API Calls
197
+
198
+ Protect against unreliable external services:
199
+
200
+ [source,ruby]
201
+ ----
202
+ job "call_payment_gateway" do
203
+ runs_with PaymentGatewayWorker
204
+
205
+ circuit_breaker threshold: 5,
206
+ timeout: 120
207
+
208
+ fallback_to "queue_for_retry"
209
+ end
210
+ ----
211
+
212
+ === Database Operations
213
+
214
+ Prevent connection pool exhaustion:
215
+
216
+ [source,ruby]
217
+ ----
218
+ job "query_database" do
219
+ runs_with DatabaseWorker
220
+
221
+ circuit_breaker threshold: 10,
222
+ timeout: 60,
223
+ shared_key: "database_pool"
224
+
225
+ fallback_to "use_read_replica"
226
+ end
227
+ ----
228
+
229
+ === Microservice Communication
230
+
231
+ Protect service mesh communication:
232
+
233
+ [source,ruby]
234
+ ----
235
+ job "call_user_service" do
236
+ runs_with UserServiceWorker
237
+
238
+ circuit_breaker threshold: 3,
239
+ timeout: 30,
240
+ shared_key: "user_service"
241
+
242
+ fallback_to "use_cached_users"
243
+ end
244
+ ----
245
+
246
+ == Best Practices
247
+
248
+ === Choose Appropriate Thresholds
249
+
250
+ * *Low threshold (2-3)*: For critical services where fast failure is important
251
+ * *Medium threshold (5-10)*: For services with occasional transient failures
252
+ * *High threshold (10+)*: For services where you want to allow more retries
253
+
254
+ === Set Reasonable Timeouts
255
+
256
+ * *Short timeout (15-30s)*: For services that recover quickly
257
+ * *Medium timeout (60s)*: Standard timeout for most services
258
+ * *Long timeout (120s+)*: For services with slow recovery processes
259
+
260
+ === Combine with Retry Logic
261
+
262
+ Circuit breakers work well with retry logic:
263
+
264
+ [source,ruby]
265
+ ----
266
+ job "resilient_api_call" do
267
+ runs_with ApiWorker
268
+
269
+ # Retry transient failures
270
+ retry_on_error max_attempts: 3,
271
+ backoff: :exponential
272
+
273
+ # Protect against sustained failures
274
+ circuit_breaker threshold: 5,
275
+ timeout: 60
276
+
277
+ # Final fallback
278
+ fallback_to "use_cache"
279
+ end
280
+ ----
281
+
282
+ === Monitor Circuit Breaker State
283
+
284
+ Track circuit breaker metrics:
285
+
286
+ * Number of times circuit opened
287
+ * Time spent in each state
288
+ * Failure rates before/after protection
289
+ * Fallback activation frequency
290
+
291
+ === Use Shared Circuit Breakers Wisely
292
+
293
+ Share circuit breakers for:
294
+
295
+ * Jobs calling the same external service
296
+ * Jobs that should fail together
297
+ * Jobs sharing the same resource pool
298
+
299
+ Use separate circuit breakers for:
300
+
301
+ * Independent services
302
+ * Services with different SLAs
303
+ * Jobs with different criticality levels
304
+
305
+ == Integration with Other Features
306
+
307
+ === With Retry Logic
308
+
309
+ [source,ruby]
310
+ ----
311
+ job "api_call" do
312
+ runs_with ApiWorker
313
+
314
+ # First: Retry transient failures
315
+ retry_on_error max_attempts: 3,
316
+ backoff: :exponential,
317
+ initial_delay: 1
318
+
319
+ # Second: Circuit breaker for sustained failures
320
+ circuit_breaker threshold: 10,
321
+ timeout: 60
322
+
323
+ # Final: Fallback when circuit opens
324
+ fallback_to "cached_data"
325
+ end
326
+ ----
327
+
328
+ === With Error Handlers
329
+
330
+ [source,ruby]
331
+ ----
332
+ job "monitored_api_call" do
333
+ runs_with ApiWorker
334
+
335
+ circuit_breaker threshold: 5,
336
+ timeout: 60
337
+
338
+ # Log circuit breaker events
339
+ on_error do |error, context|
340
+ if error.is_a?(Fractor::Workflow::CircuitOpenError)
341
+ Metrics.increment("circuit_breaker.open")
342
+ AlertService.notify("Circuit breaker opened for #{context.job_id}")
343
+ end
344
+ end
345
+
346
+ fallback_to "cached_data"
347
+ end
348
+ ----
349
+
350
+ == Related Examples
351
+
352
+ * link:../retry/README.adoc[Retry Workflow] - Automatic retry with backoff strategies
353
+ * link:../conditional/README.adoc[Conditional Workflow] - Conditional job execution
354
+ * link:../fan_out/README.adoc[Fan-Out Workflow] - Parallel job execution
355
+
356
+ == Learn More
357
+
358
+ * link:../../../docs/workflows.adoc[Workflow Documentation]
359
+ * link:../../../docs/getting-started.adoc[Getting Started Guide]
360
+ * link:../README.adoc[All Workflow Examples]
@@ -0,0 +1,225 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../../lib/fractor"
4
+
5
+ # This example demonstrates circuit breaker pattern for fault tolerance
6
+ # in workflows. The circuit breaker prevents cascading failures by
7
+ # failing fast when a service is unavailable.
8
+ module CircuitBreakerExample
9
+ # Input data for the workflow
10
+ class ApiRequest < Fractor::Work
11
+ attr_reader :endpoint, :should_fail
12
+
13
+ def initialize(endpoint, should_fail: false)
14
+ @endpoint = endpoint
15
+ @should_fail = should_fail
16
+ super(endpoint)
17
+ end
18
+ end
19
+
20
+ # Output data from the workflow
21
+ class ApiResponse
22
+ attr_reader :data, :source
23
+
24
+ def initialize(data:, source: :primary)
25
+ @data = data
26
+ @source = source
27
+ end
28
+ end
29
+
30
+ # Worker that simulates calling an unreliable external API
31
+ class UnreliableApiWorker < Fractor::Worker
32
+ input_type ApiRequest
33
+ output_type ApiResponse
34
+
35
+ def process(work)
36
+ if work.should_fail
37
+ raise StandardError, "API service unavailable"
38
+ end
39
+
40
+ # Simulate API call
41
+ sleep 0.1
42
+ result = ApiResponse.new(
43
+ data: "Data from #{work.endpoint}",
44
+ source: :primary,
45
+ )
46
+
47
+ Fractor::WorkResult.new(result: result, work: work)
48
+ end
49
+ end
50
+
51
+ # Worker that provides fallback data from cache
52
+ class CachedDataWorker < Fractor::Worker
53
+ input_type ApiRequest
54
+ output_type ApiResponse
55
+
56
+ def process(work)
57
+ # Simulate cache lookup
58
+ sleep 0.05
59
+ result = ApiResponse.new(
60
+ data: "Cached data for #{work.endpoint}",
61
+ source: :cache,
62
+ )
63
+
64
+ Fractor::WorkResult.new(result: result, work: work)
65
+ end
66
+ end
67
+
68
+ # Workflow demonstrating circuit breaker with fallback
69
+ class CircuitBreakerWorkflow < Fractor::Workflow
70
+ workflow "circuit-breaker-example" do
71
+ input_type ApiRequest
72
+ output_type ApiResponse
73
+ start_with "fetch_from_api"
74
+
75
+ # Primary API job with circuit breaker protection
76
+ job "fetch_from_api" do
77
+ runs_with UnreliableApiWorker
78
+ inputs_from_workflow
79
+
80
+ # Circuit breaker configuration:
81
+ # - Opens after 3 failures
82
+ # - Stays open for 60 seconds
83
+ # - Allows 2 test calls when half-open
84
+ circuit_breaker threshold: 3,
85
+ timeout: 60,
86
+ half_open_calls: 2
87
+
88
+ # Fallback to cache when circuit opens
89
+ fallback_to "fetch_from_cache"
90
+
91
+ # Log circuit breaker events (expected behavior in test scenarios)
92
+ on_error do |error, _context|
93
+ puts "⚠️ API fallback triggered: #{error.message}"
94
+ end
95
+
96
+ outputs_to_workflow
97
+ terminates_workflow
98
+ end
99
+
100
+ # Fallback job that uses cached data
101
+ job "fetch_from_cache" do
102
+ runs_with CachedDataWorker
103
+ inputs_from_workflow
104
+ outputs_to_workflow
105
+ terminates_workflow
106
+ end
107
+ end
108
+ end
109
+
110
+ # Workflow demonstrating shared circuit breaker across jobs
111
+ class SharedCircuitBreakerWorkflow < Fractor::Workflow
112
+ workflow "shared-circuit-breaker-example" do
113
+ input_type ApiRequest
114
+ output_type ApiResponse
115
+ start_with "fetch_user_data"
116
+
117
+ # First API job using shared circuit breaker
118
+ job "fetch_user_data" do
119
+ runs_with UnreliableApiWorker
120
+ inputs_from_workflow
121
+
122
+ # Use shared circuit breaker for the API service
123
+ circuit_breaker threshold: 5,
124
+ timeout: 60,
125
+ half_open_calls: 3,
126
+ shared_key: "external_api"
127
+
128
+ fallback_to "fetch_cached_user_data"
129
+ end
130
+
131
+ # Second API job sharing the same circuit breaker
132
+ job "fetch_profile_data" do
133
+ runs_with UnreliableApiWorker
134
+ inputs_from_workflow
135
+ needs "fetch_user_data"
136
+
137
+ # Same shared_key means same circuit breaker instance
138
+ circuit_breaker threshold: 5,
139
+ timeout: 60,
140
+ half_open_calls: 3,
141
+ shared_key: "external_api"
142
+
143
+ fallback_to "fetch_cached_profile_data"
144
+ end
145
+
146
+ # Fallback jobs
147
+ job "fetch_cached_user_data" do
148
+ runs_with CachedDataWorker
149
+ inputs_from_workflow
150
+ end
151
+
152
+ job "fetch_cached_profile_data" do
153
+ runs_with CachedDataWorker
154
+ inputs_from_workflow
155
+ end
156
+ end
157
+ end
158
+
159
+ # Demonstration runner
160
+ def self.run_basic_example
161
+ puts "\n#{'=' * 60}"
162
+ puts "Circuit Breaker Example - Basic Protection"
163
+ puts "=" * 60
164
+
165
+ workflow = CircuitBreakerWorkflow.new
166
+
167
+ # Successful request
168
+ puts "\n1️⃣ Successful API call:"
169
+ request = ApiRequest.new("users/123", should_fail: false)
170
+ result = workflow.execute(input: request)
171
+ puts "✅ Result: #{result.output.data} (source: #{result.output.source})"
172
+
173
+ # Trigger circuit breaker with failures
174
+ puts "\n2️⃣ Triggering circuit breaker (3 failures):"
175
+ 3.times do |i|
176
+ request = ApiRequest.new("users/#{i}", should_fail: true)
177
+ result = workflow.execute(input: request)
178
+ if result.success?
179
+ puts " Attempt #{i + 1}: Success"
180
+ else
181
+ puts " Attempt #{i + 1}: Failed (using fallback)"
182
+ end
183
+ end
184
+
185
+ # Circuit should now be open - fallback activated
186
+ puts "\n3️⃣ Circuit open - using fallback:"
187
+ request = ApiRequest.new("users/456", should_fail: true) # Still failing to show fallback
188
+ result = workflow.execute(input: request)
189
+ puts "✅ Result: #{result.output.data} (source: #{result.output.source})"
190
+ puts " Note: Got cached data because primary failed"
191
+
192
+ puts "\n#{'=' * 60}"
193
+ end
194
+
195
+ def self.run_shared_example
196
+ puts "\n#{'=' * 60}"
197
+ puts "Circuit Breaker Example - Shared Protection"
198
+ puts "=" * 60
199
+
200
+ workflow = SharedCircuitBreakerWorkflow.new
201
+
202
+ puts "\n1️⃣ Multiple jobs sharing same circuit breaker:"
203
+ puts " Each failure contributes to the shared threshold"
204
+
205
+ # Cause some failures
206
+ 3.times do |i|
207
+ request = ApiRequest.new("endpoint_#{i}", should_fail: true)
208
+ result = workflow.execute(input: request)
209
+ if result.success?
210
+ puts " Failure #{i + 1}: Unexpected success"
211
+ else
212
+ puts " Failure #{i + 1}: Building up to threshold..."
213
+ end
214
+ end
215
+
216
+ puts "\n Shared circuit breaker protects all jobs using it"
217
+ puts "=" * 60
218
+ end
219
+ end
220
+
221
+ # Run examples if executed directly
222
+ if __FILE__ == $PROGRAM_NAME
223
+ CircuitBreakerExample.run_basic_example
224
+ CircuitBreakerExample.run_shared_example
225
+ end