cmdx 1.0.0 → 1.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 (169) hide show
  1. checksums.yaml +4 -4
  2. data/.cursor/prompts/rspec.md +20 -0
  3. data/.cursor/prompts/yardoc.md +8 -0
  4. data/.rubocop.yml +5 -0
  5. data/CHANGELOG.md +101 -49
  6. data/README.md +2 -1
  7. data/docs/ai_prompts.md +10 -0
  8. data/docs/basics/call.md +11 -2
  9. data/docs/basics/chain.md +10 -1
  10. data/docs/basics/context.md +9 -0
  11. data/docs/basics/setup.md +9 -0
  12. data/docs/callbacks.md +14 -37
  13. data/docs/configuration.md +68 -27
  14. data/docs/getting_started.md +11 -0
  15. data/docs/internationalization.md +148 -0
  16. data/docs/interruptions/exceptions.md +10 -1
  17. data/docs/interruptions/faults.md +11 -2
  18. data/docs/interruptions/halt.md +9 -0
  19. data/docs/logging.md +14 -4
  20. data/docs/middlewares.md +53 -43
  21. data/docs/outcomes/result.md +9 -0
  22. data/docs/outcomes/states.md +9 -0
  23. data/docs/outcomes/statuses.md +9 -0
  24. data/docs/parameters/coercions.md +58 -38
  25. data/docs/parameters/defaults.md +10 -1
  26. data/docs/parameters/definitions.md +9 -0
  27. data/docs/parameters/namespacing.md +9 -0
  28. data/docs/parameters/validations.md +8 -67
  29. data/docs/testing.md +22 -13
  30. data/docs/tips_and_tricks.md +9 -0
  31. data/docs/workflows.md +14 -4
  32. data/lib/cmdx/.DS_Store +0 -0
  33. data/lib/cmdx/callback.rb +36 -56
  34. data/lib/cmdx/callback_registry.rb +82 -73
  35. data/lib/cmdx/chain.rb +65 -122
  36. data/lib/cmdx/chain_inspector.rb +22 -115
  37. data/lib/cmdx/chain_serializer.rb +17 -148
  38. data/lib/cmdx/coercion.rb +49 -0
  39. data/lib/cmdx/coercion_registry.rb +94 -0
  40. data/lib/cmdx/coercions/array.rb +18 -36
  41. data/lib/cmdx/coercions/big_decimal.rb +21 -33
  42. data/lib/cmdx/coercions/boolean.rb +21 -40
  43. data/lib/cmdx/coercions/complex.rb +18 -31
  44. data/lib/cmdx/coercions/date.rb +20 -39
  45. data/lib/cmdx/coercions/date_time.rb +22 -39
  46. data/lib/cmdx/coercions/float.rb +19 -32
  47. data/lib/cmdx/coercions/hash.rb +22 -41
  48. data/lib/cmdx/coercions/integer.rb +20 -33
  49. data/lib/cmdx/coercions/rational.rb +20 -32
  50. data/lib/cmdx/coercions/string.rb +23 -31
  51. data/lib/cmdx/coercions/time.rb +24 -40
  52. data/lib/cmdx/coercions/virtual.rb +14 -31
  53. data/lib/cmdx/configuration.rb +57 -171
  54. data/lib/cmdx/context.rb +22 -165
  55. data/lib/cmdx/core_ext/hash.rb +42 -67
  56. data/lib/cmdx/core_ext/module.rb +35 -79
  57. data/lib/cmdx/core_ext/object.rb +63 -98
  58. data/lib/cmdx/correlator.rb +40 -156
  59. data/lib/cmdx/error.rb +37 -202
  60. data/lib/cmdx/errors.rb +165 -202
  61. data/lib/cmdx/fault.rb +55 -158
  62. data/lib/cmdx/faults.rb +26 -137
  63. data/lib/cmdx/immutator.rb +22 -109
  64. data/lib/cmdx/lazy_struct.rb +103 -187
  65. data/lib/cmdx/log_formatters/json.rb +14 -40
  66. data/lib/cmdx/log_formatters/key_value.rb +14 -40
  67. data/lib/cmdx/log_formatters/line.rb +14 -48
  68. data/lib/cmdx/log_formatters/logstash.rb +14 -57
  69. data/lib/cmdx/log_formatters/pretty_json.rb +14 -50
  70. data/lib/cmdx/log_formatters/pretty_key_value.rb +13 -46
  71. data/lib/cmdx/log_formatters/pretty_line.rb +16 -54
  72. data/lib/cmdx/log_formatters/raw.rb +19 -49
  73. data/lib/cmdx/logger.rb +20 -82
  74. data/lib/cmdx/logger_ansi.rb +18 -75
  75. data/lib/cmdx/logger_serializer.rb +24 -114
  76. data/lib/cmdx/middleware.rb +38 -60
  77. data/lib/cmdx/middleware_registry.rb +81 -77
  78. data/lib/cmdx/middlewares/correlate.rb +41 -226
  79. data/lib/cmdx/middlewares/timeout.rb +46 -185
  80. data/lib/cmdx/parameter.rb +120 -198
  81. data/lib/cmdx/parameter_evaluator.rb +231 -0
  82. data/lib/cmdx/parameter_inspector.rb +25 -56
  83. data/lib/cmdx/parameter_registry.rb +59 -84
  84. data/lib/cmdx/parameter_serializer.rb +23 -74
  85. data/lib/cmdx/railtie.rb +24 -107
  86. data/lib/cmdx/result.rb +254 -260
  87. data/lib/cmdx/result_ansi.rb +19 -85
  88. data/lib/cmdx/result_inspector.rb +27 -68
  89. data/lib/cmdx/result_logger.rb +18 -81
  90. data/lib/cmdx/result_serializer.rb +28 -132
  91. data/lib/cmdx/rspec/matchers.rb +28 -0
  92. data/lib/cmdx/rspec/result_matchers/be_executed.rb +42 -0
  93. data/lib/cmdx/rspec/result_matchers/be_failed_task.rb +94 -0
  94. data/lib/cmdx/rspec/result_matchers/be_skipped_task.rb +94 -0
  95. data/lib/cmdx/rspec/result_matchers/be_state_matchers.rb +59 -0
  96. data/lib/cmdx/rspec/result_matchers/be_status_matchers.rb +57 -0
  97. data/lib/cmdx/rspec/result_matchers/be_successful_task.rb +87 -0
  98. data/lib/cmdx/rspec/result_matchers/have_bad_outcome.rb +51 -0
  99. data/lib/cmdx/rspec/result_matchers/have_caused_failure.rb +58 -0
  100. data/lib/cmdx/rspec/result_matchers/have_chain_index.rb +59 -0
  101. data/lib/cmdx/rspec/result_matchers/have_context.rb +86 -0
  102. data/lib/cmdx/rspec/result_matchers/have_empty_metadata.rb +54 -0
  103. data/lib/cmdx/rspec/result_matchers/have_good_outcome.rb +52 -0
  104. data/lib/cmdx/rspec/result_matchers/have_metadata.rb +114 -0
  105. data/lib/cmdx/rspec/result_matchers/have_preserved_context.rb +66 -0
  106. data/lib/cmdx/rspec/result_matchers/have_received_thrown_failure.rb +64 -0
  107. data/lib/cmdx/rspec/result_matchers/have_runtime.rb +78 -0
  108. data/lib/cmdx/rspec/result_matchers/have_thrown_failure.rb +76 -0
  109. data/lib/cmdx/rspec/task_matchers/be_well_formed_task.rb +62 -0
  110. data/lib/cmdx/rspec/task_matchers/have_callback.rb +85 -0
  111. data/lib/cmdx/rspec/task_matchers/have_cmd_setting.rb +68 -0
  112. data/lib/cmdx/rspec/task_matchers/have_executed_callbacks.rb +92 -0
  113. data/lib/cmdx/rspec/task_matchers/have_middleware.rb +46 -0
  114. data/lib/cmdx/rspec/task_matchers/have_parameter.rb +181 -0
  115. data/lib/cmdx/task.rb +213 -425
  116. data/lib/cmdx/task_deprecator.rb +55 -0
  117. data/lib/cmdx/task_processor.rb +245 -0
  118. data/lib/cmdx/task_serializer.rb +22 -70
  119. data/lib/cmdx/utils/ansi_color.rb +13 -89
  120. data/lib/cmdx/utils/log_timestamp.rb +13 -42
  121. data/lib/cmdx/utils/monotonic_runtime.rb +13 -63
  122. data/lib/cmdx/utils/name_affix.rb +21 -71
  123. data/lib/cmdx/validator.rb +48 -0
  124. data/lib/cmdx/validator_registry.rb +86 -0
  125. data/lib/cmdx/validators/exclusion.rb +55 -94
  126. data/lib/cmdx/validators/format.rb +31 -85
  127. data/lib/cmdx/validators/inclusion.rb +65 -110
  128. data/lib/cmdx/validators/length.rb +117 -133
  129. data/lib/cmdx/validators/numeric.rb +123 -130
  130. data/lib/cmdx/validators/presence.rb +38 -79
  131. data/lib/cmdx/version.rb +1 -7
  132. data/lib/cmdx/workflow.rb +46 -339
  133. data/lib/cmdx.rb +1 -1
  134. data/lib/generators/cmdx/install_generator.rb +14 -31
  135. data/lib/generators/cmdx/task_generator.rb +39 -55
  136. data/lib/generators/cmdx/templates/install.rb +61 -11
  137. data/lib/generators/cmdx/workflow_generator.rb +41 -66
  138. data/lib/locales/ar.yml +35 -0
  139. data/lib/locales/cs.yml +35 -0
  140. data/lib/locales/da.yml +35 -0
  141. data/lib/locales/de.yml +35 -0
  142. data/lib/locales/el.yml +35 -0
  143. data/lib/locales/en.yml +19 -20
  144. data/lib/locales/es.yml +19 -20
  145. data/lib/locales/fi.yml +35 -0
  146. data/lib/locales/fr.yml +35 -0
  147. data/lib/locales/he.yml +35 -0
  148. data/lib/locales/hi.yml +35 -0
  149. data/lib/locales/it.yml +35 -0
  150. data/lib/locales/ja.yml +35 -0
  151. data/lib/locales/ko.yml +35 -0
  152. data/lib/locales/nl.yml +35 -0
  153. data/lib/locales/no.yml +35 -0
  154. data/lib/locales/pl.yml +35 -0
  155. data/lib/locales/pt.yml +35 -0
  156. data/lib/locales/ru.yml +35 -0
  157. data/lib/locales/sv.yml +35 -0
  158. data/lib/locales/th.yml +35 -0
  159. data/lib/locales/tr.yml +35 -0
  160. data/lib/locales/vi.yml +35 -0
  161. data/lib/locales/zh.yml +35 -0
  162. metadata +57 -8
  163. data/lib/cmdx/parameter_validator.rb +0 -81
  164. data/lib/cmdx/parameter_value.rb +0 -244
  165. data/lib/cmdx/parameters_inspector.rb +0 -72
  166. data/lib/cmdx/parameters_serializer.rb +0 -115
  167. data/lib/cmdx/rspec/result_matchers.rb +0 -917
  168. data/lib/cmdx/rspec/task_matchers.rb +0 -570
  169. data/lib/cmdx/validators/custom.rb +0 -102
