app/services/backup_repository/sidecar.rb
Rama develop · réplica de lectura del repositorio original.
require 'open3'
class BackupRepository
# Sidecar IO helpers shared by the External and S3 adapters. Both reach
# their destination only through a short-lived docker sidecar; this
# module holds the run/capture/stream wrappers that are otherwise
# identical.
#
# Including adapters must provide: `sidecar_command(*cmd)`.
module Sidecar
include Loggable
def sidecar_run(*cmd)
Open3.capture2e(*sidecar_command(*cmd))
end
# Like sidecar_run, but raises BackupRepository::Error on a non-zero
# exit — mutating operations must not update the DB after the actual
# rm/cp silently failed.
def sidecar_run!(*cmd)
output, status = sidecar_run(*cmd)
return output if status.success?
message = output.to_s.strip.presence || "exited with status #{status.exitstatus}"
logger.error("sidecar failed: #{cmd.inspect} — #{message}")
raise BackupRepository::Error, message
end
def sidecar_capture(*cmd)
Open3.capture2(*sidecar_command(*cmd))
end
# Runs a sidecar whose stdout is a tar stream and parses it into an
# ArchiveContents. Uses `popen2`, not `popen2e`, on purpose: the stdout
# is a binary tar — merging stderr into it would corrupt the archive.
# With `popen2` the sidecar's stderr passes straight through to HELIOS's
# own stderr/log instead.
#
# Every failure mode is logged and yields an empty archive: a non-zero
# exit, an unreadable tar, a sidecar that could not launch, and — the
# subtle one — a clean exit that nonetheless produced no entries (an S3
# object not yet readable in the moments right after upload).
def stream_sidecar_archive(*cmd)
Open3.popen2(*sidecar_command(*cmd)) do |_in, out, wait|
out.binmode
archive = BackupRepository.parse_tar_stream(BackupRepository::PipeIo.new(out))
# Drain anything left over so the wait status reflects a clean exit
# — TarReader stops as soon as it sees the zero-block sentinel.
out.read
verified_archive(archive, wait.value, cmd)
end
rescue Gem::Package::TarInvalidError => e
warn_archive("produced an unreadable tar (#{e.message})", cmd)
rescue Errno::ENOENT => e
warn_archive("could not be launched (#{e.message})", cmd)
end
# Runs a sidecar whose stdout is a tar stream and yields it to the block
# in 64 KB chunks (HTTP download). `popen2` keeps stderr off the stdout
# pipe so a docker warning can never end up inside the download;
# a non-zero exit raises BackupRepository::Error.
def stream_sidecar_download(*cmd)
Open3.popen2(*sidecar_command(*cmd)) do |_in, out, wait|
out.binmode
while (chunk = out.read(64 * 1024))
yield chunk
end
status = wait.value
next if status.success?
raise BackupRepository::Error, "sidecar exited with status #{status.exitstatus}"
end
end
private
def verified_archive(archive, status, cmd)
return warn_archive("exited #{status.exitstatus}", cmd) unless status.success?
warn_archive('produced no tar entries', cmd) if archive.entries.empty?
archive
end
def warn_archive(reason, cmd)
logger.warn("archive sidecar #{reason} — #{cmd.inspect}")
BackupRepository::EMPTY_ARCHIVE
end
end
end