Class: ScriptTracker::ExecutedScript

Inherits:
ActiveRecord::Base
  • Object
show all
Defined in:
lib/script_tracker/executed_script.rb

Constant Summary collapse

DEFAULT_TIMEOUT =

Constants

300
LOCK_KEY_PREFIX =

5 minutes in seconds

0x5343525054

Class Method Summary collapse

Instance Method Summary collapse

Class Method Details

.acquire_lock(filename) ⇒ Object



61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
# File 'lib/script_tracker/executed_script.rb', line 61

def self.acquire_lock(filename)
  lock_id = generate_lock_id(filename)

  case connection.adapter_name.downcase
  when 'postgresql'
    # Use PostgreSQL advisory locks (non-blocking)
    result = connection.execute("SELECT pg_try_advisory_lock(#{lock_id})").first
    [true, 't'].include?(result['pg_try_advisory_lock'])
  when 'mysql', 'mysql2', 'trilogy'
    # Use MySQL named locks (timeout: 0 for non-blocking)
    result = connection.execute("SELECT GET_LOCK('script_tracker_#{lock_id}', 0) AS locked").first
    result['locked'] == 1 || result[0] == 1
  else
    # Fallback: use database record with unique constraint
    # This will raise an exception if script is already running
    begin
      exists?(filename: filename, status: 'running') == false
    rescue ActiveRecord::RecordNotUnique
      false
    end
  end
rescue StandardError => e
  Rails.logger&.warn("Failed to acquire lock for #{filename}: #{e.message}")
  false
end

.cleanup_stale_running_scripts(older_than: 1.hour.ago) ⇒ Object



39
40
41
42
43
44
45
46
47
# File 'lib/script_tracker/executed_script.rb', line 39

def self.cleanup_stale_running_scripts(older_than: 1.hour.ago)
  stale_scripts = running.where('executed_at < ?', older_than)
  count = stale_scripts.count
  stale_scripts.update_all(
    status: 'failed',
    output: 'Script was marked as failed due to stale running status'
  )
  count
end

.executed?(filename) ⇒ Boolean

Class methods



27
28
29
# File 'lib/script_tracker/executed_script.rb', line 27

def self.executed?(filename)
  exists?(filename: filename)
end

.generate_lock_id(filename) ⇒ Object



104
105
106
107
108
109
# File 'lib/script_tracker/executed_script.rb', line 104

def self.generate_lock_id(filename)
  # Generate a consistent integer ID from filename for advisory locks
  # Using CRC32 to convert string to integer
  require 'zlib'
  (LOCK_KEY_PREFIX << 32) | (Zlib.crc32(filename) & 0xFFFFFFFF)
end

.mark_as_running(filename) ⇒ Object



31
32
33
34
35
36
37
# File 'lib/script_tracker/executed_script.rb', line 31

def self.mark_as_running(filename)
  create!(
    filename: filename,
    executed_at: Time.current,
    status: 'running'
  )
end

.release_lock(filename) ⇒ Object



87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
# File 'lib/script_tracker/executed_script.rb', line 87

def self.release_lock(filename)
  lock_id = generate_lock_id(filename)

  case connection.adapter_name.downcase
  when 'postgresql'
    connection.execute("SELECT pg_advisory_unlock(#{lock_id})")
  when 'mysql', 'mysql2', 'trilogy'
    connection.execute("SELECT RELEASE_LOCK('script_tracker_#{lock_id}')")
  else
    # No-op for fallback strategy
    true
  end
rescue StandardError => e
  Rails.logger&.warn("Failed to release lock for #{filename}: #{e.message}")
  false
end

.with_advisory_lock(filename) ⇒ Object

Advisory lock methods for preventing concurrent execution



50
51
52
53
54
55
56
57
58
59
# File 'lib/script_tracker/executed_script.rb', line 50

def self.with_advisory_lock(filename)
  lock_acquired = acquire_lock(filename)
  return { success: false, locked: false } unless lock_acquired

  begin
    yield
  ensure
    release_lock(filename)
  end
end

Instance Method Details

#failed?Boolean



140
141
142
# File 'lib/script_tracker/executed_script.rb', line 140

def failed?
  status == 'failed'
end

#formatted_durationObject



152
153
154
155
156
157
158
159
160
161
162
163
164
# File 'lib/script_tracker/executed_script.rb', line 152

def formatted_duration
  return 'N/A' if duration.nil?

  if duration < 1
    "#{(duration * 1000).round(2)}ms"
  elsif duration < 60
    "#{duration.round(2)}s"
  else
    minutes = (duration / 60).floor
    seconds = (duration % 60).round(2)
    "#{minutes}m #{seconds}s"
  end
end

#formatted_outputObject



166
167
168
169
170
# File 'lib/script_tracker/executed_script.rb', line 166

def formatted_output
  return 'No output' if output.blank?

  output.truncate(500)
end

#mark_failed!(error_message, execution_duration = nil) ⇒ Object



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

def mark_failed!(error_message, execution_duration = nil)
  update!(
    status: 'failed',
    output: error_message,
    duration: execution_duration
  )
end

#mark_skipped!(output_text = nil, execution_duration = nil) ⇒ Object



128
129
130
131
132
133
134
# File 'lib/script_tracker/executed_script.rb', line 128

def mark_skipped!(output_text = nil, execution_duration = nil)
  update!(
    status: 'skipped',
    output: output_text,
    duration: execution_duration
  )
end

#mark_success!(output_text = nil, execution_duration = nil) ⇒ Object

Instance methods



112
113
114
115
116
117
118
# File 'lib/script_tracker/executed_script.rb', line 112

def mark_success!(output_text = nil, execution_duration = nil)
  update!(
    status: 'success',
    output: output_text,
    duration: execution_duration
  )
end

#running?Boolean



144
145
146
# File 'lib/script_tracker/executed_script.rb', line 144

def running?
  status == 'running'
end

#skipped?Boolean



148
149
150
# File 'lib/script_tracker/executed_script.rb', line 148

def skipped?
  status == 'skipped'
end

#success?Boolean



136
137
138
# File 'lib/script_tracker/executed_script.rb', line 136

def success?
  status == 'success'
end

#timed_out?Boolean



176
177
178
179
180
# File 'lib/script_tracker/executed_script.rb', line 176

def timed_out?
  return false unless running? && timeout

  Time.current > executed_at + timeout.seconds
end

#timeout_secondsObject



172
173
174
# File 'lib/script_tracker/executed_script.rb', line 172

def timeout_seconds
  timeout || DEFAULT_TIMEOUT
end