cache_store: handle corrupt DBM database.

When the DBM database cannot be read by the current version of Ruby's
DBM library (due to corruption or another incompatibility) it segfaults
or freezes which takes down the entire Homebrew Ruby process.

This isn't desirable so instead perform a shell out with the Homebrew
Ruby to see if it can read the DBM database before we try to use the
information. If this hangs or crashes: silently delete the database and
recreate it.
This commit is contained in:
Mike McQuaid 2018-09-20 10:57:27 +01:00
parent 1ff42f9058
commit a11fe57cd2
No known key found for this signature in database
GPG Key ID: 48A898132FD8EE70
4 changed files with 41 additions and 9 deletions

View File

@ -1,5 +1,6 @@
require "dbm"
require "json"
require "timeout"
#
# `CacheStoreDatabase` acts as an interface to a persistent storage mechanism
@ -46,7 +47,7 @@ class CacheStoreDatabase
#
# @return [Boolean]
def created?
File.exist?(cache_path)
cache_path.exist?
end
private
@ -57,6 +58,10 @@ class CacheStoreDatabase
# https://docs.oracle.com/cd/E17276_01/html/api_reference/C/envopen.html
DATABASE_MODE = 0664
# Spend 5 seconds trying to read the DBM file. If it takes longer than this it
# has likely hung or segfaulted.
DBM_TEST_READ_TIMEOUT = 5
# Lazily loaded database in read/write mode. If this method is called, a
# database file with be created in the `HOMEBREW_CACHE` with name
# corresponding to the `@type` instance variable
@ -66,6 +71,29 @@ class CacheStoreDatabase
# DBM::WRCREAT: Creates the database if it does not already exist
@db ||= begin
HOMEBREW_CACHE.mkpath
if created?
dbm_test_read_cmd = SystemCommand.new(
ENV["HOMEBREW_RUBY_PATH"],
args: [
"-rdbm",
"-e",
"DBM.open('#{dbm_file_path}', #{DATABASE_MODE}, DBM::READER).size",
],
print_stderr: false,
must_succeed: true,
)
dbm_test_read_success = begin
Timeout.timeout(DBM_TEST_READ_TIMEOUT) do
dbm_test_read_cmd.run!
true
end
rescue ErrorDuringExecution, Timeout::Error
odebug "Failed to read #{dbm_file_path}!"
Process.kill(:KILL, dbm_test_read_cmd.pid)
false
end
cache_path.delete unless dbm_test_read_success
end
DBM.open(dbm_file_path, DATABASE_MODE, DBM::WRCREAT)
end
end
@ -83,7 +111,7 @@ class CacheStoreDatabase
#
# @return [String]
def dbm_file_path
File.join(HOMEBREW_CACHE, @type.to_s)
"#{HOMEBREW_CACHE}/#{@type}"
end
# The path where the database resides in the `HOMEBREW_CACHE` for the given
@ -91,7 +119,7 @@ class CacheStoreDatabase
#
# @return [String]
def cache_path
"#{dbm_file_path}.db"
Pathname("#{dbm_file_path}.db")
end
end

View File

@ -19,6 +19,8 @@ end
class SystemCommand
extend Predicable
attr_reader :pid
def self.run(executable, **options)
new(executable, **options).run!
end
@ -122,6 +124,7 @@ class SystemCommand
raw_stdin, raw_stdout, raw_stderr, raw_wait_thr =
Open3.popen3(env, [executable, executable], *args, **options)
@pid = raw_wait_thr.pid
write_input_to(raw_stdin)
raw_stdin.close_write
@ -191,6 +194,7 @@ class SystemCommand
end
def success?
return false if @exit_status.nil?
@exit_status.zero?
end

View File

@ -118,15 +118,15 @@ describe CacheStoreDatabase do
end
describe "#created?" do
let(:cache_path) { "path/to/homebrew/cache/sample.db" }
let(:cache_path) { Pathname("path/to/homebrew/cache/sample.db") }
before(:each) do
allow(subject).to receive(:cache_path).and_return(cache_path)
end
context "`File.exist?(cache_path)` returns `true`" do
context "`cache_path.exist?` returns `true`" do
before(:each) do
allow(File).to receive(:exist?).with(cache_path).and_return(true)
allow(cache_path).to receive(:exist?).and_return(true)
end
it "returns `true`" do
@ -134,9 +134,9 @@ describe CacheStoreDatabase do
end
end
context "`File.exist?(cache_path)` returns `false`" do
context "`cache_path.exist?` returns `false`" do
before(:each) do
allow(File).to receive(:exist?).with(cache_path).and_return(false)
allow(cache_path).to receive(:exist?).and_return(false)
end
it "returns `false`" do

View File

@ -47,7 +47,7 @@ setup-ruby-path() {
then
odie "Failed to install vendor Ruby."
fi
rm -rf "$vendor_dir/bundle/ruby" "$HOMEBREW_CACHE/linkage.db"
rm -rf "$vendor_dir/bundle/ruby"
HOMEBREW_RUBY_PATH="$vendor_ruby_path"
fi
fi