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:



444
445
446
447
448
449
450
451
# File 'lib/cmdx/task.rb', line 444

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.



436
437
438
# File 'lib/cmdx/task.rb', line 436

def context
  @context
end

#errorsObject (readonly)

Returns the value of attribute errors.



436
437
438
# File 'lib/cmdx/task.rb', line 436

def errors
  @errors
end

#metadataObject (readonly)

Returns the value of attribute metadata.



436
437
438
# File 'lib/cmdx/task.rb', line 436

def 
  @metadata
end

#tidObject (readonly)

Returns the value of attribute tid.



436
437
438
# File 'lib/cmdx/task.rb', line 436

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



389
390
391
# File 'lib/cmdx/task.rb', line 389

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



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

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



398
399
400
# File 'lib/cmdx/task.rb', line 398

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)
  • :allow_nil (Boolean) — default: see {Input#allow_nil}
  • :coerce (Object) — default: see {Coercions#extract}
  • :validate (Object) — default: see {Validators#extract}

Yields:

Returns:



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

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



343
344
345
# File 'lib/cmdx/task.rb', line 343

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)
  • :allow_nil (Boolean) — default: see {Input#allow_nil}
  • :coerce (Object) — default: see {Coercions#extract}
  • :validate (Object) — default: see {Validators#extract}

Yields:



315
316
317
# File 'lib/cmdx/task.rb', line 315

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:



356
357
358
359
360
361
362
363
364
365
366
367
# File 'lib/cmdx/task.rb', line 356

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



371
372
373
# File 'lib/cmdx/task.rb', line 371

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)
  • :allow_nil (Boolean) — default: see {Input#allow_nil}
  • :coerce (Object) — default: see {Coercions#extract}
  • :validate (Object) — default: see {Validators#extract}

Yields:



338
339
340
# File 'lib/cmdx/task.rb', line 338

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"



376
377
378
# File 'lib/cmdx/task.rb', line 376

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



461
462
463
464
# File 'lib/cmdx/task.rb', line 461

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:



530
531
532
533
534
535
536
537
538
539
540
# File 'lib/cmdx/task.rb', line 530

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



468
469
470
# File 'lib/cmdx/task.rb', line 468

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:



510
511
512
513
514
515
516
517
518
519
520
# File 'lib/cmdx/task.rb', line 510

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:



491
492
493
494
495
496
497
498
499
500
501
# File 'lib/cmdx/task.rb', line 491

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:



552
553
554
555
556
557
558
559
560
561
562
563
564
# File 'lib/cmdx/task.rb', line 552

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:



477
478
479
480
481
482
# File 'lib/cmdx/task.rb', line 477

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