Class: Tasker::Task

Inherits:
ApplicationRecord show all
Defined in:
app/models/tasker/task.rb

Overview

Task represents a workflow process that contains multiple workflow steps. Each Task is identified by a name and has a context which defines the parameters for the task. Tasks track their status via state machine transitions, initiator, source system, and other metadata.

Examples:

Creating a task from a task request

task_request = Tasker::Types::TaskRequest.new(name: 'process_order', context: { order_id: 123 })
task = Tasker::Task.create_with_defaults!(task_request)

Constant Summary collapse

ALPHANUM_PLUS_HYPHEN_DASH =

Regular expression to sanitize strings for database operations

/[^0-9a-z\-_]/i

Class Method Summary collapse

Instance Method Summary collapse

Methods inherited from ApplicationRecord

configure_database_connections, database_configuration_exists?

Class Method Details

.activeActiveRecord::Relation

Scopes tasks that are currently active (not in terminal states)

Returns:

  • (ActiveRecord::Relation)

    Tasks that are not complete, error, or cancelled



182
183
184
185
186
187
188
189
190
191
192
193
194
195
# File 'app/models/tasker/task.rb', line 182

scope :active, lambda {
  # Active tasks are those with at least one workflow step whose most recent transition
  # is NOT in a terminal state. Using EXISTS subquery for clarity and performance.
  where(<<-SQL.squish)
    EXISTS (
      SELECT 1
      FROM tasker_workflow_steps ws
      INNER JOIN tasker_workflow_step_transitions wst ON wst.workflow_step_id = ws.workflow_step_id
      WHERE ws.task_id = tasker_tasks.task_id
        AND wst.most_recent = true
        AND wst.to_state NOT IN ('complete', 'error', 'skipped', 'resolved_manually')
    )
  SQL
}

.by_annotationActiveRecord::Relation

Scopes a query to find tasks with a specific annotation value

Parameters:

  • name (String)

    The annotation type name

  • key (String, Symbol)

    The key within the annotation to match

  • value (String)

    The value to match against

Returns:

  • (ActiveRecord::Relation)

    Tasks matching the annotation criteria



96
97
98
99
100
101
102
# File 'app/models/tasker/task.rb', line 96

scope :by_annotation,
lambda { |name, key, value|
  clean_key = key.to_s.gsub(ALPHANUM_PLUS_HYPHEN_DASH, '')
  joins(:task_annotations, :annotation_types)
    .where({ annotation_types: { name: name.to_s.strip } })
    .where("tasker_task_annotations.annotation->>'#{clean_key}' = :value", value: value)
}

.by_current_stateActiveRecord::Relation

Scopes a query to find tasks by their current state using state machine transitions

Parameters:

  • state (String, nil)

    The state to filter by. If nil, returns all tasks with current state information

Returns:

  • (ActiveRecord::Relation)

    Tasks with current state, optionally filtered by specific state



109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
# File 'app/models/tasker/task.rb', line 109

scope :by_current_state,
lambda { |state = nil|
  relation = joins(<<-SQL.squish)
    INNER JOIN (
      SELECT DISTINCT ON (task_id) task_id, to_state
      FROM tasker_task_transitions
      ORDER BY task_id, sort_key DESC
    ) current_transitions ON current_transitions.task_id = tasker_tasks.task_id
  SQL

  if state.present?
    relation.where(current_transitions: { to_state: state })
  else
    relation
  end
}

.completed_sinceActiveRecord::Relation

Scopes tasks completed within a specific time period

Parameters:

  • since_time (Time)

    The earliest completion time to include

Returns:

  • (ActiveRecord::Relation)

    Tasks completed since the specified time



152
153
154
155
156
157
# File 'app/models/tasker/task.rb', line 152

scope :completed_since, lambda { |since_time|
  joins(workflow_steps: :workflow_step_transitions)
    .where('tasker_workflow_step_transitions.to_state = ? AND tasker_workflow_step_transitions.most_recent = ?', 'complete', true)
    .where('tasker_workflow_step_transitions.created_at > ?', since_time)
    .distinct
}

