Skip to content

Commit 760d766

Browse files
committed
Database#close invokes discard if the pid has changed
This will help prevent corrupting the database. Also provide a warning to let people know they're doing something that's not supported by sqlite.
1 parent 4f0ab9f commit 760d766

File tree

5 files changed

+53
-15
lines changed

5 files changed

+53
-15
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,12 @@
22

33
## next / unreleased
44

5+
### Added
6+
7+
- New method `Database#discard` is intended to be used after forking a process with an open database connection. Forking is not supported by sqlite, and as an alternativ `discard` will "close" the connection and release what resources can be safely released. [#558] @flavorjones
8+
- `Database#close` will detect if it's being closed from a child process, and if so will emit a warning and invoke `discard` instead. [#558] @flavorjones
9+
10+
511
### Improved
612

713
- Use `sqlite3_close_v2` to close databases in a deferred manner if there are unclosed prepared statements. Previously closing a database while statements were open resulted in a `BusyException`. See https://www.sqlite.org/c3ref/close.html for more context. [#557] @flavorjones

ext/sqlite3/database.c

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ static void
2323
deallocate(void *ctx)
2424
{
2525
sqlite3RubyPtr c = (sqlite3RubyPtr)ctx;
26-
sqlite3 *db = c->db;
26+
sqlite3 *db = c->db;
2727

2828
if (db) { sqlite3_close_v2(db); }
2929
xfree(c);
@@ -62,8 +62,6 @@ utf16_string_value_ptr(VALUE str)
6262
return RSTRING_PTR(str);
6363
}
6464

65-
static VALUE sqlite3_rb_close(VALUE self);
66-
6765
sqlite3RubyPtr
6866
sqlite3_database_unwrap(VALUE database)
6967
{
@@ -119,18 +117,16 @@ rb_sqlite3_disable_quirk_mode(VALUE self)
119117
#endif
120118
}
121119

122-
/* call-seq: db.close
123-
*
124-
* Closes this database.
125-
*/
126120
static VALUE
127-
sqlite3_rb_close(VALUE self)
121+
sqlite3_rb__close(VALUE self)
128122
{
129123
sqlite3RubyPtr ctx;
130124
TypedData_Get_Struct(self, sqlite3Ruby, &database_type, ctx);
131125

132-
CHECK(ctx->db, sqlite3_close_v2(ctx->db));
133-
ctx->db = NULL;
126+
if (ctx->db) {
127+
CHECK(ctx->db, sqlite3_close_v2(ctx->db));
128+
ctx->db = NULL;
129+
}
134130

135131
rb_iv_set(self, "-aggregators", Qnil);
136132

@@ -921,7 +917,7 @@ init_sqlite3_database(void)
921917
rb_define_private_method(cSqlite3Database, "open_v2", rb_sqlite3_open_v2, 3);
922918
rb_define_private_method(cSqlite3Database, "open16", rb_sqlite3_open16, 1);
923919
rb_define_method(cSqlite3Database, "collation", collation, 2);
924-
rb_define_method(cSqlite3Database, "close", sqlite3_rb_close, 0);
920+
rb_define_private_method(cSqlite3Database, "_close", sqlite3_rb__close, 0);
925921
rb_define_method(cSqlite3Database, "discard", sqlite3_rb_discard, 0);
926922
rb_define_method(cSqlite3Database, "closed?", closed_p, 0);
927923
rb_define_method(cSqlite3Database, "total_changes", total_changes, 0);

lib/sqlite3/database.rb

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,7 @@ def initialize file, options = {}, zvfs = nil
133133
@results_as_hash = options[:results_as_hash]
134134
@readonly = mode & Constants::Open::READONLY != 0
135135
@default_transaction_mode = options[:default_transaction_mode] || :deferred
136+
@owner_pid = Process.pid
136137

137138
if block_given?
138139
begin
@@ -143,6 +144,24 @@ def initialize file, options = {}, zvfs = nil
143144
end
144145
end
145146

147+
# Close the database and release all associated resources.
148+
#
149+
# ⚠ If the process that created the database forks a child process, and this method is called
150+
# from the child process, then this method will _not_ free memory resources and instead will
151+
# call discard. This is a memory leak, but is safer than risking database corruption.
152+
#
153+
# See adr/2024-09-fork-safety.md for more information on fork safety.
154+
def close
155+
if Process.pid != @owner_pid
156+
warn "An open sqlite database connection was inherited from a forked process and " \
157+
"is being discarded. This is a memory leak. If possible, please close all sqlite " \
158+
"database connections before forking.", uplevel: 1
159+
discard
160+
else
161+
_close
162+
end
163+
end
164+
146165
# call-seq: db.encoding
147166
#
148167
# Fetch the encoding set on this database

test/helper.rb

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,6 @@
11
require "sqlite3"
22
require "minitest/autorun"
33

4-
if ENV["GITHUB_ACTIONS"] == "true" || ENV["CI"]
5-
$VERBOSE = nil
6-
end
7-
84
puts "info: ruby version: #{RUBY_DESCRIPTION}"
95
puts "info: gem version: #{SQLite3::VERSION}"
106
puts "info: sqlite version: #{SQLite3::SQLITE_VERSION}/#{SQLite3::SQLITE_LOADED_VERSION}"

test/test_database.rb

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -740,6 +740,27 @@ def test_discard_a_connection
740740
end
741741
end
742742

743+
def test_close_in_a_new_process_calls_discard_and_warns
744+
db = SQLite3::Database.new("test.db")
745+
746+
assert_equal(Process.pid, db.instance_variable_get(:@owner_pid))
747+
748+
called = false
749+
db.define_singleton_method(:discard) do
750+
called = true
751+
end
752+
db.instance_variable_set(:@owner_pid, 1)
753+
754+
assert_output(nil, /warning: An open sqlite database connection was inherited from a forked process/) do
755+
db.close
756+
end
757+
assert(called)
758+
ensure
759+
db.instance_variable_set(:@owner_pid, Process.pid)
760+
db.close
761+
FileUtils.rm_f("test.db")
762+
end
763+
743764
def test_discard_a_closed_connection
744765
db = SQLite3::Database.new("test.db")
745766
db.close

0 commit comments

Comments
 (0)