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.
- checksums.yaml +4 -4
- data/.rubocop_todo.yml +227 -102
- data/README.adoc +113 -1940
- 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/continuous_chat_common/message_protocol.rb +1 -1
- 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/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/performance_monitoring.rb +120 -0
- data/examples/pipeline_processing/README.adoc +740 -26
- data/examples/pipeline_processing/pipeline_processing.rb +2 -2
- data/examples/priority_work_example.rb +155 -0
- data/examples/producer_subscriber/README.adoc +889 -46
- data/examples/scatter_gather/README.adoc +829 -27
- data/examples/simple/README.adoc +347 -0
- data/examples/specialized_workers/README.adoc +622 -26
- data/examples/specialized_workers/specialized_workers.rb +44 -8
- 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 +60 -65
- 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 +32 -0
- data/lib/fractor/shutdown_handler.rb +168 -0
- data/lib/fractor/signal_handler.rb +80 -0
- data/lib/fractor/supervisor.rb +382 -269
- 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 +20 -0
- data/lib/fractor/work_result.rb +181 -9
- data/lib/fractor/worker.rb +73 -0
- 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 -101
- data/lib/fractor/wrapped_ractor3.rb +161 -0
- data/lib/fractor/wrapped_ractor4.rb +242 -0
- data/lib/fractor.rb +92 -4
- metadata +179 -6
- data/tests/sample.rb.bak +0 -309
- 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
|