.create_with_defaults!(task_request) ⇒ Tasker::Task

Creates a task with default values from a task request and saves it to the database

Parameters:

Returns:

Raises:

  • (ActiveRecord::RecordInvalid)

    If the task is invalid



239
240
241
242
243
# File 'app/models/tasker/task.rb', line 239

def self.create_with_defaults!(task_request)
  task = from_task_request(task_request)
  task.save!
  task
end

.created_sinceActiveRecord::Relation

Scopes tasks created within a specific time period

Parameters:

  • since_time (Time)

    The earliest creation time to include

Returns:

  • (ActiveRecord::Relation)

    Tasks created since the specified time



143
144
145
# File 'app/models/tasker/task.rb', line 143

scope :created_since, lambda { |since_time|
  where('tasker_tasks.created_at > ?', since_time)
}

.failed_sinceActiveRecord::Relation

Scopes tasks that have failed within a specific time period This scope identifies tasks that are actually in a failed state (task status = 'error'), not just tasks that have some failed steps but may still be progressing.

Parameters:

  • since_time (Time)

    The earliest failure time to include

Returns:

  • (ActiveRecord::Relation)

    Tasks that have transitioned to error state since the specified time



166
167
168
169
170
171
172
173
174
175
176
# File 'app/models/tasker/task.rb', line 166

scope :failed_since, lambda { |since_time|
  joins(<<-SQL.squish)
    INNER JOIN (
      SELECT DISTINCT ON (task_id) task_id, to_state, created_at
      FROM tasker_task_transitions
      ORDER BY task_id, sort_key DESC
    ) current_transitions ON current_transitions.task_id = tasker_tasks.task_id
  SQL
    .where(current_transitions: { to_state: Tasker::Constants::TaskStatuses::ERROR })
    .where('current_transitions.created_at > ?', since_time)
}

.from_task_request(task_request) ⇒ Tasker::Task

Creates a new unsaved task instance from a task request

Parameters:

Returns:



249
250
251
252
253
254
255
256
257
258
259
260
# File 'app/models/tasker/task.rb', line 249

def self.from_task_request(task_request)
  named_task = Tasker::NamedTask.find_or_create_by_full_name!(name: task_request.name,
                                                              namespace_name: task_request.namespace, version: task_request.version)
  # Extract values from task_request, removing nils
  request_values = get_request_options(task_request)
  # Merge defaults with request values
  options = get_default_task_request_options(named_task).merge(request_values)

  task = new(options)
  task.named_task = named_task
  task
end

.get_default_task_request_options(named_task) ⇒ Hash

Provides default options for a task

Parameters:

Returns:

  • (Hash)

    Hash of default task options



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

def self.get_default_task_request_options(named_task)
  {
    initiator: Tasker::Constants::UNKNOWN,
    source_system: Constants::UNKNOWN,
    reason: Tasker::Constants::UNKNOWN,
    complete: false,
    tags: [],
    bypass_steps: [],
    requested_at: Time.zone.now,
    named_task_id: named_task.named_task_id
  }
end

.get_request_options(task_request) ⇒ Hash

Extracts and compacts options from a task request

Parameters:

Returns:

  • (Hash)

    Hash of non-nil task options from the request



266
267
268
269
270
271
272
273
274
275
276
# File 'app/models/tasker/task.rb', line 266

def self.get_request_options(task_request)
  {
    initiator: task_request.initiator,
    source_system: task_request.source_system,
    reason: task_request.reason,
    tags: task_request.tags,
    bypass_steps: task_request.bypass_steps,
    requested_at: task_request.requested_at,
    context: task_request.context
  }.compact
end

.in_namespaceActiveRecord::Relation

Scopes tasks by namespace name through the named_task association

Parameters:

  • namespace_name (String)

    The namespace name to filter by

Returns:

  • (ActiveRecord::Relation)

    Tasks in the specified namespace



202
203
204
205
# File 'app/models/tasker/task.rb', line 202

