Class: Fractor::Workflow::JobExecutor

Inherits:
Object
  • Object
show all
Defined in:
lib/fractor/workflow/execution/job_executor.rb

Overview

Executes a single workflow job, handling all aspects of job execution including input building, work creation, and supervisor orchestration.

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(context, logger, workflow: nil, completed_jobs: nil, failed_jobs: nil, dead_letter_queue: nil, circuit_breakers: nil) ⇒ JobExecutor

Initialize the job executor.

Parameters:

  • context (WorkflowContext)

    The workflow execution context

  • logger (WorkflowLogger)

    The workflow logger

  • workflow (Workflow) (defaults to: nil)

    The workflow instance

  • completed_jobs (Set<String>) (defaults to: nil)

    Set of completed job names

  • failed_jobs (Set<String>) (defaults to: nil)

    Set of failed job names

  • dead_letter_queue (DeadLetterQueue, nil) (defaults to: nil)

    Optional DLQ for failed jobs

  • circuit_breakers (CircuitBreakerRegistry) (defaults to: nil)

    Circuit breaker registry



23
24
25
26
27
28
29
30
31
32
# File 'lib/fractor/workflow/execution/job_executor.rb', line 23

def initialize(context, logger, workflow: nil, completed_jobs: nil, failed_jobs: nil,
               dead_letter_queue: nil, circuit_breakers: nil)
  @context = context
  @logger = logger
  @workflow = workflow
  @completed_jobs = completed_jobs || Set.new
  @failed_jobs = failed_jobs || Set.new
  @dead_letter_queue = dead_letter_queue
  @circuit_breakers = circuit_breakers || CircuitBreakerRegistry.new
end

Instance Attribute Details

#contextObject (readonly)

Returns the value of attribute context.



12
13
14
# File 'lib/fractor/workflow/execution/job_executor.rb', line 12

def context
  @context
end

#dead_letter_queueObject (readonly)

Returns the value of attribute dead_letter_queue.



12
13
14
# File 'lib/fractor/workflow/execution/job_executor.rb', line 12

def dead_letter_queue
  @dead_letter_queue
end

#loggerObject (readonly)

Returns the value of attribute logger.



12
13
14
# File 'lib/fractor/workflow/execution/job_executor.rb', line 12

def logger
  @logger
end

Instance Method Details

#execute_once(job, job_trace = nil) ⇒ Object

Execute a job once (no retry logic).

Parameters:

Returns:

  • (Object)

    The job output



39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
# File 'lib/fractor/workflow/execution/job_executor.rb', line 39

def execute_once(job, job_trace = nil)
  # Build input for this job
  job_input = @context.build_job_input(job)
  job_trace&.set_input(job_input)

  # Create work item - if job_input is already a Work object, use it directly
  # to avoid double-wrapping (e.g., when using custom Work subclasses)
  work = if job_input.is_a?(Work)
           job_input
         else
           Work.new(job_input)
         end

  # Execute with circuit breaker if configured
  if job.circuit_breaker_enabled?
    execute_with_circuit_breaker(job, work, job_trace)
  else
    execute_with_supervisor(job, work)
  end
end

#execute_with_circuit_breaker(job, work, _job_trace = nil) ⇒ Object

Execute a job with circuit breaker protection.

Parameters:

Returns:

  • (Object)

    The job output



123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
# File 'lib/fractor/workflow/execution/job_executor.rb', line 123

def execute_with_circuit_breaker(job, work, _job_trace = nil)
  breaker_key = job.circuit_breaker_key

  # Get or create circuit breaker orchestrator for this job
  orchestrator = @circuit_breakers.get_or_create_orchestrator(
    breaker_key,
    **job.circuit_breaker_config.slice(:threshold, :timeout,
                                       :half_open_calls),
    job_name: job.name,
    debug: ENV["FRACTOR_DEBUG"] == "1",
  )

  # Log circuit state before execution
  log_circuit_breaker_state(job, orchestrator)

  begin
    orchestrator.execute_with_breaker(job) do
      execute_with_supervisor(job, work)
    end
  rescue Workflow::CircuitOpenError => e
    log_circuit_breaker_open(job, orchestrator)
    raise WorkflowExecutionError,
          "Circuit breaker open for job '#{job.name}': #{e.message}"
  end
end

#execute_with_retry(job, job_trace = nil) ⇒ Object

Execute a job with retry logic.

Parameters:

Returns:

  • (Object)

    The job output



65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
# File 'lib/fractor/workflow/execution/job_executor.rb', line 65

def execute_with_retry(job, job_trace = nil)
  retry_config = job.retry_config

  # Create retry orchestrator with the job's retry configuration
  orchestrator = RetryOrchestrator.new(retry_config,
                                       debug: ENV["FRACTOR_DEBUG"] == "1")

  # Execute with retry logic
  orchestrator.execute_with_retry(job) do |j|
    execute_once(j, job_trace)
  end
rescue StandardError => e
  # Get retry state for DLQ entry
  retry_state = orchestrator.state
  add_to_dead_letter_queue(job, e, retry_state)
  raise e
end

#execute_with_supervisor(job, work) ⇒ Object

Execute a job using a supervisor.

Parameters:

  • job (Job)

    The job to execute

  • work (Work)

    The work item to process

Returns:

  • (Object)

    The job output



88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
# File 'lib/fractor/workflow/execution/job_executor.rb', line 88

def execute_with_supervisor(job, work)
  supervisor = Supervisor.new(
    worker_pools: [
      {
        worker_class: job.worker_class,
        num_workers: job.num_workers || 1,
      },
    ],
  )

  supervisor.add_work_item(work)
  supervisor.run

  # Check for errors first (before checking results)
  unless supervisor.results.errors.empty?
    error = supervisor.results.errors.first
    raise WorkflowExecutionError,
          "Job '#{job.name}' encountered error: #{error.error}"
  end

  # Get the result
  results = supervisor.results.results
  if results.empty?
    raise WorkflowExecutionError, "Job '#{job.name}' produced no results"
  end

  results.first.result
end