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.
Files changed (80) hide show
  1. checksums.yaml +7 -0
  2. data/.envrc +1 -0
  3. data/.github/workflows/deploy-github-pages.yml +52 -0
  4. data/.rubocop.yml +57 -0
  5. data/CHANGELOG.md +4 -0
  6. data/COMMITS.md +196 -0
  7. data/LICENSE +21 -0
  8. data/README.md +481 -0
  9. data/Rakefile +15 -0
  10. data/benchmarks/parallel_vs_sequential.rb +98 -0
  11. data/benchmarks/pipeline_overhead.rb +130 -0
  12. data/docs/api/middleware.md +468 -0
  13. data/docs/api/parallel-step.md +363 -0
  14. data/docs/api/pipeline.md +382 -0
  15. data/docs/api/result.md +375 -0
  16. data/docs/concurrent/best-practices.md +687 -0
  17. data/docs/concurrent/introduction.md +246 -0
  18. data/docs/concurrent/parallel-steps.md +418 -0
  19. data/docs/concurrent/performance.md +481 -0
  20. data/docs/core-concepts/flow-control.md +452 -0
  21. data/docs/core-concepts/middleware.md +389 -0
  22. data/docs/core-concepts/overview.md +219 -0
  23. data/docs/core-concepts/pipeline.md +315 -0
  24. data/docs/core-concepts/result.md +168 -0
  25. data/docs/core-concepts/steps.md +391 -0
  26. data/docs/development/benchmarking.md +443 -0
  27. data/docs/development/contributing.md +380 -0
  28. data/docs/development/dagwood-concepts.md +435 -0
  29. data/docs/development/testing.md +514 -0
  30. data/docs/getting-started/examples.md +197 -0
  31. data/docs/getting-started/installation.md +62 -0
  32. data/docs/getting-started/quick-start.md +218 -0
  33. data/docs/guides/choosing-concurrency-model.md +441 -0
  34. data/docs/guides/complex-workflows.md +440 -0
  35. data/docs/guides/data-fetching.md +478 -0
  36. data/docs/guides/error-handling.md +635 -0
  37. data/docs/guides/file-processing.md +505 -0
  38. data/docs/guides/validation-patterns.md +496 -0
  39. data/docs/index.md +169 -0
  40. data/examples/.gitignore +3 -0
  41. data/examples/01_basic_pipeline.rb +112 -0
  42. data/examples/02_error_handling.rb +178 -0
  43. data/examples/03_middleware.rb +186 -0
  44. data/examples/04_parallel_automatic.rb +221 -0
  45. data/examples/05_parallel_explicit.rb +279 -0
  46. data/examples/06_real_world_ecommerce.rb +288 -0
  47. data/examples/07_real_world_etl.rb +277 -0
  48. data/examples/08_graph_visualization.rb +246 -0
  49. data/examples/09_pipeline_visualization.rb +266 -0
  50. data/examples/10_concurrency_control.rb +235 -0
  51. data/examples/11_sequential_dependencies.rb +243 -0
  52. data/examples/12_none_constant.rb +161 -0
  53. data/examples/README.md +374 -0
  54. data/examples/regression_test/01_basic_pipeline.txt +38 -0
  55. data/examples/regression_test/02_error_handling.txt +92 -0
  56. data/examples/regression_test/03_middleware.txt +61 -0
  57. data/examples/regression_test/04_parallel_automatic.txt +86 -0
  58. data/examples/regression_test/05_parallel_explicit.txt +80 -0
  59. data/examples/regression_test/06_real_world_ecommerce.txt +53 -0
  60. data/examples/regression_test/07_real_world_etl.txt +58 -0
  61. data/examples/regression_test/08_graph_visualization.txt +429 -0
  62. data/examples/regression_test/09_pipeline_visualization.txt +305 -0
  63. data/examples/regression_test/10_concurrency_control.txt +96 -0
  64. data/examples/regression_test/11_sequential_dependencies.txt +86 -0
  65. data/examples/regression_test/12_none_constant.txt +64 -0
  66. data/examples/regression_test.rb +105 -0
  67. data/lib/simple_flow/dependency_graph.rb +120 -0
  68. data/lib/simple_flow/dependency_graph_visualizer.rb +326 -0
  69. data/lib/simple_flow/middleware.rb +36 -0
  70. data/lib/simple_flow/parallel_executor.rb +80 -0
  71. data/lib/simple_flow/pipeline.rb +405 -0
  72. data/lib/simple_flow/result.rb +88 -0
  73. data/lib/simple_flow/step_tracker.rb +58 -0
  74. data/lib/simple_flow/version.rb +5 -0
  75. data/lib/simple_flow.rb +41 -0
  76. data/mkdocs.yml +146 -0
  77. data/pipeline_graph.dot +51 -0
  78. data/pipeline_graph.html +60 -0
  79. data/pipeline_graph.mmd +19 -0
  80. metadata +127 -0