@@ -2,255 +2,70 @@
2
2
 
3
3
  module CMDx
4
4
  module Middlewares
5
- ##
6
- # Correlation middleware for ensuring consistent correlation ID context during task execution.
7
- #
8
- # The Correlate middleware establishes and maintains correlation ID context throughout
9
- # task execution, enabling seamless request tracking across task boundaries. It ensures
10
- # that all tasks within an execution chain share the same correlation identifier for
11
- # comprehensive traceability and debugging.
12
- #
13
- # ## Correlation ID Precedence
14
- #
15
- # The middleware determines the correlation ID using the following precedence:
16
- # 1. **Explicit correlation ID** - Value provided during middleware initialization
17
- # 2. **Current thread correlation** - Existing correlation from `CMDx::Correlator.id`
18
- # 3. **Chain identifier** - The task's chain ID if no thread correlation exists
19
- # 4. **Generated UUID** - New correlation ID if none of the above is available
20
- #
21
- # ## Conditional Execution
22
- #
23
- # The middleware supports conditional execution using `:if` and `:unless` options:
24
- # - `:if` - Only applies correlation when the condition evaluates to true
25
- # - `:unless` - Only applies correlation when the condition evaluates to false
26
- # - Conditions can be Procs, method symbols, or boolean values
27
- #
28
- # ## Thread Safety
29
- #
30
- # The middleware uses `CMDx::Correlator.use` to establish a correlation context
31
- # that is automatically restored after task execution, ensuring thread-local
32
- # isolation and proper cleanup even in case of exceptions.
33
- #
34
- # ## Integration with CMDx Framework
35
- #
36
- # - **Automatic activation**: Can be applied globally or per-task via `use` directive
37
- # - **Chain integration**: Works seamlessly with CMDx::Chain correlation inheritance
38
- # - **Nested tasks**: Maintains correlation context across nested task calls
39
- # - **Exception safety**: Restores correlation context even when tasks fail
40
- #
41
- # @example Basic task-specific middleware application
42
- # class ProcessOrderTask < CMDx::Task
43
- # use CMDx::Middlewares::Correlate
44
- #
45
- # def call
46
- # # Task execution maintains correlation context
47
- # SendEmailTask.call(context) # Inherits same correlation
48
- # end
49
- # end
50
- #
51
- # @example Middleware with explicit correlation ID
52
- # class ProcessOrderTask < CMDx::Task
53
- # use CMDx::Middlewares::Correlate, id: "order-processing-123"
54
- #
55
- # def call
56
- # # Always uses "order-processing-123" as correlation ID
57
- # context.correlation_used = CMDx::Correlator.id
58
- # end
59
- # end
60
- #
61
- # result = ProcessOrderTask.call(order_id: 123)
62
- # result.context.correlation_used # => "order-processing-123"
63
- #
64
- # @example Middleware with dynamic correlation ID using procs
65
- # class ProcessOrderTask < CMDx::Task
66
- # use CMDx::Middlewares::Correlate, id: -> { "order-#{order_id}-#{Time.now.to_i}" }
67
- #
68
- # def call
69
- # # Uses dynamically generated correlation ID
70
- # context.correlation_used = CMDx::Correlator.id
71
- # end
72
- # end
73
- #
74
- # result = ProcessOrderTask.call(order_id: 456)
75
- # result.context.correlation_used # => "order-456-1703123456"
76
- #
77
- # @example Middleware with method-based correlation ID
78
- # class ProcessOrderTask < CMDx::Task
79
- # use CMDx::Middlewares::Correlate, id: :generate_order_correlation
80
- #
81
- # def call
82
- # # Uses correlation ID from generate_order_correlation method
83
- # context.correlation_used = CMDx::Correlator.id
84
- # end
85
- #
86
- # private
87
- #
88
- # def generate_order_correlation
89
- # "order-#{order_id}-#{context.request_id}"
90
- # end
91
- # end
92
- #
93
- # @example Conditional correlation based on environment
94
- # class ProcessOrderTask < CMDx::Task
95
- # use CMDx::Middlewares::Correlate, unless: -> { Rails.env.test? }
96
- #
97
- # def call
98
- # # Correlation only applied in non-test environments
99
- # context.order = Order.find(order_id)
100
- # end
101
- # end
102
- #
103
- # @example Conditional correlation based on task state
104
- # class ProcessOrderTask < CMDx::Task
105
- # use CMDx::Middlewares::Correlate, if: :correlation_required?
106
- #
107
- # def call
108
- # # Correlation applied only when correlation_required? returns true
109
- # context.order = Order.find(order_id)
110
- # end
111
- #
112
- # private
113
- #
114
- # def correlation_required?
115
- # context.tracking_enabled == true
116
- # end
117
- # end
118
- #
119
- # @example Nested task correlation propagation
120
- # class ParentTask < CMDx::Task
121
- # use CMDx::Middlewares::Correlate
122
- #
123
- # def call
124
- # # Correlation established at parent level
125
- # ChildTask.call(context)
126
- # end
127
- # end
128
- #
129
- # class ChildTask < CMDx::Task
130
- # use CMDx::Middlewares::Correlate
131
- #
132
- # def call
133
- # # Inherits parent's correlation ID
134
- # context.child_correlation = CMDx::Correlator.id
135
- # end
136
- # end
137
- #
138
- # @example Exception handling with correlation restoration
139
- # class RiskyTask < CMDx::Task
140
- # use CMDx::Middlewares::Correlate
141
- #
142
- # def call
143
- # raise StandardError, "Task failed"
144
- # end
145
- # end
146
- #
147
- # CMDx::Correlator.id = "original-correlation"
148
- #
149
- # begin
150
- # RiskyTask.call
151
- # rescue StandardError
152
- # CMDx::Correlator.id # => "original-correlation" (properly restored)
153
- # end
154
- #
155
- # @see CMDx::Correlator Thread-safe correlation ID management
156
- # @see CMDx::Chain Chain execution context with correlation inheritance
157
- # @see CMDx::Middleware Base middleware class
158
- # @since 1.0.0
5
+ # Middleware that manages correlation IDs for task execution tracing.
6
+ # Automatically generates or uses provided correlation IDs to track task execution
7
+ # across complex workflows, enabling better debugging and monitoring.
159
8
  class Correlate < CMDx::Middleware