scope :in_namespace, lambda { |namespace_name|
  joins(named_task: :task_namespace)
    .where(tasker_task_namespaces: { name: namespace_name })
}

.unique_task_types_countInteger

Class method for counting unique task types

Returns:

  • (Integer)

    Count of unique task names



230
231
232
# File 'app/models/tasker/task.rb', line 230

def self.unique_task_types_count
  joins(:named_task).distinct.count('tasker_named_tasks.name')
end

.with_task_nameActiveRecord::Relation

Scopes tasks by task name through the named_task association

Parameters:

  • task_name (String)

    The task name to filter by

Returns:

  • (ActiveRecord::Relation)

    Tasks with the specified name



212
213
214
215
# File 'app/models/tasker/task.rb', line 212

scope :with_task_name, lambda { |task_name|
  joins(:named_task)
    .where(tasker_named_tasks: { name: task_name })
}

.with_versionActiveRecord::Relation

Scopes tasks by version through the named_task association

Parameters:

  • version (String)

    The version to filter by

Returns:

  • (ActiveRecord::Relation)

    Tasks with the specified version



222
223
224
225
# File 'app/models/tasker/task.rb', line 222

scope :with_version, lambda { |version|
  joins(:named_task)
    .where(tasker_named_tasks: { version: version })
}

Instance Method Details

#all_steps_complete?Boolean

Checks if all steps in the task are complete

Returns:

  • (Boolean)

    True if all steps are complete, false otherwise



318
319
320
# File 'app/models/tasker/task.rb', line 318

def all_steps_complete?
  Tasker::StepReadinessStatus.all_steps_complete_for_task?(self)
end

#dependency_graphHash

Provides runtime dependency graph analysis Delegates to RuntimeGraphAnalyzer for graph-based analysis

Returns:

  • (Hash)

    Runtime dependency graph analysis



311
312
313
# File 'app/models/tasker/task.rb', line 311

def dependency_graph
  runtime_analyzer.analyze
end

#get_step_by_name(name) ⇒ Tasker::WorkflowStep?

Finds a workflow step by its name

Parameters:

  • name (String)

    The name of the step to find

Returns:



299
300
301
# File 'app/models/tasker/task.rb', line 299

def get_step_by_name(name)
  workflow_steps.includes(:named_step).where(named_step: { name: name }).first
end

#reloadObject



326
327
328
329
# File 'app/models/tasker/task.rb', line 326

def reload
  super
  @task_execution_context = nil
end

#runtime_analyzerObject



303
304
305
# File 'app/models/tasker/task.rb', line 303

def runtime_analyzer
  @runtime_analyzer ||= Tasker::Analysis::RuntimeGraphAnalyzer.new(task: self)
end

#state_machineObject

State machine integration



70
71
72
73
74
75
76
# File 'app/models/tasker/task.rb', line 70

def state_machine
  @state_machine ||= Tasker::StateMachine::TaskStateMachine.new(
    self,
    transition_class: Tasker::TaskTransition,
    association_name: :task_transitions
  )
end

#statusObject

Status is now entirely managed by the state machine



79
80
81
82
83
84
85
86
87
# File 'app/models/tasker/task.rb', line 79

def status
  if new_record?
    # For new records, return the initial state
    Tasker::Constants::TaskStatuses::PENDING
  else
    # For persisted records, use state machine
    state_machine.current_state
  end
end

#task_execution_contextObject



322
323
324
# File 'app/models/tasker/task.rb', line 322

def task_execution_context
  @task_execution_context ||= Tasker::TaskExecutionContext.new(task_id)
end

#with_all_associatedActiveRecord::Relation

Includes all associated models for efficient querying

Returns:

  • (ActiveRecord::Relation)

    Tasks with all associated records preloaded



129
130
131
132
133
134
# File 'app/models/tasker/task.rb', line 129

scope :with_all_associated, lambda {
  includes(named_task: [:task_namespace])
    .includes(workflow_steps: %i[named_step parents children])
    .includes(task_annotations: %i[annotation_type])
    .includes(:task_transitions)
}