Skip to content

Commit

Permalink
Record the hardlinks in the commit message, so we can restore them
Browse files Browse the repository at this point in the history
git doesn't care about hardlinks; when it does the checkout it creates
different files.

Here we record the hardlinks in the install_dir as a json blob in the
commit message, then use that to re-hardlink after cache restore.

Signed-off-by: Richard Clamp <[email protected]>
  • Loading branch information
richardc committed Mar 16, 2018
1 parent 819a35d commit 29dee9e
Show file tree
Hide file tree
Showing 2 changed files with 116 additions and 3 deletions.
43 changes: 41 additions & 2 deletions lib/omnibus/git_cache.rb
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ class GitCache
# will not have the generated content, so these snapshots would be
# incompatible with the current omnibus codebase. Incrementing the serial
# number ensures these old shapshots will not be used in subsequent builds.
SERIAL_NUMBER = 3
SERIAL_NUMBER = 4

REQUIRED_GIT_FILES = %w{
HEAD
Expand Down Expand Up @@ -126,11 +126,12 @@ def incremental

create_cache_path
remove_git_dirs
hardlinks = find_hardlinks

git_cmd("add -A -f")

begin
git_cmd(%Q{commit -q -m "Backup of #{tag}"})
git_cmd("commit -q -F -", input: "Backup of #{tag}\n\n#{FFI_Yajl::Encoder.encode(hardlinks, pretty: true)}")
rescue CommandFailed => e
raise unless e.message.include?("nothing to commit")
end
Expand Down Expand Up @@ -159,6 +160,7 @@ def restore

def restore_from_cache
git_cmd("checkout -f restore_here")
restore_hardlinks
ensure
git_cmd("tag -d restore_here")
end
Expand All @@ -184,6 +186,43 @@ def remove_git_dirs
true
end

# Discover any hardlinked files in the install_dir
#
# @return [Hash{String => Array<String>}]
def find_hardlinks
hardlink_sources = {}
hardlinks = {}
Omnibus::FileSyncer.all_files_under(install_dir).each do |path|
stat = File.stat(path)
if stat.ftype.to_sym == :file && stat.nlink > 1
key = [stat.dev, stat.ino]
if source = hardlink_sources[key]
hardlinks[source] ||= []
hardlinks[source] << path
else
hardlink_sources[key] = path
end
end
end
hardlinks
end

# Restores any hardlinking from the commit message body. Body is assumed to
# be the JSON encoded return value from #find_hardlinks.
#
# @return true
def restore_hardlinks
body = git_cmd("log --format=%b -n 1").stdout
hardlinks = FFI_Yajl::Parser.parse(body)

hardlinks.each do |source, dest|
dest.each do |path|
FileUtils.ln(source, path, force: true)
end
end
true
end

private

#
Expand Down
76 changes: 75 additions & 1 deletion spec/unit/git_cache_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -105,10 +105,83 @@ module Omnibus
end
end

describe "#find_hardlinks" do
let(:regular_file_stat) do
stat = double(File::Stat)
allow(stat).to receive(:ftype).and_return(:file)
allow(stat).to receive(:nlink).and_return(1)
stat
end

let(:hardlinked_file_stat) do
stat = double(File::Stat)
allow(stat).to receive(:ftype).and_return(:file)
allow(stat).to receive(:nlink).and_return(2)
allow(stat).to receive(:dev).and_return(5)
allow(stat).to receive(:ino).and_return(25)
stat
end

before do
allow(File).to receive(:stat).and_return(regular_file_stat)
allow(File).to receive(:stat).with("foo").and_return(hardlinked_file_stat)
allow(File).to receive(:stat).with("bar").and_return(hardlinked_file_stat)

allow(Omnibus::FileSyncer).to receive(:all_files_under).and_return(
%w{ foo bar baz quux }
)
end

it "returns some hardlinks" do
expect(ipc.find_hardlinks).to eq({ "foo" => ["bar"] })
end
end

describe "#restore_hardlinks" do
let(:hardlinks) do
{
"/opt/demo/bin/file1" => [
"/opt/demo/bin/file2",
"/opt/demo/bin/file3",
],
}
end

let(:git_log_output) { FFI_Yajl::Encoder.encode(hardlinks) }

let(:log_cmd) do
cmd_double = double(Mixlib::ShellOut)
allow(cmd_double).to receive(:stdout).and_return(git_log_output)
cmd_double
end

before(:each) do
allow(ipc).to receive(:git_cmd)
.with("log --format=%b -n 1").and_return(log_cmd)
allow(FileUtils).to receive(:ln)
end

it "checks the commit message" do
expect(ipc).to receive(:git_cmd)
.with("log --format=%b -n 1")

ipc.restore_hardlinks
end

it "recreates hardlinks" do
expect(FileUtils).to receive(:ln)
.with("/opt/demo/bin/file1", "/opt/demo/bin/file2", force: true)
expect(FileUtils).to receive(:ln)
.with("/opt/demo/bin/file1", "/opt/demo/bin/file3", force: true)
ipc.restore_hardlinks
end
end

describe "#incremental" do
before(:each) do
allow(ipc).to receive(:git_cmd)
allow(ipc).to receive(:create_cache_path)
allow(ipc).to receive(:find_hardlinks).and_return({})
end

it "creates the cache path" do
Expand All @@ -125,7 +198,7 @@ module Omnibus

it "commits the backup for the software" do
expect(ipc).to receive(:git_cmd)
.with(%Q{commit -q -m "Backup of #{ipc.tag}"})
.with("commit -q -F -", input: "Backup of #{ipc.tag}\n\n{\n\n}\n")
ipc.incremental
end

Expand Down Expand Up @@ -176,6 +249,7 @@ module Omnibus
allow(ipc).to receive(:git_cmd)
.with(%Q{tag -f restore_here "#{ipc.tag}"})
allow(ipc).to receive(:create_cache_path)
allow(ipc).to receive(:restore_hardlinks)
end

it "creates the cache path" do
Expand Down

0 comments on commit 29dee9e

Please sign in to comment.