@@ -0,0 +1,266 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require_relative '../lib/simple_flow'
5
+ require 'timecop'
6
+ Timecop.travel(Time.local(2001, 9, 11, 7, 0, 0))
7
+
8
+ # Direct pipeline visualization - no need to recreate dependency structure!
9
+
10
+ puts "=" * 60
11
+ puts "Direct Pipeline Visualization"
12
+ puts "=" * 60
13
+ puts
14
+
15
+ # Example 1: Visualize directly from pipeline
16
+ puts "Example 1: Simple Pipeline Visualization"
17
+ puts "-" * 60
18
+ puts
19
+
20
+ pipeline = SimpleFlow::Pipeline.new do
21
+ step :validate, ->(result) {
22
+ result.with_context(:validated, true).continue(result.value)
23
+ }, depends_on: :none
24
+
25
+ step :fetch_data, ->(result) {
26
+ result.with_context(:data, [1, 2, 3]).continue(result.value)
27
+ }, depends_on: [:validate]
28
+
29
+ step :process_data, ->(result) {
30
+ result.continue(result.value * 2)
31
+ }, depends_on: [:fetch_data]
32
+ end
33
+
34
+ # Visualize directly - no manual graph creation needed!
35
+ puts pipeline.visualize_ascii
36
+ puts
37
+
38
+ # Example 2: E-commerce pipeline with parallel steps
39
+ puts "\n" + "=" * 60
40
+ puts "Example 2: E-commerce Pipeline (Automatic Visualization)"
41
+ puts "=" * 60
42
+ puts
43
+
44
+ ecommerce_pipeline = SimpleFlow::Pipeline.new do
45
+ step :validate_order, ->(result) {
46
+ result.continue(result.value)
47
+ }, depends_on: :none
48
+
49
+ # These will run in parallel
50
+ step :check_inventory, ->(result) {
51
+ result.with_context(:inventory, :ok).continue(result.value)
52
+ }, depends_on: [:validate_order]
53
+
54
+ step :calculate_shipping, ->(result) {
55
+ result.with_context(:shipping, 10).continue(result.value)
56
+ }, depends_on: [:validate_order]
57
+
58
+ step :calculate_totals, ->(result) {
59
+ result.continue(result.value)
60
+ }, depends_on: [:check_inventory, :calculate_shipping]
61
+
62
+ step :process_payment, ->(result) {
63
+ result.continue(result.value)
64
+ }, depends_on: [:calculate_totals]
65
+
66
+ step :reserve_inventory, ->(result) {
67
+ result.continue(result.value)
68
+ }, depends_on: [:process_payment]
69
+
70
+ step :create_shipment, ->(result) {
71
+ result.continue(result.value)
72
+ }, depends_on: [:reserve_inventory]
73
+
74
+ # These will run in parallel
75
+ step :send_email, ->(result) {
76
+ result.continue(result.value)
77
+ }, depends_on: [:create_shipment]
78
+
79
+ step :send_sms, ->(result) {
80
+ result.continue(result.value)
81
+ }, depends_on: [:create_shipment]
82
+
83
+ step :finalize_order, ->(result) {
84
+ result.continue("Order complete!")
85
+ }, depends_on: [:send_email, :send_sms]
86
+ end
87
+
88
+ # Display the visualization
89
+ puts ecommerce_pipeline.visualize_ascii
90
+ puts
91
+
92
+ # Example 3: Get execution plan
93
+ puts "\n" + "=" * 60
94
+ puts "Example 3: Execution Plan (Direct from Pipeline)"
95
+ puts "=" * 60
96
+ puts
97
+
98
+ puts ecommerce_pipeline.execution_plan
99
+ puts
100
+
101
+ # Example 4: Export to different formats
102
+ puts "\n" + "=" * 60
103
+ puts "Example 4: Export Formats (Direct from Pipeline)"
104
+ puts "=" * 60
105
+ puts
106
+
107
+ # Export to Graphviz DOT
108
+ File.write('pipeline_graph.dot', ecommerce_pipeline.visualize_dot)
109
+ puts "✓ Exported to Graphviz DOT: pipeline_graph.dot"
110
+ puts " Generate image: dot -Tpng pipeline_graph.dot -o pipeline.png"
111
+ puts
112
+
113
+ # Export to Mermaid
114
+ File.write('pipeline_graph.mmd', ecommerce_pipeline.visualize_mermaid)
115
+ puts "✓ Exported to Mermaid: pipeline_graph.mmd"
116
+ puts " View at: https://mermaid.live/"
117
+ puts
118
+
119
+ # Export to HTML (need the visualizer object for this)
120
+ if visualizer = ecommerce_pipeline.visualize
121
+ File.write('pipeline_graph.html', visualizer.to_html(title: "E-commerce Pipeline"))
122
+ puts "✓ Exported to HTML: pipeline_graph.html"
123
+ puts " Open in browser for interactive visualization"
124
+ end
125
+ puts
126
+
127
+ # Example 5: ETL Pipeline
128
+ puts "\n" + "=" * 60
129
+ puts "Example 5: ETL Pipeline Visualization"
130
+ puts "=" * 60
131
+ puts
132
+
133
+ etl_pipeline = SimpleFlow::Pipeline.new do
134
+ # Extract phase - all run in parallel
135
+ step :extract_users, ->(result) {
136
+ result.with_context(:users, []).continue(result.value)
137
+ }, depends_on: :none
138
+
139
+ step :extract_orders, ->(result) {
140
+ result.with_context(:orders, []).continue(result.value)
141
+ }, depends_on: :none
142
+
143
+ step :extract_products, ->(result) {
144
+ result.with_context(:products, []).continue(result.value)
145
+ }, depends_on: :none
146
+
147
+ # Transform phase - all run in parallel after extraction
148
+ step :transform_users, ->(result) {
149
+ result.continue(result.value)
150
+ }, depends_on: [:extract_users]
151
+
152
+ step :transform_orders, ->(result) {
153
+ result.continue(result.value)
154
+ }, depends_on: [:extract_orders]
155
+
156
+ step :transform_products, ->(result) {
157
+ result.continue(result.value)
158
+ }, depends_on: [:extract_products]
159
+
160
+ # Aggregate phase - can run in parallel
161
+ step :aggregate_user_stats, ->(result) {
162
+ result.continue(result.value)
163
+ }, depends_on: [:transform_users, :transform_orders]
164
+
165
+ step :aggregate_category_stats, ->(result) {
166
+ result.continue(result.value)
167
+ }, depends_on: [:transform_products]
168
+
169
+ # Validate
170
+ step :validate_data, ->(result) {
171
+ result.continue(result.value)
172
+ }, depends_on: [:aggregate_user_stats]
173
+
174
+ # Load
175
+ step :prepare_output, ->(result) {
176
+ result.continue("ETL Complete!")
177
+ }, depends_on: [:validate_data, :aggregate_category_stats]
178
+ end
179
+
180
+ puts etl_pipeline.execution_plan
181
+ puts
182
+
183
+ # Example 6: Check if pipeline can be visualized
184
+ puts "\n" + "=" * 60
185
+ puts "Example 6: Checking Visualization Availability"
186
+ puts "=" * 60
187
+ puts
188
+
189
+ # Pipeline with named steps - can be visualized
190
+ named_pipeline = SimpleFlow::Pipeline.new do
191
+ step :step_a, ->(r) { r.continue(r.value) }, depends_on: :none
192
+ step :step_b, ->(r) { r.continue(r.value) }, depends_on: [:step_a]
193
+ end
194
+
195
+ # Pipeline with unnamed steps - cannot be auto-visualized
196
+ unnamed_pipeline = SimpleFlow::Pipeline.new do
197
+ step ->(r) { r.continue(r.value) }
198
+ step ->(r) { r.continue(r.value) }
199
+ end
200
+
201
+ puts "Named pipeline has dependency graph? #{!named_pipeline.dependency_graph.nil?}"
202
+ puts "Unnamed pipeline has dependency graph? #{!unnamed_pipeline.dependency_graph.nil?}"
203
+ puts
204
+ puts "Note: Only pipelines with named steps and dependencies can be auto-visualized"
205
+ puts
206
+
207
+ # Example 7: Working with the dependency graph directly
208
+ puts "\n" + "=" * 60
209
+ puts "Example 7: Advanced - Access Dependency Graph"
210
+ puts "=" * 60
211
+ puts
212
+
213
+ if graph = ecommerce_pipeline.dependency_graph
214
+ puts "Pipeline dependency information:"
215
+ puts " Total steps: #{graph.dependencies.size}"
216
+ puts " Execution phases: #{graph.parallel_order.size}"
217
+ puts " Parallel groups:"
218
+ graph.parallel_order.each_with_index do |group, idx|
219
+ puts " Phase #{idx + 1}: #{group.join(', ')}"
220
+ end
221
+ end
222
+ puts
223
+
224
+ # Example 8: Compare different pipeline structures
225
+ puts "\n" + "=" * 60
226
+ puts "Example 8: Pipeline Structure Comparison"
227
+ puts "=" * 60
228
+ puts
229
+
230
+ # Linear pipeline
231
+ linear = SimpleFlow::Pipeline.new do
232
+ step :step1, ->(r) { r.continue(r.value) }, depends_on: :none
233
+ step :step2, ->(r) { r.continue(r.value) }, depends_on: [:step1]
234
+ step :step3, ->(r) { r.continue(r.value) }, depends_on: [:step2]
235
+ step :step4, ->(r) { r.continue(r.value) }, depends_on: [:step3]
236
+ end
237
+
238
+ # Parallel pipeline
239
+ parallel = SimpleFlow::Pipeline.new do
240
+ step :start, ->(r) { r.continue(r.value) }, depends_on: :none
241
+ step :task1, ->(r) { r.continue(r.value) }, depends_on: [:start]
242
+ step :task2, ->(r) { r.continue(r.value) }, depends_on: [:start]
243
+ step :task3, ->(r) { r.continue(r.value) }, depends_on: [:start]
244
+ step :end, ->(r) { r.continue(r.value) }, depends_on: [:task1, :task2, :task3]
245
+ end
246
+
247
+ puts "Linear Pipeline:"
248
+ puts linear.execution_plan
249
+ puts
250
+
251
+ puts "\nParallel Pipeline:"
252
+ puts parallel.execution_plan
253
+ puts
254
+
255
+ puts "=" * 60
256
+ puts "Direct pipeline visualization completed!"
257
+ puts
258
+ puts "Key Takeaway:"
259
+ puts " No need to manually recreate dependency structures!"
260
+ puts " Just call pipeline.visualize_ascii, pipeline.visualize_dot, etc."
261
+ puts
262
+ puts "Generated files:"
263
+ puts " - pipeline_graph.dot"
264
+ puts " - pipeline_graph.mmd"
265
+ puts " - pipeline_graph.html"
266
+ puts "=" * 60
@@ -0,0 +1,235 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require_relative '../lib/simple_flow'
5
+ require 'timecop'
6
+ Timecop.travel(Time.local(2001, 9, 11, 7, 0, 0))
7
+
8
+ # Controlling Concurrency Model Per Pipeline
9
+
10
+ puts "=" * 60
11
+ puts "Per-Pipeline Concurrency Control"
12
+ puts "=" * 60
13
+ puts
14
+
15
+ # Check async availability
16
+ async_available = SimpleFlow::ParallelExecutor.async_available?
17
+ puts "Async gem available: #{async_available ? '✓ Yes' : '✗ No'}"
18
+ puts
19
+
20
+ # Example 1: Auto-detect (default behavior)
21
+ puts "Example 1: Auto-Detect Concurrency (Default)"
22
+ puts "-" * 60
23
+ puts
24
+
25
+ pipeline_auto = SimpleFlow::Pipeline.new do # concurrency: :auto is default
26
+ step ->(result) {
27
+ puts " Processing with auto-detected concurrency..."
28
+ result.continue(result.value)
29
+ }
30
+ end
31
+
32
+ puts "Pipeline concurrency setting: #{pipeline_auto.concurrency}"
33
+ puts "Will use: #{async_available ? 'Async (fiber-based)' : 'Threads'}"
34
+ puts
35
+
36
+ # Example 2: Force threads (even if async is available)
37
+ puts "\nExample 2: Force Threads"
38
+ puts "-" * 60
39
+ puts
40
+
41
+ pipeline_threads = SimpleFlow::Pipeline.new(concurrency: :threads) do
42
+ parallel do
43
+ step ->(result) {
44
+ puts " [Thread-based] Task A running..."
45
+ sleep 0.05
46
+ result.with_context(:task_a, :done).continue(result.value)
47
+ }
48
+
49
+ step ->(result) {
50
+ puts " [Thread-based] Task B running..."
51
+ sleep 0.05
52
+ result.with_context(:task_b, :done).continue(result.value)
53
+ }
54
+
55
+ step ->(result) {
56
+ puts " [Thread-based] Task C running..."
57
+ sleep 0.05
58
+ result.with_context(:task_c, :done).continue(result.value)
59
+ }
60
+ end
61
+ end
62
+
63
+ puts "Pipeline concurrency setting: #{pipeline_threads.concurrency}"
64
+ puts "Will use: Ruby Threads (even if async is available)"
65
+
66
+ result = pipeline_threads.call(SimpleFlow::Result.new(nil))
67
+ puts "Result context: #{result.context}"
68
+ puts
69
+
70
+ # Example 3: Force async (requires async gem)
71
+ puts "\nExample 3: Force Async"
72
+ puts "-" * 60
73
+ puts
74
+
75
+ if async_available
76
+ pipeline_async = SimpleFlow::Pipeline.new(concurrency: :async) do
77
+ step :validate, ->(result) {
78
+ puts " [Async] Validating..."
79
+ result.continue(result.value)
80
+ }, depends_on: :none
81
+
82
+ step :fetch_a, ->(result) {
83
+ puts " [Async] Fetching A..."
84
+ sleep 0.05
85
+ result.with_context(:a, :done).continue(result.value)
86
+ }, depends_on: [:validate]
87
+
88
+ step :fetch_b, ->(result) {
89
+ puts " [Async] Fetching B..."
90
+ sleep 0.05
91
+ result.with_context(:b, :done).continue(result.value)
92
+ }, depends_on: [:validate]
93
+
94
+ step :merge, ->(result) {
95
+ puts " [Async] Merging results..."
96
+ result.continue("Complete")
97
+ }, depends_on: [:fetch_a, :fetch_b]
98
+ end
99
+
100
+ puts "Pipeline concurrency setting: #{pipeline_async.concurrency}"
101
+ puts "Will use: Async (fiber-based concurrency)"
102
+
103
+ result2 = pipeline_async.call_parallel(SimpleFlow::Result.new(nil))
104
+ puts "Result: #{result2.value}"
105
+ puts "Context: #{result2.context}"
106
+ else
107
+ puts "Cannot create async pipeline - async gem not available"
108
+ puts "Would raise: ArgumentError: Concurrency set to :async but async gem is not available"
109
+ end
110
+ puts
111
+
112
+ # Example 4: Different pipelines, different concurrency
113
+ puts "\nExample 4: Mixed Concurrency in Same Application"
114
+ puts "-" * 60
115
+ puts
116
+
117
+ # Low-volume user pipeline - threads are simpler
118
+ user_pipeline = SimpleFlow::Pipeline.new(concurrency: :threads) do
119
+ step :validate, ->(result) {
120
+ puts " [User/Threads] Validating user..."
121
+ result.continue(result.value)
122
+ }, depends_on: :none
123
+
124
+ step :fetch_profile, ->(result) {
125
+ puts " [User/Threads] Fetching profile..."
126
+ sleep 0.02
127
+ result.with_context(:profile, { name: "John" }).continue(result.value)
128
+ }, depends_on: [:validate]
129
+
130
+ step :fetch_settings, ->(result) {
131
+ puts " [User/Threads] Fetching settings..."
132
+ sleep 0.02
133
+ result.with_context(:settings, { theme: "dark" }).continue(result.value)
134
+ }, depends_on: [:validate]
135
+ end
136
+
137
+ # High-volume batch pipeline - use async if available
138
+ batch_concurrency = async_available ? :async : :threads
139
+ batch_pipeline = SimpleFlow::Pipeline.new(concurrency: batch_concurrency) do
140
+ step :load, ->(result) {
141
+ puts " [Batch/#{batch_concurrency.to_s.capitalize}] Loading batch..."
142
+ result.continue(result.value)
143
+ }, depends_on: :none
144
+
145
+ step :process_batch_1, ->(result) {
146
+ puts " [Batch/#{batch_concurrency.to_s.capitalize}] Processing batch 1..."
147
+ sleep 0.02
148
+ result.with_context(:batch_1, :done).continue(result.value)
149
+ }, depends_on: [:load]
150
+
151
+ step :process_batch_2, ->(result) {
152
+ puts " [Batch/#{batch_concurrency.to_s.capitalize}] Processing batch 2..."
153
+ sleep 0.02
154
+ result.with_context(:batch_2, :done).continue(result.value)
155
+ }, depends_on: [:load]
156
+ end
157
+
158
+ puts "User pipeline uses: #{user_pipeline.concurrency}"
159
+ puts "Batch pipeline uses: #{batch_pipeline.concurrency}"
160
+ puts
161
+
162
+ puts "Running user pipeline..."
163
+ user_result = user_pipeline.call_parallel(SimpleFlow::Result.new({ user_id: 123 }))
164
+ puts "User result: #{user_result.context}"
165
+
166
+ puts "\nRunning batch pipeline..."
167
+ batch_result = batch_pipeline.call_parallel(SimpleFlow::Result.new({ batch_id: 456 }))
168
+ puts "Batch result: #{batch_result.context}"
169
+ puts
170
+
171
+ # Example 5: Error handling for invalid concurrency
172
+ puts "\nExample 5: Error Handling"
173
+ puts "-" * 60
174
+ puts
175
+
176
+ puts "Valid options: :auto, :threads, :async"
177
+ puts
178
+
179
+ begin
180
+ invalid_pipeline = SimpleFlow::Pipeline.new(concurrency: :invalid) do
181
+ step ->(result) { result.continue(result.value) }
182
+ end
183
+ rescue ArgumentError => e
184
+ puts "✓ Caught expected error for invalid concurrency:"
185
+ puts " #{e.message}"
186
+ end
187
+
188
+ puts
189
+
190
+ unless async_available
191
+ begin
192
+ async_pipeline = SimpleFlow::Pipeline.new(concurrency: :async) do
193
+ step ->(result) { result.continue(result.value) }
194
+ end
195
+ rescue ArgumentError => e
196
+ puts "✓ Caught expected error when async not available:"
197
+ puts " #{e.message}"
198
+ end
199
+ end
200
+
201
+ puts
202
+
203
+ # Example 6: Checking pipeline concurrency setting
204
+ puts "\nExample 6: Inspecting Concurrency Settings"
205
+ puts "-" * 60
206
+ puts
207
+
208
+ pipelines = [
209
+ SimpleFlow::Pipeline.new, # default
210
+ SimpleFlow::Pipeline.new(concurrency: :auto), # explicit auto
211
+ SimpleFlow::Pipeline.new(concurrency: :threads), # threads
212
+ ]
213
+
214
+ if async_available
215
+ pipelines << SimpleFlow::Pipeline.new(concurrency: :async) # async
216
+ end
217
+
218
+ pipelines.each_with_index do |pipeline, index|
219
+ puts "Pipeline #{index + 1}:"
220
+ puts " Concurrency: #{pipeline.concurrency}"
221
+ puts " Async available: #{pipeline.async_available?}"
222
+ puts
223
+ end
224
+
225
+ puts "=" * 60
226
+ puts "Concurrency control examples completed!"
227
+ puts "=" * 60
228
+ puts
229
+ puts "Key Takeaways:"
230
+ puts " • concurrency: :auto (default) - auto-detects best option"
231
+ puts " • concurrency: :threads - always uses Ruby threads"
232
+ puts " • concurrency: :async - requires async gem, uses fibers"
233
+ puts " • Different pipelines can use different concurrency models"
234
+ puts " • Choose based on your specific workload requirements"
235
+ puts