160
9
 
161
- # @return [String, nil] The explicit correlation ID to use
10
+ # @return [String, Symbol, Proc, nil] The explicit correlation ID to use, or callable that generates one
11
+ attr_reader :id
12
+
162
13
  # @return [Hash] The conditional options for correlation application
163
- attr_reader :id, :conditional
14
+ attr_reader :conditional
164
15
 
165
- ##
166
- # Initializes the Correlate middleware with optional configuration.
16
+ # Initializes the correlation middleware with optional configuration.
167
17
  #
168
18
  # @param options [Hash] configuration options for the middleware
169
- # @option options [String, Symbol, Proc] :id explicit correlation ID to use (takes precedence over all other sources)
170
- # @option options [Proc, Symbol, Boolean] :if condition that must be true for middleware to execute
171
- # @option options [Proc, Symbol, Boolean] :unless condition that must be false for middleware to execute
19
+ # @option options [String, Symbol, Proc] :id explicit correlation ID or callable to generate one
20
+ # @option options [Symbol, Proc] :if condition that must be truthy to apply correlation
21
+ # @option options [Symbol, Proc] :unless condition that must be falsy to apply correlation
22
+ #
23
+ # @return [Correlate] new instance of the middleware
24
+ #
25
+ # @example Register with a middleware instance
26
+ # use :middleware, CMDx::Middlewares::Correlate.new(id: "request-123")
172
27
  #
