Skip to content

Commit c6bc510

Browse files
committed
Automatically close any open writable connections after a fork.
1 parent f1d4bce commit c6bc510

File tree

7 files changed

+196
-28
lines changed

7 files changed

+196
-28
lines changed

README.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -153,14 +153,14 @@ fine to share, but may require manual locking for thread safety.
153153

154154
[Sqlite is not fork
155155
safe](https://www.sqlite.org/howtocorrupt.html#_carrying_an_open_database_connection_across_a_fork_)
156-
and you should not carry an open database connection across a `fork()`. Using an inherited
156+
and instructs users to not carry an open writable database connection across a `fork()`. Using an inherited
157157
connection in the child may corrupt your database, leak memory, or cause other undefined behavior.
158158

159-
Instead, whenever possible, close connections in the parent before forking.
159+
To help protect users of this gem from accidental corruption due to this lack of fork safety, the gem will immediately close any open writable databases in the child after a fork.
160160

161-
If that's not possible or convenient, then immediately close any inherited connections in the child
162-
after forking, before opening any new connections. This will incur a small one-time memory leak per
163-
connection, but that's preferable to potentially corrupting your database.
161+
Whenever possible, close writable connections in the parent before forking. Discarding writable
162+
connections in the child will incur a small one-time memory leak per connection, but that's
163+
preferable to potentially corrupting your database.
164164

165165
See [./adr/2024-09-fork-safety.md](./adr/2024-09-fork-safety.md) for more information and context.
166166

adr/2024-09-fork-safety.md

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11

2-
# 2024-09 Discard database connections when carried across fork()of fork safety
2+
# 2024-09 Automatically close database connections when carried across fork()
33

44
## Status
55

@@ -15,17 +15,23 @@ SQLite is known to not be fork-safe[^howto], so this was not entirely surprising
1515
Advice from upstream contributors[^advice] is, essentially: don't fork if you have open database connections. Or, if you have forked, don't call `sqlite3_close` on those connections and thereby leak some amount of memory in the child process. Neither of these options are ideal, see below.
1616

1717

18-
## Decision
18+
## Decisions
1919

20-
Open database connections carried across a `fork()` will not be fully closed in the child process, to avoid the risk of corrupting the database file.
20+
1. Open writable database connections carried across a `fork()` will automatically be closed in the child process to mitigate the risk of corrupting the database file.
21+
2. These connections will be incompletely closed ("discarded") which will result in a one-time memory leak in the child process.
2122

22-
The sqlite3-ruby gem will track the ID of the process that opened each database connection. If, when the database is closed (either explicitly with `Database#close` or implicitly via GC) the current process ID is different from the original process, then we "discard" the connection.
23+
First, the gem will register an "after fork" handler via `Process._fork` that will close any open writable database connections in the child process. This is a best-effort attempt to avoid corruption, but it is not guaranteed to prevent corruption in all cases. Any connections closed by this handler will also emit a warning to let users know what's happening.
24+
25+
Second, the sqlite3-ruby gem will store the ID of the process that opened each database connection. If, when a writable database is closed (either explicitly with `Database#close` or implicitly via GC or after-fork callback) the current process ID is different from the original process, then we "discard" the connection.
2326

2427
"Discard" here means:
2528

2629
- The `Database` object acts "closed", including returning `true` from `#closed?`.
2730
- `sqlite3_close_v2` is not called on the object, because it is unsafe to do so per sqlite instructions[^howto]. As a result, some memory will be lost permanently (a one-time "memory leak").
2831
- Open file descriptors associated with the database are closed.
32+
- Any memory that can be freed safely is recovered.
33+
34+
Note that readonly databases are being treated as "fork safe" and are not affected by any of these changes.
2935

3036

3137
## Consequences

ext/sqlite3/database.c

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@ static void
1616
close_or_discard_db(sqlite3RubyPtr ctx)
1717
{
1818
if (ctx->db) {
19-
if (ctx->owner == getpid()) {
19+
int isReadonly = (ctx->flags & SQLITE_OPEN_READONLY);
20+
21+
if (isReadonly || ctx->owner == getpid()) {
2022
// Ordinary close.
2123
sqlite3_close_v2(ctx->db);
2224
} else {
@@ -25,9 +27,10 @@ close_or_discard_db(sqlite3RubyPtr ctx)
2527
sqlite3_file *sfile;
2628
int status;
2729

28-
rb_warning("An open sqlite database connection was inherited from a forked process and "
29-
"is being discarded. This is a memory leak. If possible, please close all sqlite "
30-
"database connections before forking.");
30+
rb_warning("An open writable sqlite database connection was inherited from a "
31+
"forked process and is being closed to prevent the risk of corruption. "
32+
"If possible, please close all writable sqlite database connections "
33+
"before forking.");
3134

3235
// release as much heap memory as possible by deallocating non-essential memory
3336
// allocations held by the database library. Memory used to cache database pages to
@@ -124,6 +127,7 @@ rb_sqlite3_open_v2(VALUE self, VALUE file, VALUE mode, VALUE zvfs)
124127
{
125128
sqlite3RubyPtr ctx;
126129
int status;
130+
int flags;
127131

128132
TypedData_Get_Struct(self, sqlite3Ruby, &database_type, ctx);
129133

@@ -136,14 +140,16 @@ rb_sqlite3_open_v2(VALUE self, VALUE file, VALUE mode, VALUE zvfs)
136140
# endif
137141
#endif
138142

143+
flags = NUM2INT(mode);
139144
status = sqlite3_open_v2(
140145
StringValuePtr(file),
141146
&ctx->db,
142-
NUM2INT(mode),
147+
flags,
143148
NIL_P(zvfs) ? NULL : StringValuePtr(zvfs)
144149
);
145150

146-
CHECK(ctx->db, status)
151+
CHECK(ctx->db, status);
152+
ctx->flags = flags;
147153

148154
return self;
149155
}
@@ -919,6 +925,10 @@ rb_sqlite3_open16(VALUE self, VALUE file)
919925

920926
status = sqlite3_open16(utf16_string_value_ptr(file), &ctx->db);
921927

928+
// these are the perm flags used implicitly by sqlite3_open16,
929+
// see https://www.sqlite.org/capi3ref.html#sqlite3_open
930+
ctx->flags = SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE;
931+
922932
CHECK(ctx->db, status)
923933

924934
return INT2NUM(status);

ext/sqlite3/database.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ struct _sqlite3Ruby {
99
int stmt_timeout;
1010
struct timespec stmt_deadline;
1111
rb_pid_t owner;
12+
int flags;
1213
};
1314

1415
typedef struct _sqlite3Ruby sqlite3Ruby;

lib/sqlite3/database.rb

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
require "sqlite3/pragmas"
66
require "sqlite3/statement"
77
require "sqlite3/value"
8+
require "sqlite3/fork_safety"
89

910
module SQLite3
1011
# The Database class encapsulates a single connection to a SQLite3 database.
@@ -134,6 +135,8 @@ def initialize file, options = {}, zvfs = nil
134135
@readonly = mode & Constants::Open::READONLY != 0
135136
@default_transaction_mode = options[:default_transaction_mode] || :deferred
136137

138+
ForkSafety.track(self)
139+
137140
if block_given?
138141
begin
139142
yield self

lib/sqlite3/fork_safety.rb

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
# frozen_string_literal: true
2+
3+
require "weakref"
4+
5+
# based on Rails's active_support/fork_safety.rb
6+
module SQLite3
7+
module ForkSafety
8+
module CoreExt
9+
def _fork
10+
pid = super
11+
if pid == 0
12+
ForkSafety.discard
13+
end
14+
pid
15+
end
16+
end
17+
18+
@databases = []
19+
20+
class << self
21+
def hook!
22+
::Process.singleton_class.prepend(CoreExt)
23+
end
24+
25+
def track(database)
26+
@databases << WeakRef.new(database)
27+
end
28+
29+
def discard
30+
@databases.each do |db|
31+
next unless db.weakref_alive?
32+
33+
unless db.closed? || db.readonly?
34+
db.close
35+
end
36+
end
37+
@databases.clear
38+
end
39+
end
40+
end
41+
end
42+
43+
SQLite3::ForkSafety.hook!

test/test_database.rb

Lines changed: 118 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -721,40 +721,37 @@ def test_transaction_returns_block_result
721721
result = @db.transaction { :foo }
722722
assert_equal :foo, result
723723
end
724+
end
724725

725-
def test_discard_a_connection
726+
class TestDiscardDatabase < SQLite3::TestCase
727+
def test_fork_discards_an_open_readwrite_connection
726728
skip("interpreter doesn't support fork") unless Process.respond_to?(:fork)
727729
skip("valgrind doesn't handle forking") if i_am_running_in_valgrind
728730

731+
GC.start
729732
begin
733+
db = SQLite3::Database.new("test.db")
730734
read, write = IO.pipe
731735

732-
db = SQLite3::Database.new("test.db")
733-
db.execute("attach database 'test.db' as 'foo';") # exercise sqlite3_db_name()
736+
old_stderr, $stderr = $stderr, StringIO.new
734737
Process.fork do
735738
read.close
736-
$stderr = StringIO.new
737739

738-
result = db.close
739-
740-
write.write((result == db) ? "ok\n" : "fail\n")
741740
write.write(db.closed? ? "ok\n" : "fail\n")
742741
write.write($stderr.string)
743742

744743
write.close
745744
exit!
746745
end
746+
$stderr = old_stderr
747747
write.close
748-
749-
assert1, assert2, *stderr = *read.readlines
748+
assertion, *stderr = *read.readlines
750749
read.close
751750

752-
assert_equal("ok", assert1.chomp, "return value was not the database")
753-
assert_equal("ok", assert2.chomp, "closed? did not return true")
754-
751+
assert_equal("ok", assertion.chomp, "closed? did not return true")
755752
assert_equal(1, stderr.count, "unexpected output on stderr: #{stderr.inspect}")
756753
assert_match(
757-
/warning: An open sqlite database connection was inherited from a forked process/,
754+
/warning: An open writable sqlite database connection was inherited from a forked process/,
758755
stderr.first,
759756
"expected warning was not emitted"
760757
)
@@ -763,5 +760,113 @@ def test_discard_a_connection
763760
FileUtils.rm_f("test.db")
764761
end
765762
end
763+
764+
def test_fork_does_not_discard_closed_connections
765+
skip("interpreter doesn't support fork") unless Process.respond_to?(:fork)
766+
skip("valgrind doesn't handle forking") if i_am_running_in_valgrind
767+
768+
GC.start
769+
begin
770+
db = SQLite3::Database.new("test.db")
771+
read, write = IO.pipe
772+
773+
db.close
774+
775+
old_stderr, $stderr = $stderr, StringIO.new
776+
Process.fork do
777+
read.close
778+
779+
write.write($stderr.string)
780+
781+
write.close
782+
exit!
783+
end
784+
$stderr = old_stderr
785+
write.close
786+
stderr = read.readlines
787+
read.close
788+
789+
assert_equal(0, stderr.count, "unexpected output on stderr: #{stderr.inspect}")
790+
ensure
791+
db.close
792+
FileUtils.rm_f("test.db")
793+
end
794+
end
795+
796+
def test_fork_does_not_discard_readonly_connections
797+
skip("interpreter doesn't support fork") unless Process.respond_to?(:fork)
798+
skip("valgrind doesn't handle forking") if i_am_running_in_valgrind
799+
800+
GC.start
801+
begin
802+
SQLite3::Database.open("test.db") do |db|
803+
db.execute("create table foo (bar int)")
804+
db.execute("insert into foo values (1)")
805+
end
806+
807+
db = SQLite3::Database.new("test.db", readonly: true)
808+
read, write = IO.pipe
809+
810+
old_stderr, $stderr = $stderr, StringIO.new
811+
Process.fork do
812+
read.close
813+
814+
write.write(db.closed? ? "fail\n" : "ok\n") # should be open and readable
815+
write.write((db.execute("select * from foo") == [[1]]) ? "ok\n" : "fail\n")
816+
write.write($stderr.string)
817+
818+
write.close
819+
exit!
820+
end
821+
$stderr = old_stderr
822+
write.close
823+
assertion1, assertion2, *stderr = *read.readlines
824+
read.close
825+
826+
assert_equal("ok", assertion1.chomp, "closed? did not return false")
827+
assert_equal("ok", assertion2.chomp, "could not read from database")
828+
assert_equal(0, stderr.count, "unexpected output on stderr: #{stderr.inspect}")
829+
ensure
830+
db&.close
831+
FileUtils.rm_f("test.db")
832+
end
833+
end
834+
835+
def test_close_does_not_discard_readonly_connections
836+
skip("interpreter doesn't support fork") unless Process.respond_to?(:fork)
837+
skip("valgrind doesn't handle forking") if i_am_running_in_valgrind
838+
839+
GC.start
840+
begin
841+
SQLite3::Database.open("test.db") do |db|
842+
db.execute("create table foo (bar int)")
843+
db.execute("insert into foo values (1)")
844+
end
845+
846+
db = SQLite3::Database.new("test.db", readonly: true)
847+
read, write = IO.pipe
848+
849+
old_stderr, $stderr = $stderr, StringIO.new
850+
Process.fork do
851+
read.close
852+
853+
db.close
854+
855+
write.write($stderr.string)
856+
857+
write.close
858+
exit!
859+
end
860+
$stderr = old_stderr
861+
write.close
862+
stderr = read.readlines
863+
read.close
864+
865+
assert_equal(0, stderr.count, "unexpected output on stderr: #{stderr.inspect}")
866+
ensure
867+
db&.close
868+
FileUtils.rm_f("test.db")
869+
end
870+
end
766871
end
767872
end

0 commit comments

Comments
 (0)