Skip to content

Commit 4b97a95

Browse files
authored
Fix segfault when database connections timeout (#218)
1 parent cdaabee commit 4b97a95

File tree

4 files changed

+68
-2
lines changed

4 files changed

+68
-2
lines changed

.github/workflows/ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ jobs:
4949
matrix:
5050
os: ["ubuntu-20.04", "windows-2019"]
5151
elixir: ["1.14", "1.13", "1.12", "1.11"]
52-
otp: ["25", 24", "23"]
52+
otp: ["25", "24", "23"]
5353
exclude:
5454
- otp: "25"
5555
os: "windows-2019"

c_src/sqlite3_nif.c

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ static sqlite3_mem_methods default_alloc_methods = {0};
2222
typedef struct connection
2323
{
2424
sqlite3* db;
25+
ErlNifMutex* mutex;
2526
} connection_t;
2627

2728
typedef struct statement
@@ -219,6 +220,12 @@ exqlite_open(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[])
219220
return make_error_tuple(env, "database_open_failed");
220221
}
221222

223+
conn->mutex = enif_mutex_create("exqlite:connection");
224+
if (conn->mutex == NULL) {
225+
enif_release_resource(conn);
226+
return make_error_tuple(env, "failed_to_create_mutex");
227+
}
228+
222229
sqlite3_busy_timeout(conn->db, 2000);
223230

224231
result = enif_make_resource(env, conn);
@@ -256,17 +263,24 @@ exqlite_close(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[])
256263
}
257264
}
258265

266+
// close connection in critical section to avoid race-condition
267+
// cases. Cases such as query timeout and connection pooling
268+
// attempting to close the connection
269+
enif_mutex_lock(conn->mutex);
270+
259271
// note: _v2 may not fully close the connection, hence why we check if
260272
// any transaction is open above, to make sure other connections aren't blocked.
261273
// v1 is guaranteed to close or error, but will return error if any
262274
// unfinalized statements, which we likely have, as we rely on the destructors
263275
// to later run to clean those up
264276
rc = sqlite3_close_v2(conn->db);
265277
if (rc != SQLITE_OK) {
278+
enif_mutex_unlock(conn->mutex);
266279
return make_sqlite3_error_tuple(env, rc, conn->db);
267280
}
268281

269282
conn->db = NULL;
283+
enif_mutex_unlock(conn->mutex);
270284

271285
return make_atom(env, "ok");
272286
}
@@ -357,11 +371,21 @@ exqlite_prepare(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[])
357371
if (!statement) {
358372
return make_error_tuple(env, "out_of_memory");
359373
}
374+
statement->statement = NULL;
360375

361376
enif_keep_resource(conn);
362377
statement->conn = conn;
363378

379+
// ensure connection is not getting closed by parallel thread
380+
enif_mutex_lock(conn->mutex);
381+
if (conn->db == NULL) {
382+
enif_mutex_unlock(conn->mutex);
383+
enif_release_resource(statement);
384+
return make_error_tuple(env, "connection closed");
385+
}
364386
rc = sqlite3_prepare_v3(conn->db, (char*)bin.data, bin.size, 0, &statement->statement, NULL);
387+
enif_mutex_unlock(conn->mutex);
388+
365389
if (rc != SQLITE_OK) {
366390
enif_release_resource(statement);
367391
return make_sqlite3_error_tuple(env, rc, conn->db);
@@ -857,6 +881,7 @@ connection_type_destructor(ErlNifEnv* env, void* arg)
857881
if (conn->db) {
858882
sqlite3_close_v2(conn->db);
859883
conn->db = NULL;
884+
enif_mutex_destroy(conn->mutex);
860885
}
861886
}
862887

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
defmodule Exqlite.TimeoutSegfaultTest do
2+
use ExUnit.Case
3+
4+
@moduletag :slow_test
5+
6+
setup do
7+
{:ok, path} = Temp.path()
8+
on_exit(fn -> File.rm(path) end)
9+
10+
%{path: path}
11+
end
12+
13+
test "segfault", %{path: path} do
14+
{:ok, conn} =
15+
DBConnection.start_link(Exqlite.Connection,
16+
busy_timeout: 50_000,
17+
pool_size: 50,
18+
timeout: 1,
19+
database: path,
20+
journal_mode: :wal
21+
)
22+
23+
query = %Exqlite.Query{statement: "create table foo(id integer, val integer)"}
24+
{:ok, _, _} = DBConnection.execute(conn, query, [])
25+
26+
values = for i <- 1..1000, do: "(#{i}, #{i})"
27+
statement = "insert into foo(id, val) values #{Enum.join(values, ",")}"
28+
insert_query = %Exqlite.Query{statement: statement}
29+
30+
1..5000
31+
|> Task.async_stream(fn _ ->
32+
try do
33+
DBConnection.execute(conn, insert_query, [], timeout: 1)
34+
catch
35+
kind, reason ->
36+
IO.puts("Error: #{inspect(kind)} reason: #{inspect(reason)}")
37+
end
38+
end)
39+
|> Stream.run()
40+
end
41+
end

test/test_helper.exs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
ExUnit.start(capture_log: true)
1+
ExUnit.start(capture_log: true, timeout: 120_000, exclude: [:slow_test])

0 commit comments

Comments
 (0)