simple_flow 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.envrc +1 -0
- data/.github/workflows/deploy-github-pages.yml +52 -0
- data/.rubocop.yml +57 -0
- data/CHANGELOG.md +4 -0
- data/COMMITS.md +196 -0
- data/LICENSE +21 -0
- data/README.md +481 -0
- data/Rakefile +15 -0
- data/benchmarks/parallel_vs_sequential.rb +98 -0
- data/benchmarks/pipeline_overhead.rb +130 -0
- data/docs/api/middleware.md +468 -0
- data/docs/api/parallel-step.md +363 -0
- data/docs/api/pipeline.md +382 -0
- data/docs/api/result.md +375 -0
- data/docs/concurrent/best-practices.md +687 -0
- data/docs/concurrent/introduction.md +246 -0
- data/docs/concurrent/parallel-steps.md +418 -0
- data/docs/concurrent/performance.md +481 -0
- data/docs/core-concepts/flow-control.md +452 -0
- data/docs/core-concepts/middleware.md +389 -0
- data/docs/core-concepts/overview.md +219 -0
- data/docs/core-concepts/pipeline.md +315 -0
- data/docs/core-concepts/result.md +168 -0
- data/docs/core-concepts/steps.md +391 -0
- data/docs/development/benchmarking.md +443 -0
- data/docs/development/contributing.md +380 -0
- data/docs/development/dagwood-concepts.md +435 -0
- data/docs/development/testing.md +514 -0
- data/docs/getting-started/examples.md +197 -0
- data/docs/getting-started/installation.md +62 -0
- data/docs/getting-started/quick-start.md +218 -0
- data/docs/guides/choosing-concurrency-model.md +441 -0
- data/docs/guides/complex-workflows.md +440 -0
- data/docs/guides/data-fetching.md +478 -0
- data/docs/guides/error-handling.md +635 -0
- data/docs/guides/file-processing.md +505 -0
- data/docs/guides/validation-patterns.md +496 -0
- data/docs/index.md +169 -0
- data/examples/.gitignore +3 -0
- data/examples/01_basic_pipeline.rb +112 -0
- data/examples/02_error_handling.rb +178 -0
- data/examples/03_middleware.rb +186 -0
- data/examples/04_parallel_automatic.rb +221 -0
- data/examples/05_parallel_explicit.rb +279 -0
- data/examples/06_real_world_ecommerce.rb +288 -0
- data/examples/07_real_world_etl.rb +277 -0
- data/examples/08_graph_visualization.rb +246 -0
- data/examples/09_pipeline_visualization.rb +266 -0
- data/examples/10_concurrency_control.rb +235 -0
- data/examples/11_sequential_dependencies.rb +243 -0
- data/examples/12_none_constant.rb +161 -0
- data/examples/README.md +374 -0
- data/examples/regression_test/01_basic_pipeline.txt +38 -0
- data/examples/regression_test/02_error_handling.txt +92 -0
- data/examples/regression_test/03_middleware.txt +61 -0
- data/examples/regression_test/04_parallel_automatic.txt +86 -0
- data/examples/regression_test/05_parallel_explicit.txt +80 -0
- data/examples/regression_test/06_real_world_ecommerce.txt +53 -0
- data/examples/regression_test/07_real_world_etl.txt +58 -0
- data/examples/regression_test/08_graph_visualization.txt +429 -0
- data/examples/regression_test/09_pipeline_visualization.txt +305 -0
- data/examples/regression_test/10_concurrency_control.txt +96 -0
- data/examples/regression_test/11_sequential_dependencies.txt +86 -0
- data/examples/regression_test/12_none_constant.txt +64 -0
- data/examples/regression_test.rb +105 -0
- data/lib/simple_flow/dependency_graph.rb +120 -0
- data/lib/simple_flow/dependency_graph_visualizer.rb +326 -0
- data/lib/simple_flow/middleware.rb +36 -0
- data/lib/simple_flow/parallel_executor.rb +80 -0
- data/lib/simple_flow/pipeline.rb +405 -0
- data/lib/simple_flow/result.rb +88 -0
- data/lib/simple_flow/step_tracker.rb +58 -0
- data/lib/simple_flow/version.rb +5 -0
- data/lib/simple_flow.rb +41 -0
- data/mkdocs.yml +146 -0
- data/pipeline_graph.dot +51 -0
- data/pipeline_graph.html +60 -0
- data/pipeline_graph.mmd +19 -0
- metadata +127 -0
data/README.md
ADDED
|
@@ -0,0 +1,481 @@
|
|
|
1
|
+
# SimpleFlow
|
|
2
|
+
|
|
3
|
+
[](https://www.ruby-lang.org)
|
|
4
|
+
[](https://github.com/MadBomber/simple_flow)
|
|
5
|
+
[](https://github.com/MadBomber/simple_flow)
|
|
6
|
+
[](https://madbomber.github.io/simple_flow)
|
|
7
|
+
[](LICENSE)
|
|
8
|
+
|
|
9
|
+
A lightweight, modular Ruby framework for building composable data processing pipelines with middleware support, flow control, and parallel execution.
|
|
10
|
+
|
|
11
|
+
📚 **[Full Documentation](https://madbomber.github.io/simple_flow)** | 🚀 **[Getting Started](https://madbomber.github.io/simple_flow/getting-started/quick-start/)** | 📖 **[API Reference](https://madbomber.github.io/simple_flow/api/result/)**
|
|
12
|
+
|
|
13
|
+
## Overview
|
|
14
|
+
|
|
15
|
+
SimpleFlow provides a clean architecture for orchestrating multi-step workflows with:
|
|
16
|
+
|
|
17
|
+
- **Immutable Results** - Thread-safe value objects
|
|
18
|
+
- **Composable Steps** - Mix and match processing units
|
|
19
|
+
- **Flow Control** - Built-in halt/continue mechanisms
|
|
20
|
+
- **Middleware Support** - Cross-cutting concerns via decorator pattern
|
|
21
|
+
- **Parallel Execution** - Automatic and explicit concurrency
|
|
22
|
+
- **Visualization** - Export pipelines to Graphviz, Mermaid, HTML
|
|
23
|
+
|
|
24
|
+
## Installation
|
|
25
|
+
|
|
26
|
+
Add to your `Gemfile`:
|
|
27
|
+
|
|
28
|
+
```ruby
|
|
29
|
+
gem 'simple_flow'
|
|
30
|
+
|
|
31
|
+
# Optional: for fiber-based concurrency (recommended for I/O-bound tasks)
|
|
32
|
+
gem 'async', '~> 2.0'
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Then run:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
bundle install
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
**Note on Parallel Execution:**
|
|
42
|
+
- **Without** `async` gem: Uses Ruby threads for parallel execution
|
|
43
|
+
- **With** `async` gem: Uses fiber-based concurrency (more efficient for I/O-bound operations)
|
|
44
|
+
|
|
45
|
+
## Quick Start
|
|
46
|
+
|
|
47
|
+
### Basic Pipeline
|
|
48
|
+
|
|
49
|
+
```ruby
|
|
50
|
+
require 'simple_flow'
|
|
51
|
+
|
|
52
|
+
pipeline = SimpleFlow::Pipeline.new do
|
|
53
|
+
step ->(result) { result.continue(result.value.strip) }
|
|
54
|
+
step ->(result) { result.continue(result.value.upcase) }
|
|
55
|
+
step ->(result) { result.continue("Hello, #{result.value}!") }
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
result = pipeline.call(SimpleFlow::Result.new(" world "))
|
|
59
|
+
puts result.value # => "Hello, WORLD!"
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### Error Handling
|
|
63
|
+
|
|
64
|
+
**Sequential steps automatically depend on the previous step's success.** When a step halts, the pipeline stops immediately and subsequent steps are not executed.
|
|
65
|
+
|
|
66
|
+
```ruby
|
|
67
|
+
pipeline = SimpleFlow::Pipeline.new do
|
|
68
|
+
step ->(result) {
|
|
69
|
+
puts "Step 1: Validating..."
|
|
70
|
+
if result.value < 18
|
|
71
|
+
return result
|
|
72
|
+
.with_error(:validation, 'Must be 18+')
|
|
73
|
+
.halt # Pipeline stops here
|
|
74
|
+
end
|
|
75
|
+
result.continue(result.value)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
step ->(result) {
|
|
79
|
+
puts "Step 2: Processing..." # This never executes if validation fails
|
|
80
|
+
result.continue("Age #{result.value} is valid")
|
|
81
|
+
}
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
result = pipeline.call(SimpleFlow::Result.new(15))
|
|
85
|
+
# Output: "Step 1: Validating..."
|
|
86
|
+
# (Step 2 is skipped because Step 1 halted)
|
|
87
|
+
|
|
88
|
+
puts result.continue? # => false
|
|
89
|
+
puts result.errors # => {:validation=>["Must be 18+"]}
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## Architecture
|
|
93
|
+
|
|
94
|
+
```mermaid
|
|
95
|
+
graph TB
|
|
96
|
+
subgraph Pipeline
|
|
97
|
+
MW[Middleware Stack]
|
|
98
|
+
S1[Step 1]
|
|
99
|
+
S2[Step 2]
|
|
100
|
+
S3[Step 3]
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
Input[Input Result] --> MW
|
|
104
|
+
MW --> S1
|
|
105
|
+
S1 -->|continue?| S2
|
|
106
|
+
S2 -->|continue?| S3
|
|
107
|
+
S3 --> Output[Output Result]
|
|
108
|
+
|
|
109
|
+
S1 -.->|halt| Output
|
|
110
|
+
S2 -.->|halt| Output
|
|
111
|
+
|
|
112
|
+
style MW fill:#e1f5ff
|
|
113
|
+
style Output fill:#d4edda
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## Execution Modes
|
|
117
|
+
|
|
118
|
+
SimpleFlow supports two execution modes:
|
|
119
|
+
|
|
120
|
+
### Sequential Steps (Default)
|
|
121
|
+
|
|
122
|
+
**Unnamed steps execute in order, with each step automatically depending on the previous step's success.**
|
|
123
|
+
|
|
124
|
+
```ruby
|
|
125
|
+
pipeline = SimpleFlow::Pipeline.new do
|
|
126
|
+
step ->(result) { result.continue(result.value.strip) }
|
|
127
|
+
step ->(result) { result.continue(result.value.upcase) }
|
|
128
|
+
step ->(result) { result.continue("Hello, #{result.value}!") }
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
result = pipeline.call(SimpleFlow::Result.new(" world "))
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
**Key behavior:**
|
|
135
|
+
- Steps run one at a time in definition order
|
|
136
|
+
- Each step receives the result from the previous step
|
|
137
|
+
- If any step halts, the pipeline stops immediately
|
|
138
|
+
- No explicit dependencies needed
|
|
139
|
+
|
|
140
|
+
### Parallel Steps
|
|
141
|
+
|
|
142
|
+
**Named steps with dependencies run concurrently based on a dependency graph.**
|
|
143
|
+
|
|
144
|
+
```ruby
|
|
145
|
+
pipeline = SimpleFlow::Pipeline.new do
|
|
146
|
+
step :validate, validator, depends_on: :none # Or use []
|
|
147
|
+
step :fetch_a, fetcher_a, depends_on: [:validate] # Parallel
|
|
148
|
+
step :fetch_b, fetcher_b, depends_on: [:validate] # Parallel
|
|
149
|
+
step :merge, merger, depends_on: [:fetch_a, :fetch_b]
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
result = pipeline.call_parallel(SimpleFlow::Result.new(data))
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
**Key behavior:**
|
|
156
|
+
- Steps run based on dependency graph, not definition order
|
|
157
|
+
- Steps with satisfied dependencies run in parallel
|
|
158
|
+
- Must explicitly specify all dependencies
|
|
159
|
+
- Use `call_parallel` to execute
|
|
160
|
+
|
|
161
|
+
## Core Concepts
|
|
162
|
+
|
|
163
|
+
### Result Object
|
|
164
|
+
|
|
165
|
+
Immutable value object that carries data, context, and errors through the pipeline:
|
|
166
|
+
|
|
167
|
+
```ruby
|
|
168
|
+
result = SimpleFlow::Result.new({ user: 'alice' })
|
|
169
|
+
.with_context(:timestamp, Time.now)
|
|
170
|
+
.with_error(:validation, 'Email required')
|
|
171
|
+
.continue({ user: 'alice', processed: true })
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
**[Learn more →](https://madbomber.github.io/simple_flow/core-concepts/result/)**
|
|
175
|
+
|
|
176
|
+
### Pipeline
|
|
177
|
+
|
|
178
|
+
Orchestrates step execution with short-circuit evaluation:
|
|
179
|
+
|
|
180
|
+
```ruby
|
|
181
|
+
pipeline = SimpleFlow::Pipeline.new do
|
|
182
|
+
use_middleware SimpleFlow::MiddleWare::Logging
|
|
183
|
+
use_middleware SimpleFlow::MiddleWare::Instrumentation, api_key: 'app'
|
|
184
|
+
|
|
185
|
+
step ->(result) { validate(result) }
|
|
186
|
+
step ->(result) { process(result) }
|
|
187
|
+
step ->(result) { save(result) }
|
|
188
|
+
end
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
**[Learn more →](https://madbomber.github.io/simple_flow/core-concepts/pipeline/)**
|
|
192
|
+
|
|
193
|
+
### Middleware
|
|
194
|
+
|
|
195
|
+
Add cross-cutting concerns without modifying steps:
|
|
196
|
+
|
|
197
|
+
```ruby
|
|
198
|
+
class CachingMiddleware
|
|
199
|
+
def initialize(callable, cache:)
|
|
200
|
+
@callable = callable
|
|
201
|
+
@cache = cache
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
def call(result)
|
|
205
|
+
cached = @cache.get(cache_key(result))
|
|
206
|
+
return result.continue(cached) if cached
|
|
207
|
+
|
|
208
|
+
result = @callable.call(result)
|
|
209
|
+
@cache.set(cache_key(result), result.value)
|
|
210
|
+
result
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
pipeline = SimpleFlow::Pipeline.new do
|
|
215
|
+
use CachingMiddleware, cache: Redis.new
|
|
216
|
+
step ->(result) { expensive_operation(result) }
|
|
217
|
+
end
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
**[Learn more →](https://madbomber.github.io/simple_flow/core-concepts/middleware/)**
|
|
221
|
+
|
|
222
|
+
## Parallel Execution
|
|
223
|
+
|
|
224
|
+
### Automatic Parallelization
|
|
225
|
+
|
|
226
|
+
SimpleFlow automatically detects which steps can run in parallel based on dependencies:
|
|
227
|
+
|
|
228
|
+
```ruby
|
|
229
|
+
pipeline = SimpleFlow::Pipeline.new do
|
|
230
|
+
step :validate, ->(r) { validate(r) }, depends_on: :none
|
|
231
|
+
|
|
232
|
+
# These run in parallel (both depend only on :validate)
|
|
233
|
+
step :fetch_orders, ->(r) { fetch_orders(r) }, depends_on: [:validate]
|
|
234
|
+
step :fetch_products, ->(r) { fetch_products(r) }, depends_on: [:validate]
|
|
235
|
+
|
|
236
|
+
# Waits for both parallel steps
|
|
237
|
+
step :calculate, ->(r) { calculate(r) }, depends_on: [:fetch_orders, :fetch_products]
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
result = pipeline.call_parallel(SimpleFlow::Result.new(data))
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
**Note:** For steps with no dependencies, you can use either `depends_on: :none` (more readable) or `depends_on: []`.
|
|
244
|
+
|
|
245
|
+
**Execution flow:**
|
|
246
|
+
|
|
247
|
+
```mermaid
|
|
248
|
+
graph TD
|
|
249
|
+
V[validate] --> O[fetch_orders]
|
|
250
|
+
V --> P[fetch_products]
|
|
251
|
+
O --> C[calculate]
|
|
252
|
+
P --> C
|
|
253
|
+
|
|
254
|
+
style V fill:#e1f5ff
|
|
255
|
+
style O fill:#fff3cd
|
|
256
|
+
style P fill:#fff3cd
|
|
257
|
+
style C fill:#d4edda
|
|
258
|
+
|
|
259
|
+
classDef parallel fill:#fff3cd,stroke:#ffc107
|
|
260
|
+
class O,P parallel
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
### Explicit Parallel Blocks
|
|
264
|
+
|
|
265
|
+
```ruby
|
|
266
|
+
pipeline = SimpleFlow::Pipeline.new do
|
|
267
|
+
step ->(r) { validate(r) }
|
|
268
|
+
|
|
269
|
+
parallel do
|
|
270
|
+
step ->(r) { r.with_context(:api, fetch_api).continue(r.value) }
|
|
271
|
+
step ->(r) { r.with_context(:db, fetch_db).continue(r.value) }
|
|
272
|
+
step ->(r) { r.with_context(:cache, fetch_cache).continue(r.value) }
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
step ->(r) { merge_results(r) }
|
|
276
|
+
end
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
### Concurrency Control
|
|
280
|
+
|
|
281
|
+
Choose the concurrency model per pipeline:
|
|
282
|
+
|
|
283
|
+
```ruby
|
|
284
|
+
# Auto-detect (default): uses async if available, otherwise threads
|
|
285
|
+
pipeline = SimpleFlow::Pipeline.new do
|
|
286
|
+
# steps...
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
# Force threads (even if async gem is installed)
|
|
290
|
+
user_pipeline = SimpleFlow::Pipeline.new(concurrency: :threads) do
|
|
291
|
+
step :fetch_profile, profile_fetcher, depends_on: []
|
|
292
|
+
step :fetch_settings, settings_fetcher, depends_on: []
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
# Require async (raises error if async gem not available)
|
|
296
|
+
batch_pipeline = SimpleFlow::Pipeline.new(concurrency: :async) do
|
|
297
|
+
step :load_batch, batch_loader, depends_on: []
|
|
298
|
+
step :process_batch, batch_processor, depends_on: [:load_batch]
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
# Mix concurrency models in the same application!
|
|
302
|
+
user_result = user_pipeline.call_parallel(user_data) # Uses threads
|
|
303
|
+
batch_result = batch_pipeline.call_parallel(batch_data) # Uses async
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
**Concurrency options:**
|
|
307
|
+
- `:auto` (default) - Auto-detects best option (async if available, otherwise threads)
|
|
308
|
+
- `:threads` - Always uses Ruby threads (simpler, works with any gems)
|
|
309
|
+
- `:async` - Requires async gem (efficient for high-concurrency workloads)
|
|
310
|
+
|
|
311
|
+
**[Learn more →](https://madbomber.github.io/simple_flow/guides/choosing-concurrency-model/)**
|
|
312
|
+
|
|
313
|
+
**[Parallel execution →](https://madbomber.github.io/simple_flow/concurrent/parallel-steps/)**
|
|
314
|
+
|
|
315
|
+
## Visualization
|
|
316
|
+
|
|
317
|
+
Visualize your pipelines to understand execution flow:
|
|
318
|
+
|
|
319
|
+
```ruby
|
|
320
|
+
pipeline = SimpleFlow::Pipeline.new do
|
|
321
|
+
step :load, loader, depends_on: []
|
|
322
|
+
step :transform, transformer, depends_on: [:load]
|
|
323
|
+
step :validate, validator, depends_on: [:transform]
|
|
324
|
+
step :save, saver, depends_on: [:validate]
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
# ASCII visualization
|
|
328
|
+
puts pipeline.visualize_ascii
|
|
329
|
+
|
|
330
|
+
# Export to Graphviz
|
|
331
|
+
File.write('pipeline.dot', pipeline.visualize_dot)
|
|
332
|
+
|
|
333
|
+
# Export to Mermaid
|
|
334
|
+
File.write('pipeline.mmd', pipeline.visualize_mermaid)
|
|
335
|
+
|
|
336
|
+
# View execution plan
|
|
337
|
+
puts pipeline.execution_plan
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
**Generated Mermaid diagram:**
|
|
341
|
+
|
|
342
|
+
```mermaid
|
|
343
|
+
graph TB
|
|
344
|
+
load --> transform
|
|
345
|
+
transform --> validate
|
|
346
|
+
validate --> save
|
|
347
|
+
|
|
348
|
+
style load fill:#e1f5ff
|
|
349
|
+
style transform fill:#fff3cd
|
|
350
|
+
style validate fill:#fce4ec
|
|
351
|
+
style save fill:#d4edda
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
**[Learn more →](https://madbomber.github.io/simple_flow/getting-started/examples/)**
|
|
355
|
+
|
|
356
|
+
## Real-World Example
|
|
357
|
+
|
|
358
|
+
E-commerce order processing pipeline:
|
|
359
|
+
|
|
360
|
+
```ruby
|
|
361
|
+
pipeline = SimpleFlow::Pipeline.new do
|
|
362
|
+
use SimpleFlow::MiddleWare::Logging
|
|
363
|
+
use SimpleFlow::MiddleWare::Instrumentation, api_key: 'orders'
|
|
364
|
+
|
|
365
|
+
step :validate_order, ->(r) {
|
|
366
|
+
# Validation logic
|
|
367
|
+
r.continue(r.value)
|
|
368
|
+
}, depends_on: []
|
|
369
|
+
|
|
370
|
+
# Run in parallel
|
|
371
|
+
step :check_inventory, ->(r) {
|
|
372
|
+
inventory = InventoryService.check(r.value[:items])
|
|
373
|
+
r.with_context(:inventory, inventory).continue(r.value)
|
|
374
|
+
}, depends_on: [:validate_order]
|
|
375
|
+
|
|
376
|
+
step :calculate_shipping, ->(r) {
|
|
377
|
+
shipping = ShippingService.calculate(r.value[:address])
|
|
378
|
+
r.with_context(:shipping, shipping).continue(r.value)
|
|
379
|
+
}, depends_on: [:validate_order]
|
|
380
|
+
|
|
381
|
+
# Wait for parallel steps
|
|
382
|
+
step :process_payment, ->(r) {
|
|
383
|
+
payment = PaymentService.charge(r.value, r.context)
|
|
384
|
+
r.with_context(:payment, payment).continue(r.value)
|
|
385
|
+
}, depends_on: [:check_inventory, :calculate_shipping]
|
|
386
|
+
|
|
387
|
+
step :send_confirmation, ->(r) {
|
|
388
|
+
EmailService.send_confirmation(r.value, r.context)
|
|
389
|
+
r.continue(r.value)
|
|
390
|
+
}, depends_on: [:process_payment]
|
|
391
|
+
end
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
**Execution flow:**
|
|
395
|
+
|
|
396
|
+
```mermaid
|
|
397
|
+
graph TB
|
|
398
|
+
V[validate_order] --> I[check_inventory]
|
|
399
|
+
V --> S[calculate_shipping]
|
|
400
|
+
I --> P[process_payment]
|
|
401
|
+
S --> P
|
|
402
|
+
P --> C[send_confirmation]
|
|
403
|
+
|
|
404
|
+
style V fill:#e1f5ff
|
|
405
|
+
style I fill:#fff3cd
|
|
406
|
+
style S fill:#fff3cd
|
|
407
|
+
style P fill:#fce4ec
|
|
408
|
+
style C fill:#d4edda
|
|
409
|
+
|
|
410
|
+
classDef parallel fill:#fff3cd,stroke:#ffc107,stroke-width:3px
|
|
411
|
+
class I,S parallel
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
## Testing
|
|
415
|
+
|
|
416
|
+
SimpleFlow has excellent test coverage:
|
|
417
|
+
|
|
418
|
+
```bash
|
|
419
|
+
bundle exec rake test
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
**Test Results:**
|
|
423
|
+
- ✅ 134 tests passing
|
|
424
|
+
- ✅ 480 assertions
|
|
425
|
+
- ✅ 95.57% line coverage
|
|
426
|
+
|
|
427
|
+
**[Testing Guide →](https://madbomber.github.io/simple_flow/development/testing/)**
|
|
428
|
+
|
|
429
|
+
## Documentation
|
|
430
|
+
|
|
431
|
+
📚 **Comprehensive documentation available at [madbomber.github.io/simple_flow](https://madbomber.github.io/simple_flow)**
|
|
432
|
+
|
|
433
|
+
### Key Resources
|
|
434
|
+
|
|
435
|
+
- [Getting Started Guide](https://madbomber.github.io/simple_flow/getting-started/quick-start/) - Quick introduction
|
|
436
|
+
- [Core Concepts](https://madbomber.github.io/simple_flow/core-concepts/overview/) - Understanding the fundamentals
|
|
437
|
+
- [Parallel Execution](https://madbomber.github.io/simple_flow/concurrent/parallel-steps/) - Concurrent processing
|
|
438
|
+
- [Guides](https://madbomber.github.io/simple_flow/guides/error-handling/) - Error handling, validation, workflows
|
|
439
|
+
- [API Reference](https://madbomber.github.io/simple_flow/api/result/) - Complete API documentation
|
|
440
|
+
- [Contributing](https://madbomber.github.io/simple_flow/development/contributing/) - How to contribute
|
|
441
|
+
|
|
442
|
+
## Examples
|
|
443
|
+
|
|
444
|
+
Check out the `examples/` directory for comprehensive examples:
|
|
445
|
+
|
|
446
|
+
1. `01_basic_pipeline.rb` - Basic sequential processing
|
|
447
|
+
2. `02_error_handling.rb` - Error handling patterns
|
|
448
|
+
3. `03_middleware.rb` - Middleware usage
|
|
449
|
+
4. `04_parallel_automatic.rb` - Automatic parallel discovery
|
|
450
|
+
5. `05_parallel_explicit.rb` - Explicit parallel blocks
|
|
451
|
+
6. `06_real_world_ecommerce.rb` - Complete e-commerce workflow
|
|
452
|
+
7. `07_real_world_etl.rb` - ETL pipeline example
|
|
453
|
+
8. `08_graph_visualization.rb` - Manual visualization
|
|
454
|
+
9. `09_pipeline_visualization.rb` - Direct pipeline visualization
|
|
455
|
+
10. `10_concurrency_control.rb` - Per-pipeline concurrency control
|
|
456
|
+
11. `11_sequential_dependencies.rb` - Sequential step dependencies and halting
|
|
457
|
+
12. `12_none_constant.rb` - Using reserved dependency symbols `:none` and `:nothing`
|
|
458
|
+
|
|
459
|
+
## Requirements
|
|
460
|
+
|
|
461
|
+
- Ruby 3.2 or higher
|
|
462
|
+
- Optional: `async` gem (~> 2.0) for parallel execution
|
|
463
|
+
|
|
464
|
+
## License
|
|
465
|
+
|
|
466
|
+
MIT License - See [LICENSE](LICENSE) file for details
|
|
467
|
+
|
|
468
|
+
## Contributing
|
|
469
|
+
|
|
470
|
+
Contributions welcome! See [CONTRIBUTING.md](https://madbomber.github.io/simple_flow/development/contributing/) for guidelines.
|
|
471
|
+
|
|
472
|
+
## Links
|
|
473
|
+
|
|
474
|
+
- 🏠 [Homepage](https://github.com/MadBomber/simple_flow)
|
|
475
|
+
- 📚 [Documentation](https://madbomber.github.io/simple_flow)
|
|
476
|
+
- 🐛 [Issue Tracker](https://github.com/MadBomber/simple_flow/issues)
|
|
477
|
+
- 📝 [Changelog](CHANGELOG.md)
|
|
478
|
+
|
|
479
|
+
---
|
|
480
|
+
|
|
481
|
+
**Made with ❤️ by [Dewayne VanHoozer](https://github.com/MadBomber)**
|
data/Rakefile
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "bundler/gem_tasks"
|
|
4
|
+
require "rake/testtask"
|
|
5
|
+
|
|
6
|
+
Rake::TestTask.new(:test) do |t|
|
|
7
|
+
t.libs << "test"
|
|
8
|
+
t.libs << "lib"
|
|
9
|
+
t.test_files = FileList["test/**/*_test.rb"]
|
|
10
|
+
t.verbose = true
|
|
11
|
+
# Load test_helper before any tests run to ensure SimpleCov starts first
|
|
12
|
+
t.ruby_opts << "-rtest_helper"
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
task default: :test
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require 'bundler/setup'
|
|
5
|
+
require 'benchmark/ips'
|
|
6
|
+
require_relative '../lib/simple_flow'
|
|
7
|
+
|
|
8
|
+
puts '=' * 80
|
|
9
|
+
puts 'SimpleFlow Performance Benchmarks: Parallel vs Sequential Execution'
|
|
10
|
+
puts '=' * 80
|
|
11
|
+
puts
|
|
12
|
+
|
|
13
|
+
# Simulate I/O operations with different delays
|
|
14
|
+
def io_operation(delay = 0.01)
|
|
15
|
+
sleep delay
|
|
16
|
+
:completed
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Define steps that perform I/O operations
|
|
20
|
+
step1 = ->(result) { io_operation(0.01); result.continue(result.value + 1) }
|
|
21
|
+
step2 = ->(result) { io_operation(0.01); result.continue(result.value + 1) }
|
|
22
|
+
step3 = ->(result) { io_operation(0.01); result.continue(result.value + 1) }
|
|
23
|
+
step4 = ->(result) { io_operation(0.01); result.continue(result.value + 1) }
|
|
24
|
+
|
|
25
|
+
# Sequential pipeline
|
|
26
|
+
sequential_pipeline = SimpleFlow::Pipeline.new do
|
|
27
|
+
step step1
|
|
28
|
+
step step2
|
|
29
|
+
step step3
|
|
30
|
+
step step4
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Parallel pipeline
|
|
34
|
+
parallel_pipeline = SimpleFlow::Pipeline.new do
|
|
35
|
+
parallel do
|
|
36
|
+
step step1
|
|
37
|
+
step step2
|
|
38
|
+
step step3
|
|
39
|
+
step step4
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
puts "Benchmark: 4 I/O operations (0.01s each)"
|
|
44
|
+
puts "Expected sequential time: ~0.04s"
|
|
45
|
+
puts "Expected parallel time: ~0.01s"
|
|
46
|
+
puts
|
|
47
|
+
|
|
48
|
+
Benchmark.ips do |x|
|
|
49
|
+
x.config(time: 5, warmup: 2)
|
|
50
|
+
|
|
51
|
+
x.report('sequential') do
|
|
52
|
+
sequential_pipeline.call(SimpleFlow::Result.new(0))
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
x.report('parallel') do
|
|
56
|
+
parallel_pipeline.call(SimpleFlow::Result.new(0))
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
x.compare!
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
puts "\n" + '=' * 80
|
|
63
|
+
puts
|
|
64
|
+
|
|
65
|
+
# Benchmark with varying number of parallel steps
|
|
66
|
+
puts 'Benchmark: Scaling with different numbers of parallel steps'
|
|
67
|
+
puts '=' * 80
|
|
68
|
+
puts
|
|
69
|
+
|
|
70
|
+
[2, 4, 8, 16].each do |count|
|
|
71
|
+
steps = Array.new(count) { ->(r) { io_operation(0.005); r.continue(r.value) } }
|
|
72
|
+
|
|
73
|
+
seq_pipeline = SimpleFlow::Pipeline.new
|
|
74
|
+
steps.each { |s| seq_pipeline.step(s) }
|
|
75
|
+
|
|
76
|
+
par_pipeline = SimpleFlow::Pipeline.new do
|
|
77
|
+
parallel do
|
|
78
|
+
steps.each { |s| step(s) }
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
seq_time = Benchmark.realtime do
|
|
83
|
+
seq_pipeline.call(SimpleFlow::Result.new(nil))
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
par_time = Benchmark.realtime do
|
|
87
|
+
par_pipeline.call(SimpleFlow::Result.new(nil))
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
speedup = seq_time / par_time
|
|
91
|
+
|
|
92
|
+
puts "\n#{count} steps (0.005s each):"
|
|
93
|
+
puts " Sequential: #{(seq_time * 1000).round(2)}ms"
|
|
94
|
+
puts " Parallel: #{(par_time * 1000).round(2)}ms"
|
|
95
|
+
puts " Speedup: #{speedup.round(2)}x"
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
puts "\n" + '=' * 80
|