173
- # @example Basic initialization
174
- # middleware = CMDx::Middlewares::Correlate.new
28
+ # @example Register with explicit ID
29
+ # use :middleware, CMDx::Middlewares::Correlate, id: "request-123"
175
30
  #
176
- # @example With explicit correlation ID
177
- # middleware = CMDx::Middlewares::Correlate.new(id: "api-request-123")
31
+ # @example Register with dynamic ID generation
32
+ # use :middleware, CMDx::Middlewares::Correlate, id: -> { SecureRandom.uuid }
178
33
  #
179
- # @example With conditional execution
180
- # middleware = CMDx::Middlewares::Correlate.new(unless: -> { Rails.env.test? })
181
- # middleware = CMDx::Middlewares::Correlate.new(if: :correlation_enabled?)
34
+ # @example Register with conditions
35
+ # use :middleware, CMDx::Middlewares::Correlate, if: :production?, unless: :testing?
182
36
  def initialize(options = {})
183
37
  @id = options[:id]
184
38
  @conditional = options.slice(:if, :unless)
185
39
  end
186
40
 
187
- ##
188
- # Executes the task within a managed correlation context.
189
- #
190
- # First evaluates any conditional execution rules (`:if` or `:unless` options).
191
- # If conditions allow execution, establishes a correlation ID using the
192
- # precedence hierarchy and executes the task within that correlation context.
193
- # The correlation ID is automatically restored after task completion, ensuring
194
- # proper cleanup and thread isolation.
195
- #
196
- # The correlation ID determination follows this precedence:
197
- # 1. Explicit correlation ID (provided during middleware initialization)
198
- # - String/Symbol: Used as-is or called as method if task responds to it
199
- # - Proc/Lambda: Executed in task context for dynamic generation
200
- # 2. Current thread correlation (CMDx::Correlator.id)
201
- # 3. Task's chain ID (task.chain.id)
202
- # 4. Generated UUID (CMDx::Correlator.generate)
203
- #
204
- # @param task [CMDx::Task] the task instance to execute
205
- # @param callable [#call] the callable that executes the task
206
- # @return [CMDx::Result] the task execution result
207
- #
208
- # @example Basic middleware execution
209
- # middleware = CMDx::Middlewares::Correlate.new
210
- # task = ProcessOrderTask.new(order_id: 123)
211
- # callable = -> { task.call }
212
- #
213
- # result = middleware.call(task, callable)
214
- # # Task executed within correlation context
215
- #
216
- # @example Correlation ID precedence in action
217
- # # Scenario 1: Explicit string correlation ID takes precedence
218
- # middleware = CMDx::Middlewares::Correlate.new(id: "explicit-123")
219
- # middleware.call(task, callable) # Uses "explicit-123"
220
- #
221
- # # Scenario 2: Dynamic correlation ID using proc
222
- # middleware = CMDx::Middlewares::Correlate.new(id: -> { "dynamic-#{order_id}" })
223
- # middleware.call(task, callable) # Uses result of proc execution
41
+ # Executes the middleware, wrapping task execution with correlation context.
42
+ # Evaluates conditions, determines correlation ID, and executes the task within
43
+ # the correlation context for tracing purposes.
224
44
  #
