Class: Ibex::GenerationTransaction

Inherits:
Object
  • Object
show all
Includes:
GenerationTransactionRecovery, GenerationTransactionValidation
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

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

RBS:

  • (ArtifactSet artifacts, ?warning: ^(String) -> void, ?stability_check: ^() -> bool, ?source_records: Array[GenerationInput], ?lock_sleeper: ^(Float) -> void) -> void

Parameters:

  • artifacts (ArtifactSet)
  • warning: (^(String) -> void) (defaults to: ->(_message) {})
  • stability_check: (^() -> bool) (defaults to: -> { true })
  • source_records: (Array[GenerationInput]) (defaults to: [])
  • lock_sleeper: (^(Float) -> void) (defaults to: ->(seconds) { sleep(seconds) })


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: ->(_message) {}, 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.

RBS:

  • (File lock, String path) -> void

Parameters:

  • lock (File)
  • path (String)


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_locksvoid

This method returns an undefined value.

RBS:

  • () -> void



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_recordsvoid

This method returns an undefined value.

RBS:

  • () -> void



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_successvoid

This method returns an undefined value.

RBS:

  • () -> void



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
  messages = failures + sync_failures
  warn_best_effort("generation cleanup incomplete: #{messages.join('; ')}") unless messages.empty?
end

#commitvoid

This method returns an undefined value.

RBS:

  • () -> void



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

RBS:

  • (String target, String directory) -> String

Parameters:

  • target (String)
  • directory (String)

Returns:

  • (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

RBS:

  • (String directory, String suffix) -> File

Parameters:

  • directory (String)
  • suffix (String)

Returns:

  • (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.

RBS:

  • (Symbol group) -> void

Parameters:

  • group (Symbol)


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

RBS:

  • (String target) -> String

Parameters:

  • target (String)

Returns:

  • (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_recordsvoid

This method returns an undefined value.

RBS:

  • () -> void



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

RBS:

  • (Artifact artifact) -> Symbol

Parameters:

Returns:

  • (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_locksvoid

This method returns an undefined value.

RBS:

  • () -> void



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.

RBS:

  • (Exception error) -> void

Parameters:

  • error (Exception)


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.message}")
end

#stage_recordsvoid

This method returns an undefined value.

RBS:

  • () -> void



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.

RBS:

  • () -> void



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

RBS:

  • (Artifact artifact, String target) -> Integer

Parameters:

Returns:

  • (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.

RBS:

  • (String message) -> void

Parameters:

  • message (String)


243
244
245
246
247
# File 'lib/ibex/generation_transaction.rb', line 243

def warn_best_effort(message)
  @warning.call(message)
rescue StandardError
  nil
end

#with_temporary_path(directory, suffix) {|arg0| ... } ⇒ void

This method returns an undefined value.

RBS:

  • (String directory, String suffix) { (String) -> void } -> void

Parameters:

  • directory (String)
  • suffix (String)

Yields:

Yield Parameters:

  • arg0 (String)

Yield Returns:

  • (void)


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