Skip to content

Commit e13f712

Browse files
authored
Adds Sqlite3.release/2 for prepared statements (#155)
* Adds exqlite_release * Manually call release on the prepared statements The garbage collector will come back through later and release the opaque nif resource. This will at least reclaim a large portion of the memory taken up by the prepared statement. * Update documentation and changelog * Adds tests for Sqlite3.release/2
1 parent c4be3d6 commit e13f712

File tree

8 files changed

+111
-6
lines changed

8 files changed

+111
-6
lines changed

CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
# Changelog
22

33
## [Unreleased](unreleased)
4-
4+
### Fixed
5+
- Fixed perceived memory leak for prepared statements not being cleaned up in a timely manner. This would be an issue for systems under a heavy load. [#155](https://github.com/elixir-sqlite/exqlite/pull/155)
56

67
## [0.6.2] - 2021-08-25
78
### Changed

README.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,15 @@ The `Exqlite.Sqlite3` module usage is fairly straight forward.
7474

7575
# No more results
7676
:done = Exqlite.Sqlite3.step(conn, statement)
77+
78+
# Release the statement.
79+
#
80+
# It is recommended you release the statement after using it to reclaim the memory
81+
# asap, instead of letting the garbage collector eventually releasing the statement.
82+
#
83+
# If you are operating at a high load issuing thousands of statements, it would be
84+
# possible to run out of memory or cause a lot of pressure on memory.
85+
:ok = Exqlite.Sqlite3.release(conn, statement)
7786
```
7887

7988

c_src/sqlite3_nif.c

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -712,6 +712,34 @@ exqlite_deserialize(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[])
712712
return make_atom(env, "ok");
713713
}
714714

715+
static ERL_NIF_TERM
716+
exqlite_release(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[])
717+
{
718+
assert(env);
719+
720+
statement_t* statement = NULL;
721+
connection_t* conn = NULL;
722+
723+
if (argc != 2) {
724+
return enif_make_badarg(env);
725+
}
726+
727+
if (!enif_get_resource(env, argv[0], connection_type, (void**)&conn)) {
728+
return make_error_tuple(env, "invalid_connection");
729+
}
730+
731+
if (!enif_get_resource(env, argv[1], statement_type, (void**)&statement)) {
732+
return make_error_tuple(env, "invalid_statement");
733+
}
734+
735+
if (statement->statement) {
736+
sqlite3_finalize(statement->statement);
737+
statement->statement = NULL;
738+
}
739+
740+
return make_atom(env, "ok");
741+
}
742+
715743
static void
716744
connection_type_destructor(ErlNifEnv* env, void* arg)
717745
{
@@ -788,6 +816,7 @@ static ErlNifFunc nif_funcs[] = {
788816
{"transaction_status", 1, exqlite_transaction_status, ERL_NIF_DIRTY_JOB_IO_BOUND},
789817
{"serialize", 2, exqlite_serialize, ERL_NIF_DIRTY_JOB_IO_BOUND},
790818
{"deserialize", 3, exqlite_deserialize, ERL_NIF_DIRTY_JOB_IO_BOUND},
819+
{"release", 2, exqlite_release, ERL_NIF_DIRTY_JOB_IO_BOUND},
791820
};
792821

793822
ERL_NIF_INIT(Elixir.Exqlite.Sqlite3NIF, nif_funcs, on_load, NULL, NULL, NULL)

lib/exqlite/connection.ex

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -259,7 +259,8 @@ defmodule Exqlite.Connection do
259259
This callback is called in the client process.
260260
"""
261261
@impl true
262-
def handle_close(_query, _opts, state) do
262+
def handle_close(query, _opts, state) do
263+
Sqlite3.release(state.db, query.ref)
263264
{:ok, nil, state}
264265
end
265266

@@ -274,10 +275,8 @@ defmodule Exqlite.Connection do
274275
end
275276

276277
@impl true
277-
def handle_deallocate(%Query{} = _query, _cursor, _opts, state) do
278-
# We actually don't need to do anything about the cursor. Since it is a
279-
# prepared statement, it will be garbage collected by erlang when it loses
280-
# references.
278+
def handle_deallocate(%Query{} = query, _cursor, _opts, state) do
279+
Sqlite3.release(state.db, query.ref)
281280
{:ok, nil, state}
282281
end
283282

lib/exqlite/sqlite3.ex

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,24 @@ defmodule Exqlite.Sqlite3 do
166166
Sqlite3NIF.deserialize(conn, String.to_charlist(database), serialized)
167167
end
168168

169+
def release(_conn, nil), do: :ok
170+
171+
@doc """
172+
Once finished with the prepared statement, call this to release the underlying
173+
resources.
174+
175+
This should be called whenever you are done operating with the prepared statement. If
176+
the system has a high load the garbage collector may not clean up the prepared
177+
statements in a timely manner and causing higher than normal levels of memory
178+
pressure.
179+
180+
If you are operating on limited memory capacity systems, definitely call this.
181+
"""
182+
@spec release(db(), statement()) :: :ok | {:error, reason()}
183+
def release(conn, statement) do
184+
Sqlite3NIF.release(conn, statement)
185+
end
186+
169187
defp convert(%Date{} = val), do: Date.to_iso8601(val)
170188
defp convert(%Time{} = val), do: Time.to_iso8601(val)
171189
defp convert(%NaiveDateTime{} = val), do: NaiveDateTime.to_iso8601(val)

lib/exqlite/sqlite3_nif.ex

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,5 +56,8 @@ defmodule Exqlite.Sqlite3NIF do
5656
@spec deserialize(db(), String.Chars.t(), binary()) :: :ok | {:error, reason()}
5757
def deserialize(_conn, _database, _serialized), do: :erlang.nif_error(:not_loaded)
5858

59+
@spec release(db(), statement()) :: :ok | {:error, reason()}
60+
def release(_conn, _statement), do: :erlang.nif_error(:not_loaded)
61+
5962
# TODO: add statement inspection tooling https://sqlite.org/c3ref/expanded_sql.html
6063
end

test/exqlite/connection_test.exs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,4 +188,22 @@ defmodule Exqlite.ConnectionTest do
188188
assert {:ok, conn} == Connection.ping(conn)
189189
end
190190
end
191+
192+
describe ".handle_close/3" do
193+
test "releases the underlying prepared statement" do
194+
{:ok, conn} = Connection.connect(database: :memory)
195+
196+
{:ok, query, _result, conn} =
197+
%Query{statement: "create table users (id integer primary key, name text)"}
198+
|> Connection.handle_execute([], [], conn)
199+
200+
assert {:ok, nil, conn} == Connection.handle_close(query, [], conn)
201+
202+
{:ok, query, conn} =
203+
%Query{statement: "select * from users where id < ?"}
204+
|> Connection.handle_prepare([], conn)
205+
206+
assert {:ok, nil, conn} == Connection.handle_close(query, [], conn)
207+
end
208+
end
191209
end

test/exqlite/sqlite3_test.exs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,34 @@ defmodule Exqlite.Sqlite3Test do
9696
end
9797
end
9898

99+
describe ".release/2" do
100+
test "double releasing a statement" do
101+
{:ok, conn} = Sqlite3.open(":memory:")
102+
103+
:ok =
104+
Sqlite3.execute(conn, "create table test (id integer primary key, stuff text)")
105+
106+
{:ok, statement} = Sqlite3.prepare(conn, "insert into test (stuff) values (?1)")
107+
:ok = Sqlite3.release(conn, statement)
108+
:ok = Sqlite3.release(conn, statement)
109+
end
110+
111+
test "releasing a statement" do
112+
{:ok, conn} = Sqlite3.open(":memory:")
113+
114+
:ok =
115+
Sqlite3.execute(conn, "create table test (id integer primary key, stuff text)")
116+
117+
{:ok, statement} = Sqlite3.prepare(conn, "insert into test (stuff) values (?1)")
118+
:ok = Sqlite3.release(conn, statement)
119+
end
120+
121+
test "releasing a nil statement" do
122+
{:ok, conn} = Sqlite3.open(":memory:")
123+
:ok = Sqlite3.release(conn, nil)
124+
end
125+
end
126+
99127
describe ".bind/3" do
100128
test "binding values to a valid sql statement" do
101129
{:ok, conn} = Sqlite3.open(":memory:")

0 commit comments

Comments
 (0)