225
- # # Scenario 3: Method-based correlation ID
226
- # middleware = CMDx::Middlewares::Correlate.new(id: :correlation_method)
227
- # middleware.call(task, callable) # Uses task.correlation_method if it exists
45
+ # @param task [CMDx::Task] the task being executed
46
+ # @param callable [Proc] the callable that executes the task
228
47
  #
229
- # # Scenario 4: Thread correlation when no explicit ID
230
- # CMDx::Correlator.id = "thread-correlation"
231
- # middleware = CMDx::Middlewares::Correlate.new
232
- # middleware.call(task, callable) # Uses "thread-correlation"
48
+ # @return [Object] the result of the task execution
233
49
  #
234
- # # Scenario 5: Chain ID when no explicit or thread correlation
235
- # CMDx::Correlator.clear
236
- # middleware.call(task, callable) # Uses task.chain.id
50
+ # @example Task using correlation middleware
51
+ # class ProcessOrderTask < CMDx::Task
52
+ # use :middleware, CMDx::Middlewares::Correlate, id: "trace-123"
237
53
  #
238
- # # Scenario 6: Generated UUID when no other correlation exists
239
- # CMDx::Correlator.clear
240
- # # Assuming task.chain.id is nil
241
- # middleware.call(task, callable) # Uses generated UUID
54
+ # def call
55
+ # # Task execution is automatically wrapped with correlation
56
+ # end
57
+ # end
242
58
  #
