Class: CMDx::Task

Inherits:
Object
  • Object
show all
Defined in:
lib/cmdx/task.rb

Overview

Base class for all units of work. Subclasses override ‘#work` and declare their contract via `required`, `optional`, `output`, `callbacks`, `retry_on`, `deprecation`, and `settings`. Invoked via Task.execute (safe) or Task.execute! (strict, raises on failure).

Inheritance: every registry accessor (middlewares, callbacks, coercions, validators, executors, mergers, telemetry, inputs, outputs) lazily clones from the superclass’s registry (or the global configuration at the top of the hierarchy), so subclasses extend rather than replace.

See Also:

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(context = EMPTY_HASH) ⇒ Task

Note:

The built Context inherits ‘strict` mode from Settings#strict_context (falling back to Configuration#strict_context), so dynamic reads for unknown keys raise `NoMethodError` instead of returning `nil`.

Returns a new instance of Task.

Parameters:



441
442
443
444
445
446
447
448
# File 'lib/cmdx/task.rb', line 441

def initialize(context = EMPTY_HASH)
  @metadata = {}
  @tid      = SecureRandom.uuid_v7
  @errors   = Errors.new
  @context  = Context.build(context).tap do |c|
    c.strict = self.class.settings.strict_context
  end
end

Instance Attribute Details

#contextObject (readonly) Also known as: ctx

Returns the value of attribute context.



433
434
435
# File 'lib/cmdx/task.rb', line 433

def context
  @context
end

#errorsObject (readonly)

Returns the value of attribute errors.



433
434
435
# File 'lib/cmdx/task.rb', line 433

def errors
  @errors
end

#metadataObject (readonly)

Returns the value of attribute metadata.



433
434
435
# File 'lib/cmdx/task.rb', line 433

def 
  @metadata
end

#tidObject (readonly)

Returns the value of attribute tid.



433
434
435
# File 'lib/cmdx/task.rb', line 433

def tid
  @tid
end

Class Method Details

.call {|result| ... } ⇒ Result, Object

Executes the task. Never raises on failure; inspect the returned Result instead.

Parameters:

Yield Parameters:

Returns:

  • (Result, Object)

    the yielded block’s value when a block is given



386
387
388
# File 'lib/cmdx/task.rb', line 386

def execute(context = EMPTY_HASH, &)
  new(context).execute(strict: false, &)
end

.callbacksCallbacks

Returns cloned from superclass/configuration on first call.

Returns:

  • (Callbacks)

    cloned from superclass/configuration on first call



84
85
86
87
88
89
90
91
# File 'lib/cmdx/task.rb', line 84

def callbacks
  @callbacks ||=
    if superclass.respond_to?(:callbacks)
      superclass.callbacks.dup
    else
      CMDx.configuration.callbacks.dup
    end
end

.coercionsCoercions

Returns cloned from superclass/configuration on first call.

Returns:

  • (Coercions)

    cloned from superclass/configuration on first call



110
111
112
113
114
115
116
117
# File 'lib/cmdx/task.rb', line 110

def coercions
  @coercions ||=
    if superclass.respond_to?(:coercions)
      superclass.coercions.dup
    else
      CMDx.configuration.coercions.dup
    end
end

.deprecation(value = nil, **options, &block) { ... } ⇒ Deprecation?

Reads, sets, or inherits the task class’s Deprecation. With a ‘value` or block, replaces any current deprecation. Otherwise returns the locally defined one, or the superclass’s.

