Class: Ibex::GenerationTransaction
- Inherits:
-
Object
- Object
- Ibex::GenerationTransaction
- Defined in:
- lib/ibex/generation_transaction.rb,
sig/ibex/generation_transaction.rbs
Overview
Publishes a rendered ArtifactSet with rollback and a manifest-last marker.
Defined Under Namespace
Classes: Error, SourceChanged
Instance Method Summary collapse
- #acquire_lock(lock, path) ⇒ void
- #acquire_locks ⇒ void
- #backup_records ⇒ void
- #cleanup_success ⇒ void
- #commit ⇒ void
- #create_backup_link(target, directory) ⇒ String
- #create_temporary_file(directory, suffix) ⇒ File
-
#initialize(artifacts, warning: ->(_message) {}, stability_check: -> { true }, source_records: [], lock_sleeper: ->(seconds) { sleep(seconds) }) ⇒ GenerationTransaction
constructor
rubocop:disable Layout/LineLength.
- #install_group(group) ⇒ void
- #lock_path(target) ⇒ String
- #prepare_records ⇒ void
- #publication_group(artifact) ⇒ Symbol
- #release_locks ⇒ void
- #report_post_commit_failure(error) ⇒ void
- #stage_records ⇒ void
- #sync_directories! ⇒ void
- #target_mode(artifact, target) ⇒ Integer
- #warn_best_effort(message) ⇒ void
- #with_temporary_path(directory, suffix) {|arg0| ... } ⇒ void
Methods included from GenerationTransactionValidation
#ensure_sources_stable!, #existing_target, #paths_alias?, #portable_target_key, #protected_generation_paths, #protected_generation_stats, #resolved_target, #same_inode?, #stable_lock_identity?, #validate_active_locks!, #validate_current_target!, #validate_current_targets!, #validate_existing_target!, #validate_lock_boundaries!, #validate_lock_identity!, #validate_locked_targets!, #validate_source_boundaries!
Methods included from GenerationTransactionRecovery
#cleanup_artifacts, #failure_details, #handle_failure, #recovery_artifacts, #remove_artifact, #rollback, #rollback_record, #source_change_clean?, #sync_directories_best_effort
Constructor Details
#initialize(artifacts, warning: ->(_message) {}, stability_check: -> { true }, source_records: [], lock_sleeper: ->(seconds) { sleep(seconds) }) ⇒ GenerationTransaction
rubocop:disable Layout/LineLength
31 32 33 34 35 36 37 38 39 40 41 42 43 |
# File 'lib/ibex/generation_transaction.rb', line 31 def initialize(artifacts, warning: ->() {}, stability_check: -> { true }, source_records: [], lock_sleeper: ->(seconds) { sleep(seconds) }) @artifacts = artifacts @warning = warning @stability_check = stability_check @source_records = source_records @lock_sleeper = lock_sleeper @locks = [] #: Array[File] @locked_paths = [] #: Array[String] @records = [] #: Array[Hash[Symbol, untyped]] @committed = false @temporary_sequence = 0 end |
Instance Method Details
#acquire_lock(lock, path) ⇒ void
This method returns an undefined value.
128 129 130 131 132 133 134 135 136 |
# File 'lib/ibex/generation_transaction.rb', line 128 def acquire_lock(lock, path) until lock.flock(File::LOCK_EX | File::LOCK_NB) unless @stability_check.call raise SourceChanged, "(generation):1:1: generation cancelled while waiting for lock: #{path}" end @lock_sleeper.call(0.05) end end |
#acquire_locks ⇒ void
This method returns an undefined value.
111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 |
# File 'lib/ibex/generation_transaction.rb', line 111 def acquire_locks lock_paths = @records.map { |record| lock_path(record.fetch(:target)) }.uniq.sort validate_lock_boundaries!(lock_paths) @locked_paths = lock_paths @locks = [] #: Array[File] lock_paths.each do |path| flags = File::RDWR | File::CREAT flags |= File.const_get(:NOFOLLOW) if File.const_defined?(:NOFOLLOW) lock = File.new(path, flags, 0o600) @locks << lock validate_lock_identity!(path, lock) acquire_lock(lock, path) validate_lock_identity!(path, lock) end end |
#backup_records ⇒ void
This method returns an undefined value.
162 163 164 165 166 167 168 169 170 171 172 |
# File 'lib/ibex/generation_transaction.rb', line 162 def backup_records @records.each do |record| target = record.fetch(:target) next unless File.exist?(target) validate_existing_target!(target) backup = create_backup_link(target, record.fetch(:directory)) record[:backup] = backup record[:backed_up] = true end end |
#cleanup_success ⇒ void
This method returns an undefined value.
230 231 232 233 234 235 |
# File 'lib/ibex/generation_transaction.rb', line 230 def cleanup_success failures = cleanup_artifacts(preserve_backups: false) sync_failures = sync_directories_best_effort = failures + sync_failures warn_best_effort("generation cleanup incomplete: #{.join('; ')}") unless .empty? end |
#commit ⇒ void
This method returns an undefined value.
47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 |
# File 'lib/ibex/generation_transaction.rb', line 47 def commit @committed = false prepare_records validate_source_boundaries! acquire_locks prepare_records validate_source_boundaries! validate_locked_targets! ensure_sources_stable!("before staging outputs") stage_records ensure_sources_stable!("while staging outputs") backup_records sync_directories! install_group(:non_marker) sync_directories! ensure_sources_stable!("before parser publication") install_group(:parser) sync_directories! ensure_sources_stable!("after parser publication") install_group(:manifest) sync_directories! ensure_sources_stable!("after manifest publication") @committed = true cleanup_success rescue StandardError, SignalException => e @committed ? report_post_commit_failure(e) : handle_failure(e) ensure release_locks end |
#create_backup_link(target, directory) ⇒ String
185 186 187 188 189 190 191 |
# File 'lib/ibex/generation_transaction.rb', line 185 def create_backup_link(target, directory) with_temporary_path(directory, ".backup") do |path| File.link(target, path) return path end raise Error, "(generation):1:1: backup path is unavailable in #{directory}" end |
#create_temporary_file(directory, suffix) ⇒ File
175 176 177 178 179 180 181 182 |
# File 'lib/ibex/generation_transaction.rb', line 175 def create_temporary_file(directory, suffix) with_temporary_path(directory, suffix) do |path| flags = File::WRONLY | File::CREAT | File::EXCL flags |= File.const_get(:NOFOLLOW) if File.const_defined?(:NOFOLLOW) return File.new(path, flags, 0o600) end raise Error, "(generation):1:1: temporary file is unavailable in #{directory}" end |
#install_group(group) ⇒ void
This method returns an undefined value.
205 206 207 208 209 210 211 212 |
# File 'lib/ibex/generation_transaction.rb', line 205 def install_group(group) selected = @records.select { |record| publication_group(record.fetch(:artifact)) == group } selected.sort_by { |record| record.fetch(:target) }.each do |record| File.rename(record.fetch(:stage), record.fetch(:target)) record[:stage] = nil record[:installed] = true end end |
#lock_path(target) ⇒ String
139 140 141 142 |
# File 'lib/ibex/generation_transaction.rb', line 139 def lock_path(target) digest = Digest::SHA256.hexdigest(target).slice(0, 16) File.join(File.dirname(target), ".ibex-generation-#{digest}.lock") end |
#prepare_records ⇒ void
This method returns an undefined value.
80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 |
# File 'lib/ibex/generation_transaction.rb', line 80 def prepare_records @records = @artifacts.map do |artifact| target = resolved_target(artifact.path) { artifact: artifact, target: target, directory: File.dirname(target), stage: nil, backup: nil, backed_up: false, installed: false, existed: File.exist?(target), mode: target_mode(artifact, target) } end grouped = @records.group_by do |record| portable_target_key(record.fetch(:target)) end collision = grouped.find { |_key, records| records.length > 1 } return unless collision kinds = collision.fetch(1).map { |record| record.fetch(:artifact).kind }.join(", ") targets = collision.fetch(1).map { |record| record.fetch(:target) }.join(", ") raise Error, "(generation):1:1: artifact targets collide: #{targets} (#{kinds})" end |
#publication_group(artifact) ⇒ Symbol
215 216 217 218 219 220 |
# File 'lib/ibex/generation_transaction.rb', line 215 def publication_group(artifact) return :manifest if artifact.kind == :manifest return :parser if artifact.kind == :parser :non_marker end |
#release_locks ⇒ void
This method returns an undefined value.
250 251 252 253 254 255 256 257 258 259 |
# File 'lib/ibex/generation_transaction.rb', line 250 def release_locks @locks.reverse_each do |lock| lock.flock(File::LOCK_UN) lock.close rescue StandardError nil end @locks = [] #: Array[File] @locked_paths = [] #: Array[String] end |
#report_post_commit_failure(error) ⇒ void
This method returns an undefined value.
238 239 240 |
# File 'lib/ibex/generation_transaction.rb', line 238 def report_post_commit_failure(error) warn_best_effort("generation committed but cleanup failed: #{error.}") end |
#stage_records ⇒ void
This method returns an undefined value.
145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 |
# File 'lib/ibex/generation_transaction.rb', line 145 def stage_records @records.each do |record| temporary = create_temporary_file(record.fetch(:directory), ".stage") begin record[:stage] = temporary.path.dup temporary.binmode temporary.write(record.fetch(:artifact).content) temporary.flush temporary.chmod(record.fetch(:mode)) temporary.fsync ensure temporary.close end end end |
#sync_directories! ⇒ void
This method returns an undefined value.
223 224 225 226 227 |
# File 'lib/ibex/generation_transaction.rb', line 223 def sync_directories! @records.map { |record| record.fetch(:directory) }.uniq.sort.each do |directory| File.open(directory, File::RDONLY, &:fsync) end end |
#target_mode(artifact, target) ⇒ Integer
101 102 103 104 105 106 107 108 |
# File 'lib/ibex/generation_transaction.rb', line 101 def target_mode(artifact, target) return artifact.mode if artifact.mode return File.stat(target).mode & 0o7777 if File.exist?(target) mask = File.umask File.umask(mask) 0o666 & ~mask end |
#warn_best_effort(message) ⇒ void
This method returns an undefined value.
243 244 245 246 247 |
# File 'lib/ibex/generation_transaction.rb', line 243 def warn_best_effort() @warning.call() rescue StandardError nil end |
#with_temporary_path(directory, suffix) {|arg0| ... } ⇒ void
This method returns an undefined value.
194 195 196 197 198 199 200 201 202 |
# File 'lib/ibex/generation_transaction.rb', line 194 def with_temporary_path(directory, suffix) 100.times do @temporary_sequence += 1 name = ".ibex-generation-#{Process.pid}-#{object_id.to_s(36)}-#{@temporary_sequence.to_s(36)}#{suffix}" yield File.join(directory, name) rescue Errno::EEXIST next end end |