Class: ScriptTracker::Base

Inherits:
Object
  • Object
show all
Defined in:
lib/script_tracker/base.rb

Defined Under Namespace

Classes: ScriptSkipped, ScriptTimeoutError

Class Method Summary collapse

Class Method Details

.check_timeout!(start_time, max_duration = timeout) ⇒ Object

Check if execution has exceeded timeout (safer alternative to Timeout.timeout) Use this inside your scripts for manual timeout checking:

def self.execute

start_time = Time.current
User.find_each do |user|
  check_timeout!(start_time, timeout)
  # Process user...
end

end

Raises:



120
121
122
123
124
125
# File 'lib/script_tracker/base.rb', line 120

def check_timeout!(start_time, max_duration = timeout)
  elapsed = Time.current - start_time
  return unless max_duration&.positive? && elapsed > max_duration

  raise ScriptTimeoutError, "Script execution exceeded #{max_duration} seconds (elapsed: #{elapsed.round(2)}s)"
end

.executeObject

Raises:

  • (NotImplementedError)


70
71
72
# File 'lib/script_tracker/base.rb', line 70

def execute
  raise NotImplementedError, 'Subclasses must implement the execute method'
end

.execute_with_transactionObject



64
65
66
67
68
# File 'lib/script_tracker/base.rb', line 64

def execute_with_transaction
  ActiveRecord::Base.transaction do
    execute
  end
end

.log(message, level: :info) ⇒ Object



80
81
82
83
84
# File 'lib/script_tracker/base.rb', line 80

def log(message, level: :info)
  timestamp = Time.current.strftime('%Y-%m-%d %H:%M:%S')
  prefix = level == :error ? '[ERROR]' : '[INFO]'
  puts "#{prefix} [#{timestamp}] #{message}"
end

.log_progress(current, total, message = nil) ⇒ Object



86
87
88
89
90
91
# File 'lib/script_tracker/base.rb', line 86

def log_progress(current, total, message = nil)
  percentage = ((current.to_f / total) * 100).round(2)
  progress_detail = "(#{current}/#{total} - #{percentage}%)"
  msg = message ? "#{message} #{progress_detail}" : "Progress: #{current}/#{total} (#{percentage}%)"
  log(msg)
end

.process_in_batches(relation, batch_size: 1000, &block) ⇒ Object



93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
# File 'lib/script_tracker/base.rb', line 93

def process_in_batches(relation, batch_size: 1000, &block)
  total = relation.count
  log("There are #{total} records to process")
  return 0 if total == 0

  processed = 0
  log("Processing #{total} records in batches of #{batch_size}")
  relation.find_each(batch_size: batch_size) do |record|
    block.call(record)
    processed += 1
    log_interval = [batch_size, (total * 0.1).to_i].max
    log_progress(processed, total) if (processed % log_interval) == 0
  end
  log_progress(processed, total, 'Completed')
  processed
end

.run(_executed_script_record = nil) ⇒ Object



18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
# File 'lib/script_tracker/base.rb', line 18

def run(_executed_script_record = nil)
  require 'timeout'
  start_time = Time.current
  timeout_seconds = timeout

  begin
    result = nil

    # Wrap execution in timeout if specified
    # WARNING: Ruby's Timeout.timeout has known issues and can interrupt code at any point,
    # potentially leaving resources in inconsistent states. Consider these alternatives:
    # 1. Implement timeout logic within your script using Time.current checks
    # 2. Use database statement_timeout for PostgreSQL
    # 3. Monitor script execution time and kill from outside if needed
    # This timeout is provided as a last resort safety measure.
    if timeout_seconds && timeout_seconds > 0
      Timeout.timeout(timeout_seconds, ScriptTimeoutError) do
        result = execute_with_transaction
      end
    else
      result = execute_with_transaction
    end

    duration = Time.current - start_time
    output = "Script completed successfully in #{duration.round(2)}s"
    log(output)

    { success: true, skipped: false, output: output, duration: duration }
  rescue ScriptSkipped => e
    duration = Time.current - start_time
    output = e.message.presence || 'Script was skipped (no action needed)'
    { success: false, skipped: true, output: output, duration: duration }
  rescue ScriptTimeoutError
    duration = Time.current - start_time
    error_message = "Script execution exceeded timeout of #{timeout_seconds} seconds"
    log(error_message, level: :error)
    { success: false, skipped: false, output: error_message, duration: duration }
  rescue StandardError => e
    duration = Time.current - start_time
    error_message = "#{e.class}: #{e.message}\n#{e.backtrace.first(10).join("\n")}"
    log(error_message, level: :error)

    { success: false, skipped: false, output: error_message, duration: duration }
  end
end

.skip!(reason = nil) ⇒ Object

Raises:



74
75
76
77
78
# File 'lib/script_tracker/base.rb', line 74

def skip!(reason = nil)
  message = reason ? "Skipping: #{reason}" : 'Skipping script'
  log(message)
  raise ScriptSkipped, message
end

.timeoutObject

Default timeout: 5 minutes Override in subclass to customize:

def self.timeout
  3600 # 1 hour
end


14
15
16
# File 'lib/script_tracker/base.rb', line 14

def timeout
  300 # 5 minutes in seconds
end