warmwaffles

warmwaffles

Need help with Exqlite segfaulting when timing out

Author of the exqlite library here. I need some help from experienced NIF devs in hunting down the culprit of this SIGSEGV error. This bug has been keeping me awake at night for a few weeks now.

Ya ya ya, it’s C, and I should be using Rust, but right now, I don’t want to make that change just yet for this library. Let’s ignore that for now.

What I am running into is that when I have an sqlite database, and a pool of DBConnections to a single sqlite database resource on disk. SQLite has a write ahead log functionality that works pretty well in most of my use cases that I’ve been hammering it with.

Except for an issue where database connections timeout. In order to simulate the timeouts in a reproduce-able manner, we shortened the timeout window to 1 second, write 1,000 new rows to a single table and queue up 5,000 async tasks to execute the database query.

Failing test implemented here:

Can be ran by checking that branch out and running the following

mix test test/exqlite/timeout_segfault_test.exs

Marked As Solved

akash-akya

akash-akya

Hypothesis

The issue seems to be that there is a race condition between exqlite_close and exqlite_prepare. The issue happens when we execute sqlite3_prepare_v3 on a connection and in-parallel we close the connection with sqlite3_close_v2.

sqlite3_close_v2 does handle previously prepared statements, but here we are preparing the statement in-parallel while we are closing the connection. Which is causing occasional SQLITE_MISUSE.

How can we reach this state?

Given

  • disconnect callback is executed by connection process
  • handle_prepare and other callbacks are executed in the client process
  • a NIF call can not be interrupted/cancelled once started, irrespective of client side process timeout, or process kill etc.

Scenario

  1. client checkout a connection and makes a query
  2. client query function call invocation makes exqlite_prepare NIF call but fail to complete within connection timeout duration. It is 1ms in the test case we are using. This NIF call is still running or scheduled to run even after the timeout.
  3. due to connection timeout, connection-process receives timeout and initiate connection close (disconnect callback)
  4. connection-process calls exqlite_close NIF

2 & 4 are run in-parallel, leading to race-condition mentioned previously, causing segfault.

Fix

I tried to fix the race-condition using mutex, and sofar haven’t seen a single failure.

This is PoC, I guess we should handle multiple other cases as well, not just the one between close & prepare if this is the actual issue.

Also Liked

jstimps

jstimps

I’ve debugged my share of C NIFs in the past, so I hope I can provide some help. I pulled your branch and reproduced the segfault. Locally I added some fprintf-based logging to exqlite_close and noticed something that made my raise an eyebrow, so I thought I’d share. If I’m off base here, please let me know.

An fprintf statement in exqlite_close prevents the segfault on my system, so there is definitely a timing element here (race condition). I printed the memory addresses for conn and conn->db and saw many duplicate addresses, which made me wonder if exqlite_close is being called multiple times on the same resource, resulting in a double-free-like segfault when running with high concurrency.

BTW, thanks for this library. I am a fan. :slight_smile:

jstimps

jstimps

I’m not familiar with DBConnection, but I’ve written similar libraries in the past. When you do DBConnection.start_link, is it calling your connect one time and then sharing the result among the pool of 50? If that’s the case, I wonder if creating a single enif resource and allowing it to be “disconnected” from multiple concurrent processes in the pool is the source of the race condition. The DBConnection README says it calls disconnect automatically:

DBConnection also takes all of the care necessary to handle
failures, and it shuts down the connection and the socket
whenever the client does not check in the connection to avoid
recycling sockets/connections in a corrupted state (such as a socket
that is stuck inside a transaction).

(It looks like DBConnection was written with network sockets in mind. Due to the nature of SQLite, obviously there is no network socket, so the disconnect behavior may not be optimal for your use case.)

In my experience, it’s safest to ensure that an enif resource gets a dedicated BEAM process so that you can carefully control the access to it. At the very least, I assume you only want a maximum of 1 exqlite_close on a given resource; You can build that guarantee by serializing calls to Connection.close on a dedicated BEAM process. Otherwise, you can’t guarantee there are not 2 scheduler threads freeing the memory simultaneously.

We get spoiled by the BEAM scheduler and garbage collection, but when it comes to NIFs sadly we have to worry about all these issues again. I don’t believe the BEAM garbage collector is causing any problems in this case.

Edit: in my haste I probably jumped to some wrong conclusions about DBConnection. I’m going to spend some more time and try to get more concrete results.

warmwaffles

warmwaffles

Not necessarily, but I tied this pretty closely to being used by ecto_sqlite3.

I could use poolboy or nimble pool for pooled resources, but when it comes to Ecto, you have to implement the DBConnection interface in order to have it play nice with ecto_sql.

Where Next?

Popular in Questions Top

shahryarjb
Hello, I get Persian date from my client and convert it to normal calendar like this: def jalali_string_to_miladi_english_number(persi...
New
fireproofsocks
I’m working on defining a simple Ecto schema for a table (in PostGres), but I don’t see where I can define a column as NOT NULL. Conside...
New
stefanchrobot
What’s the safe way to decode a JSON string into a struct? I want to avoid calling String.to_atom. Jason.decode can give me a map with st...
New
Phillipp
Hey, I have a NanoPi-M3 and try to install Elixir on their Ubuntu image. I followed the Raspberry Pi installation instructions from the ...
New
ashish173
I am using Ecto timestamps with postgres, I can see the timestamps() use the :naive_dateime but for my use case I wanted to store the ti...
New
gonzofish
I’m currently trying to understand how to join three tables using Ecto. All the examples I’ve seen use 2, so maybe I’m just missing somet...
New
fireproofsocks
Forgive me if this is obvious, but how does one delete a database record WITHOUT selecting it first? https://hexdocs.pm/ecto/Ecto.Repo.h...
New
johnnyicon
Hi all, I've just started learning Elixir and Phoenix Framework, so please pardon my n00bness at this stage. I'm trying to use Postg...
New
baxterw3b
Hi guys, i’m new in the Elixir world, and i have to say, that i love it! i’m having some problem to understand anonymous functions with ...
New
jc00ke
Expanding on this topic: https://forum.elixirforum.net/t/map-typespec-question/19217 Let’s say I have a map with required and optional k...
New

Other popular topics Top

Qqwy
Update: How to use the Blogs & Podcasts section You can post links to your blog posts or podcasts either in one of the Official Blog...
3268 119930 1237
New
senggen
Erlang/OTP 25 [erts-13.2.2] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1] 15:22:35.803 [error] gen_event {lager_file_backend...
New
vonH
In asking this question I am more interested about the expressiveness of the language itself and less concerned about the availability of...
New
lessless
I believe there are people here who are dealing with CSV files import on the daily basis, and since Excel is a really popular tool there ...
New
AstonJ
You’re a programmer, so you don’t need spoon feeding with the conventional drivel about “this is an integer.” No. You need to know what’s...
New
_russellb
I want to try my hand at web scraping. What tools/libraries do I need to use. I’m hoping to turn this into something professional so don’...
New
New
script
If I have a string “1000 cfu/ml” . I want to remove the characters and / and space . So the string is like this "1000" What is the ...
New
stefanchrobot
What’s the safe way to decode a JSON string into a struct? I want to avoid calling String.to_atom. Jason.decode can give me a map with st...
New
baxterw3b
Hi guys, i’m new in the Elixir world, and i have to say, that i love it! i’m having some problem to understand anonymous functions with ...
New

We're in Beta

About us Mission Statement