Class: ScriptTracker::ExecutedScript
- Inherits:
-
ActiveRecord::Base
- Object
- ActiveRecord::Base
- ScriptTracker::ExecutedScript
- 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
- .acquire_lock(filename) ⇒ Object
- .cleanup_stale_running_scripts(older_than: 1.hour.ago) ⇒ Object
-
.executed?(filename) ⇒ Boolean
Class methods.
- .generate_lock_id(filename) ⇒ Object
- .mark_as_running(filename) ⇒ Object
- .release_lock(filename) ⇒ Object
-
.with_advisory_lock(filename) ⇒ Object
Advisory lock methods for preventing concurrent execution.
Instance Method Summary collapse
- #failed? ⇒ Boolean
- #formatted_duration ⇒ Object
- #formatted_output ⇒ Object
- #mark_failed!(error_message, execution_duration = nil) ⇒ Object
- #mark_skipped!(output_text = nil, execution_duration = nil) ⇒ Object
-
#mark_success!(output_text = nil, execution_duration = nil) ⇒ Object
Instance methods.
- #running? ⇒ Boolean
- #skipped? ⇒ Boolean
- #success? ⇒ Boolean
- #timed_out? ⇒ Boolean
- #timeout_seconds ⇒ Object
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_duration ⇒ Object
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_output ⇒ Object
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!(, execution_duration = nil) update!( status: 'failed', output: , 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_seconds ⇒ Object
172 173 174 |
# File 'lib/script_tracker/executed_script.rb', line 172 def timeout_seconds timeout || DEFAULT_TIMEOUT end |