Module: Ibex::GenerationTransactionRecovery

Included in:
GenerationTransaction
Defined in:
lib/ibex/generation_transaction_recovery.rb,
sig/ibex/generation_transaction_recovery.rbs

Overview

Failure recovery and best-effort cleanup for GenerationTransaction.

Instance Method Summary collapse

Instance Method Details

#cleanup_artifacts(preserve_backups:) ⇒ Array[String]

RBS:

  • (preserve_backups: bool) -> Array[String]

Parameters:

  • preserve_backups: (Boolean)

Returns:

  • (Array[String])


79
80
81
82
83
84
85
86
87
# File 'lib/ibex/generation_transaction_recovery.rb', line 79

def cleanup_artifacts(preserve_backups:)
  failures = [] #: Array[String]
  @records.each do |record|
    paths = [record[:stage]]
    paths << record[:backup] unless preserve_backups
    paths.compact.each { |path| remove_artifact(path, failures) }
  end
  failures
end

#failure_details(cause, rollback, cleanup, sync, recovery) ⇒ Array[String]

rubocop:disable Layout/LineLength

RBS:

  • (Exception cause, Array[String] rollback, Array[String] cleanup, Array[String] sync, Array[String] recovery) -> Array[String]

Parameters:

  • cause (Exception)
  • rollback (Array[String])
  • cleanup (Array[String])
  • sync (Array[String])
  • recovery (Array[String])

Returns:

  • (Array[String])


31
32
33
34
35
36
37
38
# File 'lib/ibex/generation_transaction_recovery.rb', line 31

def failure_details(cause, rollback, cleanup, sync, recovery)
  details = [cause.message]
  details << "rollback failed: #{rollback.join('; ')}" unless rollback.empty?
  details << "cleanup failed: #{cleanup.join('; ')}" unless cleanup.empty?
  details << "directory sync failed: #{sync.join('; ')}" unless sync.empty?
  details << "recovery artifacts: #{recovery.join(', ')}" unless recovery.empty?
  details
end

#handle_failure(cause) ⇒ bot

RBS:

  • (Exception cause) -> bot

Parameters:

  • cause (Exception)

Returns:

  • (bot)


10
11
12
13
14
15
16
17
18
19
20
21
22
# File 'lib/ibex/generation_transaction_recovery.rb', line 10

def handle_failure(cause)
  rollback_failures = rollback
  cleanup_failures = cleanup_artifacts(preserve_backups: !rollback_failures.empty?)
  sync_failures = sync_directories_best_effort
  raise cause if source_change_clean?(cause, rollback_failures, cleanup_failures, sync_failures)

  recovery = recovery_artifacts
  details = failure_details(cause, rollback_failures, cleanup_failures, sync_failures, recovery)
  raise GenerationTransaction::Error.new(
    "(generation):1:1: #{details.join(' | ')}",
    rollback_failed: !rollback_failures.empty?, recovery_artifacts: recovery
  ), cause: cause
end

#recovery_artifactsArray[String]

RBS:

  • () -> Array[String]

Returns:

  • (Array[String])


42
43
44
45
46
47
# File 'lib/ibex/generation_transaction_recovery.rb', line 42

def recovery_artifacts
  @records.filter_map do |record|
    backup = record[:backup]
    backup if backup && File.exist?(backup)
  end
end

#remove_artifact(path, failures) ⇒ void

This method returns an undefined value.

RBS:

  • (String path, Array[String] failures) -> void

Parameters:

  • path (String)
  • failures (Array[String])


90
91
92
93
94
95
96
# File 'lib/ibex/generation_transaction_recovery.rb', line 90

def remove_artifact(path, failures)
  File.unlink(path)
rescue Errno::ENOENT
  nil
rescue StandardError => e
  failures << "#{path}: #{e.message}"
end

#rollbackArray[String]

RBS:

  • () -> Array[String]

Returns:

  • (Array[String])


50
51
52
53
54
55
56
57
58
# File 'lib/ibex/generation_transaction_recovery.rb', line 50

def rollback
  failures = [] #: Array[String]
  @records.reverse_each do |record|
    rollback_record(record)
  rescue StandardError => e
    failures << "#{record.fetch(:target)}: #{e.message}"
  end
  failures
end

#rollback_record(record) ⇒ void

This method returns an undefined value.

RBS:

  • (Hash[Symbol, untyped] record) -> void

Parameters:

  • record (Hash[Symbol, untyped])


61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
# File 'lib/ibex/generation_transaction_recovery.rb', line 61

def rollback_record(record)
  target = record.fetch(:target)
  if record.fetch(:backed_up)
    if record.fetch(:installed)
      File.rename(record.fetch(:backup), target)
    else
      File.unlink(record.fetch(:backup))
    end
    record[:backup] = nil
    record[:backed_up] = false
    record[:installed] = false
  elsif record.fetch(:installed)
    File.unlink(target)
    record[:installed] = false
  end
end

#source_change_clean?(cause, rollback, cleanup, sync) ⇒ Boolean

RBS:

  • (Exception cause, Array[String] rollback, Array[String] cleanup, Array[String] sync) -> bool

Parameters:

  • cause (Exception)
  • rollback (Array[String])
  • cleanup (Array[String])
  • sync (Array[String])

Returns:

  • (Boolean)


25
26
27
# File 'lib/ibex/generation_transaction_recovery.rb', line 25

def source_change_clean?(cause, rollback, cleanup, sync)
  cause.is_a?(GenerationTransaction::SourceChanged) && rollback.empty? && cleanup.empty? && sync.empty?
end

#sync_directories_best_effortArray[String]

RBS:

  • () -> Array[String]

Returns:

  • (Array[String])


99
100
101
102
103
104
105
106
107
# File 'lib/ibex/generation_transaction_recovery.rb', line 99

def sync_directories_best_effort
  failures = [] #: Array[String]
  @records.map { |record| record.fetch(:directory) }.uniq.sort.each do |directory|
    File.open(directory, File::RDONLY, &:fsync)
  rescue StandardError => e
    failures << "#{directory}: #{e.message}"
  end
  failures
end