Parameters:

  • value (:log, :warn, :error, Symbol, Proc, #call, nil) (defaults to: nil)
  • block (#call, nil)

    optional block used as the deprecation callable

  • options (Hash{Symbol => Object})

    ‘:if`/`:unless` conditions (see Deprecation#initialize)

Options Hash (**options):

  • :if (Symbol, Proc, #call) — default: see {Deprecation#initialize}
  • :unless (Symbol, Proc, #call) — default: see {Deprecation#initialize}

Yields:

  • optional block used as the deprecation callable

Returns:



252
253
254
255
256
257
258
259
260
# File 'lib/cmdx/task.rb', line 252

def deprecation(value = nil, **options, &block)
  if value || block
    @deprecation = Deprecation.new(value || block, options)
  elsif defined?(@deprecation)
    @deprecation
  elsif superclass.respond_to?(:deprecation)
    superclass.deprecation
  end
end

.deprecatorsDeprecators

Returns cloned from superclass/configuration on first call.

Returns:

  • (Deprecators)

    cloned from superclass/configuration on first call



160
161
162
163
164
165
166
167
# File 'lib/cmdx/task.rb', line 160

def deprecators
  @deprecators ||=
    if superclass.respond_to?(:deprecators)
      superclass.deprecators.dup
    else
      CMDx.configuration.deprecators.dup
    end
end

.deregister(type) ⇒ Object

Dispatches to the appropriate registry’s ‘deregister` method.

Parameters:

  • type (:middleware, :callback, :coercion, :validator, :executor, :merger, :retrier, :deprecator, :input, :output)

Returns:

  • (Object)

    the registry’s self

Raises:

  • (ArgumentError)

    when ‘type` is unknown



210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
# File 'lib/cmdx/task.rb', line 210

def deregister(type, ...)
  case type
  when :middleware
    middlewares.deregister(...)
  when :callback
    callbacks.deregister(...)
  when :coercion
    coercions.deregister(...)
  when :validator
    validators.deregister(...)
  when :executor
    executors.deregister(...)
  when :merger
    mergers.deregister(...)
  when :retrier
    retriers.deregister(...)
  when :deprecator
    deprecators.deregister(...)
  when :input
    inputs.deregister(self, ...)
  when :output
    outputs.deregister(...)
  else
    raise ArgumentError, <<~MSG.chomp
      unknown registry type #{type.inspect};
      expected one of [:middleware, :callback, :coercion, :validator, :executor, :merger, :retrier, :deprecator, :input, :output].
      See https://drexed.github.io/cmdx/configuration/#registrations-register-deregister
    MSG
  end
end

.execute(context = EMPTY_HASH) {|result| ... } ⇒ Result, Object

Executes the task. Never raises on failure; inspect the returned Result instead.

Parameters:

Yield Parameters:

Returns:

  • (Result, Object)

    the yielded block’s value when a block is given



383
384
385
# File 'lib/cmdx/task.rb', line 383

def execute(context = EMPTY_HASH, &)
  new(context).execute(strict: false, &)
end

.execute!(context = EMPTY_HASH) {|result| ... } ⇒ Result, Object Also known as: call!

Strict execution. Raises Fault (or the underlying exception) on failure; otherwise identical to execute.

Parameters:

Yield Parameters:

Returns:

Raises:

  • (Fault, StandardError)

    on task failure



395
396
397
# File 'lib/cmdx/task.rb', line 395

def execute!(context = EMPTY_HASH, &)
  new(context).execute(strict: true, &)
end

.executorsExecutors

Returns cloned from superclass/configuration on first call.

Returns:

  • (Executors)

    cloned from superclass/configuration on first call



130
131
132
133
134
135
136
137
# File 'lib/cmdx/task.rb', line 130

def executors
  @executors ||=
    if superclass.respond_to?(:executors)
      superclass.executors.dup
    else
      CMDx.configuration.executors.dup
    end
end

.inputs(*names, **options) { ... } ⇒ Inputs Also known as: input

Reads, or declares more, inputs. With no names, returns the registry; with names, registers them and defines accessors.

Parameters:

  • names (Array<Symbol>)
  • options (Hash{Symbol => Object})

Options Hash (**options):

  • :description (String) — default: also accepts `:desc`
  • :as (Symbol)

    overrides the accessor name

  • :prefix (Boolean, String)

    prefix for the accessor name

  • :suffix (Boolean, String)

    suffix for the accessor name

  • :source (Symbol, Proc, #call) — default: `:context`

    where to fetch from

  • :default (Object, Symbol, Proc, #call)
  • :transform (Symbol, Proc, #call)

    mutator applied after coercion

  • :if (Symbol, Proc, #call)
  • :unless (Symbol, Proc, #call)
  • :required (Boolean)
  • :coerce (Object) — default: see {Coercions#extract}
  • :validate (Object) — default: see {Validators#extract}

Yields:

Returns:



281
282
283
284
285
286
287
288
289
290
291
292
# File 'lib/cmdx/task.rb', line 281

def inputs(*names, **options, &)
  @inputs ||=
    if superclass.respond_to?(:inputs)
      superclass.inputs.dup
    else
      Inputs.new
    end

  return @inputs if names.empty?

  @inputs.register(self, *names, **options, &)
end

.inputs_schemaHash{Symbol => Hash}

Returns serialized input definitions.

Returns:

  • (Hash{Symbol => Hash})

    serialized input definitions



340
341
342
# File 'lib/cmdx/task.rb', line 340

def inputs_schema
  inputs.registry.transform_values(&:to_h)
end

.mergersMergers

Returns cloned from superclass/configuration on first call.

Returns:

  • (Mergers)

    cloned from superclass/configuration on first call



140
141
142
143
144
145
146
147
# File 'lib/cmdx/task.rb', line 140

def mergers
  @mergers ||=
    if superclass.respond_to?(:mergers)
      superclass.mergers.dup
    else
      CMDx.configuration.mergers.dup
    end
end

.middlewaresMiddlewares

Returns cloned from superclass/configuration on first call.

Returns:

  • (Middlewares)

    cloned from superclass/configuration on first call



74
75
76
77
78
79
80
81
# File 'lib/cmdx/task.rb', line 74

def middlewares
  @middlewares ||=
    if superclass.respond_to?(:middlewares)
      superclass.middlewares.dup
    else
      CMDx.configuration.middlewares.dup
    end
end

.optional(*names, **options) { ... } ⇒ Object

Declares optional inputs (shorthand for ‘inputs …, required: false`). An explicit `required:` in `options` is ignored — use inputs when you need to set the flag dynamically.

Parameters:

  • names (Array<Symbol>)
  • options (Hash{Symbol => Object})

Options Hash (**options):

  • :description (String) — default: also accepts `:desc`
  • :as (Symbol)

    overrides the accessor name

  • :prefix (Boolean, String)

    prefix for the accessor name

  • :suffix (Boolean, String)

    suffix for the accessor name

  • :source (Symbol, Proc, #call) — default: `:context`

    where to fetch from

  • :default (Object, Symbol, Proc, #call)
  • :transform (Symbol, Proc, #call)

    mutator applied after coercion

  • :if (Symbol, Proc, #call)
  • :unless (Symbol, Proc, #call)
  • :coerce (Object) — default: see {Coercions#extract}
  • :validate (Object) — default: see {Validators#extract}

Yields:



313
314
315
# File 'lib/cmdx/task.rb', line 313

def optional(*names, **options, &)
  register(:input, *names, **options, required: false, &)
end

.outputs(*keys, **options) ⇒ Outputs Also known as: output

Reads, or declares more, outputs. With no keys, returns the registry.

Parameters:

Options Hash (**options):

  • :description (String) — default: also accepts `:desc`
  • :if (Symbol, Proc, #call)
  • :unless (Symbol, Proc, #call)
  • :default (Object, Symbol, Proc, #call)

Returns:



353
354
355
356
357
358
359
360
361
362
363
364
# File 'lib/cmdx/task.rb', line 353

def outputs(*keys, **options)
  @outputs ||=
    if superclass.respond_to?(:outputs)
      superclass.outputs.dup
    else
      Outputs.new
    end

  return @outputs if keys.empty?

  @outputs.register(*keys, **options)
end

.outputs_schemaHash{Symbol => Hash}

Returns serialized output definitions.

Returns:

  • (Hash{Symbol => Hash})

    serialized output definitions



368
369
370
# File 'lib/cmdx/task.rb', line 368

def outputs_schema
  outputs.registry.transform_values(&:to_h)
end

.register(type) ⇒ Object

Dispatches to the appropriate registry’s ‘register` method.

Parameters:

  • type (:middleware, :callback, :coercion, :validator, :executor, :merger, :retrier, :deprecator, :input, :output)

Returns:

  • (Object)

    the registry’s self

Raises:

  • (ArgumentError)

    when ‘type` is unknown



174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
# File 'lib/cmdx/task.rb', line 174

def register(type, ...)
  case type
  when :middleware
    middlewares.register(...)
  when :callback
    callbacks.register(...)
  when :coercion
    coercions.register(...)
  when :validator
    validators.register(...)
  when :executor
    executors.register(...)
  when :merger
    mergers.register(...)
  when :retrier
    retriers.register(...)
  when :deprecator
    deprecators.register(...)
  when :input
    inputs.register(self, ...)
  when :output
    outputs.register(...)
  else
    raise ArgumentError, <<~MSG.chomp
      unknown registry type #{type.inspect};
      expected one of [:middleware, :callback, :coercion, :validator, :executor, :merger, :retrier, :deprecator, :input, :output].
      See https://drexed.github.io/cmdx/configuration/#registrations-register-deregister
    MSG
  end
end

.required(*names, **options) { ... } ⇒ Object

Declares required inputs (shorthand for ‘inputs …, required: true`). An explicit `required:` in `options` is ignored — use inputs when you need to set the flag dynamically.

Parameters:

  • names (Array<Symbol>)
  • options (Hash{Symbol => Object})

Options Hash (**options):

  • :description (String) — default: also accepts `:desc`
  • :as (Symbol)

    overrides the accessor name

  • :prefix (Boolean, String)

    prefix for the accessor name

  • :suffix (Boolean, String)

    suffix for the accessor name

  • :source (Symbol, Proc, #call) — default: `:context`

    where to fetch from

  • :default (Object, Symbol, Proc, #call)
  • :transform (Symbol, Proc, #call)

    mutator applied after coercion

  • :if (Symbol, Proc, #call)
  • :unless (Symbol, Proc, #call)
  • :coerce (Object) — default: see {Coercions#extract}
  • :validate (Object) — default: see {Validators#extract}

Yields:



335
336
337
# File 'lib/cmdx/task.rb', line 335

def required(*names, **options, &)
  register(:input, *names, **options, required: true, &)
end

.retriersRetriers

Returns cloned from superclass/configuration on first call.

Returns:

  • (Retriers)

    cloned from superclass/configuration on first call



150
151
152
153
154
155
156
157
# File 'lib/cmdx/task.rb', line 150

def retriers
  @retriers ||=
    if superclass.respond_to?(:retriers)
      superclass.retriers.dup
    else
      CMDx.configuration.retriers.dup
    end
end

.retry_on(*exceptions, **options) {|attempt, delay| ... } ⇒ Retry

Declares exceptions to retry on. Builds on the superclass’s ‘Retry`. Passing no exceptions returns the current (possibly inherited) Retry.

Parameters:

  • exceptions (Array<Class>)
  • options (Hash{Symbol => Object})

Options Hash (**options):

  • :limit (Integer) — default: see {Retry#initialize}
  • :delay (Float) — default: see {Retry#initialize}
  • :max_delay (Float) — default: see {Retry#initialize}
  • :jitter (Symbol, Proc, #call) — default: see {Retry#initialize}
  • :if (Symbol, Proc, #call)

    gate ‘(task, error, attempt)` for retries

  • :unless (Symbol, Proc, #call)

    gate ‘(task, error, attempt)` for retries

Yields:

  • (attempt, delay)

    optional custom jitter block

Returns:



33
34
35
36
37
38
39
40
41
42
43
44
# File 'lib/cmdx/task.rb', line 33

def retry_on(*exceptions, **options, &)
  @retry_on ||=
    if superclass.respond_to?(:retry_on)
      superclass.retry_on.build(exceptions, options, &)
    else
      Retry.new(exceptions, options, &)
    end

  return @retry_on if exceptions.empty?

  @retry_on = @retry_on.build(exceptions, options, &)
end

.settings(options = EMPTY_HASH) ⇒ Settings

Reads or extends this class’s Settings. Inherits from the superclass.

Parameters:

  • options (Hash{Symbol => Object}) (defaults to: EMPTY_HASH)

    merged onto the current settings

Options Hash (options):

  • :logger (Logger) — default: see {Settings#initialize}
  • :log_formatter (#call) — default: see {Settings#initialize}
  • :log_level (Integer) — default: see {Settings#initialize}
  • :backtrace_cleaner (#call) — default: see {Settings#initialize}
  • :log_exclusions (Array<Symbol>) — default: see {Settings#initialize}
  • :tags (Array<Symbol, String>) — default: see {Settings#initialize}
  • :strict_context (Boolean) — default: see {Settings#initialize}
  • :correlation_id (#call)

    callable returning a String id resolved once by Runtime when the root chain is acquired; surfaces as ‘result.xid` (see Settings#correlation_id)

Returns:



60
61
62
63
64
65
66
67
68
69
70
71
# File 'lib/cmdx/task.rb', line 60

def settings(options = EMPTY_HASH)
  @settings ||=
    if superclass.respond_to?(:settings)
      superclass.settings.build(options)
    else
      Settings.new(options)
    end

  return @settings if options.empty?

  @settings = @settings.build(options)
end

.telemetryTelemetry

Returns cloned from superclass/configuration on first call.

Returns:

  • (Telemetry)

    cloned from superclass/configuration on first call



100
101
102
103
104
105
106
107
# File 'lib/cmdx/task.rb', line 100

def telemetry
  @telemetry ||=
    if superclass.respond_to?(:telemetry)
      superclass.telemetry.dup
    else
      CMDx.configuration.telemetry.dup
    end
end

.typeString

Returns ‘“Workflow”` when the class includes Workflow, else `“Task”`.

Returns:

  • (String)

    ‘“Workflow”` when the class includes Workflow, else `“Task”`



373
374
375
# File 'lib/cmdx/task.rb', line 373

def type
  @type ||= include?(Workflow) ? "Workflow" : "Task"
end

.validatorsValidators

Returns cloned from superclass/configuration on first call.

Returns:

  • (Validators)

    cloned from superclass/configuration on first call



120
121
122
123
124
125
126
127
# File 'lib/cmdx/task.rb', line 120

def validators
  @validators ||=
    if superclass.respond_to?(:validators)
      superclass.validators.dup
    else
      CMDx.configuration.validators.dup
    end
end

Instance Method Details

#execute(strict: false) {|result| ... } ⇒ Result, Object Also known as: call

Executes this task instance through Runtime.

Parameters:

  • strict (Boolean) (defaults to: false)

    when ‘true`, re-raises Fault/exceptions on failure; when `false`, swallows them and returns the Result

Yield Parameters:

Returns:

  • (Result, Object)

    the yielded block’s value when a block is given, otherwise the Result

Raises:

  • (Fault, StandardError)

    only when ‘strict: true` and the task fails



458
459
460
461
# File 'lib/cmdx/task.rb', line 458

def execute(strict: false)
  result = Runtime.execute(self, strict:)
  block_given? ? yield(result) : result
end

#fail!(reason = nil, **sigdata) ⇒ void

This method returns an undefined value.

Halts ‘#work` and marks the Result as `failed`. Captures the current caller frames for the Fault backtrace and merges any `sigdata` onto #metadata before the signal is thrown.

Parameters:

  • reason (String, nil) (defaults to: nil)

    human-readable explanation surfaced on the Result/Fault

  • sigdata (Hash{Symbol => Object})

    extra metadata merged into #metadata

Raises:



527
528
529
530
531
532
533
534
535
536
537
# File 'lib/cmdx/task.rb', line 527

def fail!(reason = nil, **sigdata)
  if frozen?
    raise FrozenTaskError, <<~MSG.chomp
      cannot call :fail! on #{self.class}; the task has already been executed and frozen.
      See https://drexed.github.io/cmdx/outcomes/result/#lifecycle-predicates
    MSG
  end

  .merge!(sigdata) unless sigdata.empty?
  throw(Signal::TAG, Signal.failed(reason, metadata:, backtrace: caller_locations(1)))
end

#loggerLogger

Returns a logger tailored to this task’s settings.

Returns:

  • (Logger)

    a logger tailored to this task’s settings



465
466
467
# File 'lib/cmdx/task.rb', line 465

def logger
  @logger ||= LoggerProxy.logger(self)
end

#skip!(reason = nil, **sigdata) ⇒ void

This method returns an undefined value.

Halts ‘#work` and marks the Result as `skipped`. Any `sigdata` is merged onto #metadata before the signal is thrown.

Parameters:

  • reason (String, nil) (defaults to: nil)

    human-readable explanation surfaced on the Result

  • sigdata (Hash{Symbol => Object})

    extra metadata merged into #metadata

Raises:



507
508
509
510
511
512
513
514
515
516
517
# File 'lib/cmdx/task.rb', line 507

def skip!(reason = nil, **sigdata)
  if frozen?
    raise FrozenTaskError, <<~MSG.chomp
      cannot call :skip! on #{self.class}; the task has already been executed and frozen.
      See https://drexed.github.io/cmdx/outcomes/result/#lifecycle-predicates
    MSG
  end

  .merge!(sigdata) unless sigdata.empty?
  throw(Signal::TAG, Signal.skipped(reason, metadata:))
end

#success!(reason = nil, **sigdata) ⇒ void

This method returns an undefined value.

Halts ‘#work` early with a successful outcome. Any `sigdata` is merged onto #metadata before the signal is thrown.

Parameters:

  • reason (String, nil) (defaults to: nil)

    human-readable explanation surfaced on the Result

  • sigdata (Hash{Symbol => Object})

    extra metadata merged into #metadata

Raises:



488
489
490
491
492
493
494
495
496
497
498
# File 'lib/cmdx/task.rb', line 488

def success!(reason = nil, **sigdata)
  if frozen?
    raise FrozenTaskError, <<~MSG.chomp
      cannot call :success! on #{self.class}; the task has already been executed and frozen.
      See https://drexed.github.io/cmdx/outcomes/result/#lifecycle-predicates
    MSG
  end

  .merge!(sigdata) unless sigdata.empty?
  throw(Signal::TAG, Signal.success(reason, metadata:))
end

#throw!(other, **sigdata) ⇒ void?

Echoes another Result or Signal‘s failure outcome from this task. A no-op when `other` is not failed, letting callers conditionally bubble up nested failures without branching. Captures caller frames for the Fault backtrace and merges any `sigdata` onto #metadata before the signal is thrown.

Parameters:

  • other (Result, Signal)

    upstream outcome to mirror

  • sigdata (Hash{Symbol => Object})

    extra metadata merged into #metadata

Returns:

  • (void, nil)

    returns ‘nil` when `other` is not failed; otherwise throws Signal::TAG

Raises:



549
550
551
552
553
554
555
556
557
558
559
560
561
# File 'lib/cmdx/task.rb', line 549

def throw!(other, **sigdata)
  if frozen?
    raise FrozenTaskError, <<~MSG.chomp
      cannot call :throw! on #{self.class}; the task has already been executed and frozen.
      See https://drexed.github.io/cmdx/outcomes/result/#lifecycle-predicates
    MSG
  end

  return unless other.failed?

  .merge!(sigdata) unless sigdata.empty?
  throw(Signal::TAG, Signal.echoed(other, metadata:, backtrace: caller_locations(1)))
end

#workvoid

This method is abstract.

This method returns an undefined value.

The task’s core logic. Subclasses must override.

Raises:



474
475
476
477
478
479
# File 'lib/cmdx/task.rb', line 474

def work
  raise ImplementationError, <<~MSG.chomp
    #{self.class} must implement #work.
    See https://drexed.github.io/cmdx/basics/setup/#structure
  MSG
end