243
- # @example Conditional execution
244
- # # Middleware only executes in production
245
- # middleware = CMDx::Middlewares::Correlate.new(if: -> { Rails.env.production? })
246
- # result = middleware.call(task, callable)
247
- # # Correlation applied only in production environment
59
+ # @example Global configuration with conditional tracing
60
+ # CMDx.configure do |config|
61
+ # config.middlewares.register CMDx::Middlewares::Correlate, if: :should_trace?
62
+ # end
248
63
  def call(task, callable)
249
64
  # Check if correlation should be applied based on conditions
250
- return callable.call(task) unless task.__cmdx_eval(conditional)
65
+ return callable.call(task) unless task.cmdx_eval(conditional)
251
66
 
252
67
  # Get correlation ID using yield for dynamic generation
253
- correlation_id = task.__cmdx_yield(id) ||
68
+ correlation_id = task.cmdx_yield(id) ||
254
69
  CMDx::Correlator.id ||
255
70
  task.chain.id ||
256
71
  CMDx::Correlator.generate
@@ -2,220 +2,81 @@
2
2
 
3
3
  module CMDx
4
4
 
5
- ##
6
- # Timeout middleware that enforces execution time limits on tasks.
7
- #
8
- # This middleware wraps task execution with timeout protection, automatically
9
- # failing tasks that exceed their configured timeout duration. The timeout
10
- # value can be static, dynamic, or method-based. If no timeout is specified,
11
- # it defaults to 3 seconds. Optionally supports conditional timeout application
12
- # based on task or context state.
13
- #
14
- # ## Timeout Value Types
15
- #
16
- # The middleware supports multiple ways to specify timeout values:
17
- # - **Static values** (Integer/Float): Fixed timeout duration
18
- # - **Method symbols**: Calls the specified method on the task for dynamic calculation
19
- # - **Procs/Lambdas**: Executed in task context for runtime timeout determination
20
- #
21
- # ## Conditional Execution
22
- #
23
- # The middleware supports conditional timeout application using `:if` and `:unless` options:
24
- # - `:if` - Only applies timeout when the condition evaluates to true
25
- # - `:unless` - Only applies timeout when the condition evaluates to false
26
- # - Conditions can be Procs, method symbols, or boolean values
27
- #
28
- # @example Static timeout configuration
29
- # class ProcessOrderTask < CMDx::Task
30
- # use CMDx::Middlewares::Timeout, seconds: 30 # 30 seconds
31
- #
32
- # def call
33
- # # Task logic that might take too long
34
- # end
35
- # end
36
- #
37
- # @example Dynamic timeout using proc
38
- # class ProcessOrderTask < CMDx::Task
39
- # use CMDx::Middlewares::Timeout, seconds: -> { complex_order? ? 60 : 30 }
40
- #
41
- # def call
42
- # # Task logic with dynamic timeout based on order complexity
43
- # end
44
- #
45
- # private
46
- #
47
- # def complex_order?
48
- # context.order_items.count > 10
49
- # end
50
- # end
51
- #
52
- # @example Method-based timeout
53
- # class ProcessOrderTask < CMDx::Task
54
- # use CMDx::Middlewares::Timeout, seconds: :calculate_timeout
55
- #
56
- # def call
57
- # # Task logic with method-calculated timeout
58
- # end
59
- #
60
- # private
61
- #
62
- # def calculate_timeout
63
- # base_timeout = 30
64
- # base_timeout += (context.order_items.count * 2)
65
- # base_timeout
66
- # end
67
- # end
68
- #
69
- # @example Using default timeout (3 seconds)
70
- # class QuickTask < CMDx::Task
71
- # use CMDx::Middlewares::Timeout # 3 seconds default
72
- #
73
- # def call
74
- # # Task logic with default timeout
75
- # end
76
- # end
77
- #
78
- # @example Conditional timeout based on task context
79
- # class ProcessOrderTask < CMDx::Task
80
- # use CMDx::Middlewares::Timeout,
81
- # seconds: 30,
82
- # if: proc { context.enable_timeout? }
83
- #
84
- # def call
85
- # # Task logic with conditional timeout
86
- # end
87
- # end
88
- #
89
- # @example Conditional timeout with method reference
90
- # class ProcessOrderTask < CMDx::Task
91
- # use CMDx::Middlewares::Timeout,
92
- # seconds: 60,
93
- # unless: :skip_timeout?
94
- #
95
- # def call
96
- # # Task logic
97
- # end
98
- #
99
- # private
100
- #
101
- # def skip_timeout?
102
- # Rails.env.development?
103
- # end
104
- # end
105
- #
106
- # @example Global timeout middleware
107
- # class ApplicationTask < CMDx::Task
108
- # use CMDx::Middlewares::Timeout, seconds: 60 # Default 60 seconds
109
- # end
110
- #
111
- # @see CMDx::Middleware Base middleware class
112
- # @see CMDx::Task Task settings configuration
113
- # @see CMDx::Workflow Workflow execution context
114
-
115
- ##
116
- # Custom timeout error class that inherits from Interrupt.
117
- #
118
- # This error is raised when task execution exceeds the configured timeout
119
- # duration. It provides a clean way to distinguish timeout errors from
120
- # other types of interruptions or exceptions.
121
- #
122
- # @example Catching timeout errors
123
- # begin
124
- # task.call
125
- # rescue CMDx::TimeoutError => e
126
- # puts "Task timed out: #{e.message}"
127
- # end
128
- #
129
- # @see CMDx::Middlewares::Timeout The middleware that raises this error
5
+ # Custom exception raised when task execution exceeds the configured timeout limit.
6
+ # Inherits from Interrupt to provide consistent error handling for timeout scenarios
7
+ # and allow proper interruption of long-running tasks.
130
8
  TimeoutError = Class.new(Interrupt)
