Skip to content

Open & writable database connections carried across fork() are automatically discarded in the child #558

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
Sep 18, 2024
Prev Previous commit
Simplify discard tests
  • Loading branch information
flavorjones committed Sep 18, 2024
commit 7e97204ca8d99c8ca88d424b6390cdb5922c1c81
144 changes: 72 additions & 72 deletions test/test_discarding.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,30 +2,54 @@

module SQLite3
class TestDiscardDatabase < SQLite3::TestCase
DBPATH = "test.db"

def setup
FileUtils.rm_f(DBPATH)
super
end

def teardown
super
FileUtils.rm_f(DBPATH)
end

def in_a_forked_process
@read, @write = IO.pipe
old_stderr, $stderr = $stderr, StringIO.new

Process.fork do
@read.close
begin
yield @write
rescue => e
old_stderr.write("child exception: #{e.message}")
end
@write.write($stderr.string)
@write.close
exit!
end

$stderr = old_stderr
@write.close
*@results = *@read.readlines
@read.close
end

def test_fork_discards_an_open_readwrite_connection
skip("interpreter doesn't support fork") unless Process.respond_to?(:fork)
skip("valgrind doesn't handle forking") if i_am_running_in_valgrind
skip("ruby 3.0 doesn't have Process._fork") if RUBY_VERSION < "3.1.0"

GC.start
begin
db = SQLite3::Database.new("test.db")
read, write = IO.pipe

old_stderr, $stderr = $stderr, StringIO.new
Process.fork do
read.close
db = SQLite3::Database.new(DBPATH)

in_a_forked_process do |write|
write.write(db.closed? ? "ok\n" : "fail\n")
write.write($stderr.string)

write.close
exit!
end
$stderr = old_stderr
write.close
assertion, *stderr = *read.readlines
read.close

assertion, *stderr = *@results

assert_equal("ok", assertion.chomp, "closed? did not return true")
assert_equal(1, stderr.count, "unexpected output on stderr: #{stderr.inspect}")
Expand All @@ -35,8 +59,7 @@ def test_fork_discards_an_open_readwrite_connection
"expected warning was not emitted"
)
ensure
db.close
FileUtils.rm_f("test.db")
db&.close
end
end

Expand All @@ -46,29 +69,22 @@ def test_fork_does_not_discard_closed_connections

GC.start
begin
db = SQLite3::Database.new("test.db")
read, write = IO.pipe

db = SQLite3::Database.new(DBPATH)
db.close

old_stderr, $stderr = $stderr, StringIO.new
Process.fork do
read.close

write.write($stderr.string)

write.close
exit!
in_a_forked_process do |write|
write.write(db.closed? ? "ok\n" : "fail\n")
write.write($stderr.string) # should be empty write, no warnings emitted
write.write("done\n")
end
$stderr = old_stderr
write.close
stderr = read.readlines
read.close

assert_equal(0, stderr.count, "unexpected output on stderr: #{stderr.inspect}")
assertion, *rest = *@results

assert_equal("ok", assertion.chomp, "closed? did not return true")
assert_equal(1, rest.count, "unexpected output on stderr: #{rest.inspect}")
assert_equal("done", rest.first.chomp, "unexpected output on stderr: #{rest.inspect}")
ensure
db.close
FileUtils.rm_f("test.db")
db&.close
end
end

Expand All @@ -78,36 +94,28 @@ def test_fork_does_not_discard_readonly_connections

GC.start
begin
SQLite3::Database.open("test.db") do |db|
SQLite3::Database.open(DBPATH) do |db|
db.execute("create table foo (bar int)")
db.execute("insert into foo values (1)")
end

db = SQLite3::Database.new("test.db", readonly: true)
read, write = IO.pipe

old_stderr, $stderr = $stderr, StringIO.new
Process.fork do
read.close
db = SQLite3::Database.new(DBPATH, readonly: true)

in_a_forked_process do |write|
write.write(db.closed? ? "fail\n" : "ok\n") # should be open and readable
write.write((db.execute("select * from foo") == [[1]]) ? "ok\n" : "fail\n")
write.write($stderr.string)

write.close
exit!
write.write($stderr.string) # should be an empty write, no warnings emitted
write.write("done\n")
end
$stderr = old_stderr
write.close
assertion1, assertion2, *stderr = *read.readlines
read.close

assertion1, assertion2, *rest = *@results

assert_equal("ok", assertion1.chomp, "closed? did not return false")
assert_equal("ok", assertion2.chomp, "could not read from database")
assert_equal(0, stderr.count, "unexpected output on stderr: #{stderr.inspect}")
assert_equal(1, rest.count, "unexpected output on stderr: #{rest.inspect}")
assert_equal("done", rest.first.chomp, "unexpected output on stderr: #{rest.inspect}")
ensure
db&.close
FileUtils.rm_f("test.db")
end
end

Expand All @@ -117,42 +125,35 @@ def test_close_does_not_discard_readonly_connections

GC.start
begin
SQLite3::Database.open("test.db") do |db|
SQLite3::Database.open(DBPATH) do |db|
db.execute("create table foo (bar int)")
db.execute("insert into foo values (1)")
end

db = SQLite3::Database.new("test.db", readonly: true)
read, write = IO.pipe

old_stderr, $stderr = $stderr, StringIO.new
Process.fork do
read.close
db = SQLite3::Database.new(DBPATH, readonly: true)

in_a_forked_process do |write|
write.write(db.closed? ? "fail\n" : "ok\n") # should be open and readable
db.close

write.write($stderr.string)

write.close
exit!
write.write($stderr.string) # should be an empty write, no warnings emitted
write.write("done\n")
end
$stderr = old_stderr
write.close
stderr = read.readlines
read.close

assert_equal(0, stderr.count, "unexpected output on stderr: #{stderr.inspect}")
assertion, *rest = *@results

assert_equal("ok", assertion.chomp, "closed? did not return false")
assert_equal(1, rest.count, "unexpected output on stderr: #{rest.inspect}")
assert_equal("done", rest.first.chomp, "unexpected output on stderr: #{rest.inspect}")
ensure
db&.close
FileUtils.rm_f("test.db")
end
end

def test_a_discarded_connection_with_statements
skip("discard leaks memory") if i_am_running_in_valgrind

begin
db = SQLite3::Database.new("test.db")
db = SQLite3::Database.new(DBPATH)
db.execute("create table foo (bar int)")
db.execute("insert into foo values (1)")
stmt = db.prepare("select * from foo")
Expand All @@ -165,8 +166,7 @@ def test_a_discarded_connection_with_statements
assert_nothing_raised { stmt.close }
assert_predicate(stmt, :closed?)
ensure
db.close
FileUtils.rm_f("test.db")
db&.close
end
end
end
Expand Down
Loading