Skip to content

Commit d16ac2c

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

File tree

9 files changed

+207
-30
lines changed

9 files changed

+207
-30
lines changed

CHANGELOG.md

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,16 @@
22

33
## next / unreleased
44

5-
### Changed
5+
### Fork safety improvements
6+
7+
Sqlite itself is [not fork-safe](https://www.sqlite.org/howtocorrupt.html#_carrying_an_open_database_connection_across_a_fork_). Specifically, writing in a child process to a database connection that was created in the parent process may corrupt the database file. To mitigate this risk, sqlite3-ruby has implemented the following changes:
8+
9+
- Open writable database connections carried across a `fork()` will immediately be closed in the child process to mitigate the risk of corrupting the database file.
10+
- These connections will be incompletely closed ("discarded") which will result in a one-time memory leak in the child process.
11+
12+
If it's at all possible, we strongly recommend that you close writable database connections in the parent before forking.
613

7-
- Any database connections carried across a `fork()` will not be fully closed to help protect database files against corruption. Using a database connection in a child process that was created in a parent process is unsafe and may corrupt the database file. If an inherited connection is closed then a warning will be emitted and some reserved memory will be lost to the child process permanently. See the README "Fork Safety" section and `adr/2024-09-fork-safety.md` for more information. [#558] @flavorjones
14+
See the README "Fork Safety" section and `adr/2024-09-fork-safety.md` for more information. [#558] @flavorjones
815

916

1017
### Improved

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_tracker.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!

sqlite3.gemspec

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ Gem::Specification.new do |s|
6262
"lib/sqlite3/constants.rb",
6363
"lib/sqlite3/database.rb",
6464
"lib/sqlite3/errors.rb",
65+
"lib/sqlite3/fork_safety.rb",
6566
"lib/sqlite3/pragmas.rb",
6667
"lib/sqlite3/resultset.rb",
6768
"lib/sqlite3/statement.rb",

test/test_database.rb

Lines changed: 119 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -721,40 +721,38 @@ 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
730+
skip("ruby 3.0 doesn't have Process._fork") if RUBY_VERSION < "3.1.0"
728731

732+
GC.start
729733
begin
734+
db = SQLite3::Database.new("test.db")
730735
read, write = IO.pipe
731736

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

738-
result = db.close
739-
740-
write.write((result == db) ? "ok\n" : "fail\n")
741741
write.write(db.closed? ? "ok\n" : "fail\n")
742742
write.write($stderr.string)
743743

744744
write.close
745745
exit!
746746
end
747+
$stderr = old_stderr
747748
write.close
748-
749-
assert1, assert2, *stderr = *read.readlines
749+
assertion, *stderr = *read.readlines
750750
read.close
751751

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

0 commit comments

Comments
 (0)