131
9
 
132
10
  module Middlewares
11
+ # Middleware that provides execution timeout protection for tasks.
12
+ # Automatically interrupts task execution if it exceeds the specified time limit,
13
+ # preventing runaway processes and ensuring system responsiveness.
133
14
  class Timeout < CMDx::Middleware
134
15
 
135
16
  # @return [Integer, Float, Symbol, Proc] The timeout value in seconds
17
+ attr_reader :seconds
18
+
136
19
  # @return [Hash] The conditional options for timeout application
137
- attr_reader :seconds, :conditional
20
+ attr_reader :conditional
138
21
 
139
- ##
140
- # Initializes the timeout middleware.
22
+ # Initializes the timeout middleware with optional configuration.
141
23
  #
142
- # @param options [Hash] Configuration options for the timeout middleware
143
- # @option options [Integer, Float, Symbol, Proc] :seconds Timeout value in seconds.
144
- # - Integer/Float: Used as-is for static timeout
145
- # - Symbol: Called as method on task if it exists, otherwise used as numeric value
146
- # - Proc/Lambda: Executed in task context for dynamic timeout calculation
147
- # Defaults to 3 seconds if not provided.
148
- # @option options [Symbol, Proc] :if Condition that must be truthy for timeout to be applied
149
- # @option options [Symbol, Proc] :unless Condition that must be falsy for timeout to be applied
24
+ # @param options [Hash] configuration options for the middleware
25
+ # @option options [Integer, Float, Symbol, Proc] :seconds timeout duration in seconds (default: 3)
26
+ # @option options [Symbol, Proc] :if condition that must be truthy to apply timeout
27
+ # @option options [Symbol, Proc] :unless condition that must be falsy to apply timeout
150
28
  #
151
- # @example Static timeout configuration
152
- # CMDx::Middlewares::Timeout.new(seconds: 30)
29
+ # @return [Timeout] new instance of the middleware
153
30
  #
154
- # @example Dynamic timeout with proc
155
- # CMDx::Middlewares::Timeout.new(seconds: -> { heavy_operation? ? 120 : 30 })
31
+ # @example Register with a middleware instance
32
+ # use :middleware, CMDx::Middlewares::Timeout.new(seconds: 30)
156
33
  #
157
- # @example Method-based timeout
158
- # CMDx::Middlewares::Timeout.new(seconds: :calculate_timeout_limit)
34
+ # @example Register with fixed timeout
35
+ # use :middleware, CMDx::Middlewares::Timeout, seconds: 30
159
36
  #
160
- # @example Using default timeout (3 seconds)
161
- # CMDx::Middlewares::Timeout.new
37
+ # @example Register with dynamic timeout
38
+ # use :middleware, CMDx::Middlewares::Timeout, seconds: -> { Rails.env.test? ? 1 : 10 }
162
39
  #
163
- # @example Conditional timeout
164
- # CMDx::Middlewares::Timeout.new(seconds: 30, if: :production_mode?)
165
- # CMDx::Middlewares::Timeout.new(seconds: 60, unless: proc { Rails.env.test? })
40
+ # @example Register with conditions
41
+ # use :middleware, CMDx::Middlewares::Timeout, seconds: 5, if: :long_running?, unless: :skip_timeout?
166
42
  def initialize(options = {})
167
43
  @seconds = options[:seconds] || 3
168
44
  @conditional = options.slice(:if, :unless)
169
45
  end
170
46
 
171
- ##
172
- # Executes the task with conditional timeout protection.
173
- #
174
- # Evaluates the conditional options to determine if timeout should be applied.
175
- # If conditions are met, resolves the timeout value using and wraps the task
176
- # execution with a timeout mechanism that will interrupt execution if it exceeds
177
- # the configured time limit. If conditions are not met, executes the task
178
- # without timeout protection.
47
+ # Executes the middleware, wrapping task execution with timeout protection.
48
+ # Evaluates conditions, determines timeout duration, and executes the task within
49
+ # the timeout boundary to prevent runaway execution.
179
50
  #
180
- # The timeout value determination follows this precedence:
181
- # 1. Explicit timeout value (provided during middleware initialization)
182
- # - Integer/Float: Used as-is for static timeout
183
- # - Symbol: Called as method on task if it exists, otherwise used as numeric value
184
- # - Proc/Lambda: Executed in task context for dynamic timeout calculation
185
- # 2. Default value of 3 seconds if no timeout is specified
51
+ # @param task [CMDx::Task] the task being executed
52
+ # @param callable [Proc] the callable that executes the task
186
53
  #
187
- # @param task [CMDx::Task] The task instance to execute
188
- # @param callable [#call] The next middleware or task execution callable
189
- # @return [CMDx::Result] The task execution result
190
- # @raise [TimeoutError] If execution exceeds the configured timeout and conditions are met
54
+ # @return [Object] the result of the task execution
191
55
  #
192
- # @example Static timeout - successful execution
193
- # # Task completes in 5 seconds, timeout is 30 seconds, condition is true
194
- # result = task.call # => success
56
+ # @raise [TimeoutError] when task execution exceeds the timeout limit
195
57
  #
196
- # @example Static timeout - timeout exceeded
197
- # # Task would take 60 seconds, timeout is 30 seconds, condition is true
198
- # result = task.call # => failed with timeout error
58
+ # @example Task using timeout middleware
59
+ # class ProcessFileTask < CMDx::Task
60
+ # use :middleware, CMDx::Middlewares::Timeout, seconds: 10
199
61
  #
200
- # @example Dynamic timeout with proc
201
- # # Task uses proc to calculate 120 seconds for complex operation
202
- # # Task completes in 90 seconds
203
- # result = task.call # => success
62
+ # def call
63
+ # # Task execution is automatically wrapped with timeout protection
64
+ # end
65
+ # end
204
66
  #
205
- # @example Method-based timeout
206
- # # Task calls :timeout_limit method which returns 45 seconds
207
- # # Task completes in 30 seconds
208
- # result = task.call # => success
209
- #
210
- # @example Condition not met
211
- # # Task takes 60 seconds, timeout is 30 seconds, but condition is false
212
- # result = task.call # => success (no timeout applied)
67
+ # @example Global configuration with conditional timeout
68
+ # CMDx.configure do |config|
69
+ # config.middlewares.register CMDx::Middlewares::Timeout, seconds: 30, if: :large_dataset?
70
+ # end
213
71
  def call(task, callable)
214
72
  # Check if timeout should be applied based on conditions
215
- return callable.call(task) unless task.__cmdx_eval(conditional)
73
+ return callable.call(task) unless task.cmdx_eval(conditional)
216
74
 
217
75
  # Get seconds using yield for dynamic generation
218
- limit = task.__cmdx_yield(seconds) || 3
76
+ limit = task.cmdx_yield(seconds) || 3
77
+
78
+ # Ensure limit is numeric, fallback to default if not
79
+ limit = 3 unless limit.is_a?(Numeric)
219
80
 
220
81
  # Apply timeout protection
221
82
  ::Timeout.timeout(limit, TimeoutError, "execution exceeded #{limit} seconds") do