SQLite Forum

Does the design of deferred transactions lead to deadlocks?
Login

Does the design of deferred transactions lead to deadlocks?

(1) By anonymous on 2022-10-05 03:14:49 [source]

Hi. I'd like to ask if I understand the design of deferred transactions correctly. Having read https://www.sqlite.org/lang_transaction.html and https://www.sqlite.org/lockingv3.html, here's what I think is happening...

Suppose you have process 1 doing (this is pseudocode so it's omitting other arguments that would normally be passed to sqlite3_exec()):
sqlite3_exec("BEGIN TRANSACTION"); // DEFERRED is the default.
sqlite3_exec("SELECT ...");
some_non_sqlite_code();
sqlite3_exec("UPDATE ...");
sqlite3_exec("COMMIT");

And then you have process 2 doing:
sqlite3_exec("INSERT ...");

Now, during the execution of some_non_sqlite_code(), process 1 holds a read (SHARED) lock on the database. If during this time, process 2 runs the above INSERT command, it gets a write (RESERVED followed by PENDING) lock, at which point it needs to wait for process 1 to clear its SHARED lock. But, when process 1 calls the UPDATE statement, it can't upgrade its SHARED lock to a RESERVED lock until process 2 clears its PENDING lock. So at this point, each process is blocked on the other one until one of them times out with a SQLITE_BUSY.

Is this the correct understanding of DEFERRED transaction behavior? And if so, does this basically mean that application code shouldn't use deferred transactions for transactions that involve a read operation followed by application code followed by a write operation?

Thank you for any insights and/or corrections you can offer.

(2) By Igor Tandetnik (itandetnik) on 2022-10-05 04:30:58 in reply to 1 [link] [source]

Yes, the deadlock is possible. Exactly this scenario is described in the documentation. BEGIN IMMEDIATE was invented in large part to prevent this situation, if I recall correctly. Consider using it for a transaction that starts as a reader but expects to upgrade to a writer later.

(4) By KIT.james (kjames3411) on 2022-10-05 14:26:26 in reply to 2 [link] [source]

I have been bitten by this one many times it seems. What is the point of DEFERRED then? Are there cases when busy handlers are not called (and dead lock continues indefinitely?)

If DEFERRED causes dead locks so easily, don't you think it should be written in the docs?

Thank you for your answers.

(5) By David Raymond (dvdraymond) on 2022-10-05 14:47:13 in reply to 4 [link] [source]

The point of DEFERRED and it being the default is that it's all any transaction needs when it's just reading data and doesn't need to write anything. It's only when you try to upgrade it to a writing transaction that any deadlocks happen.

Other potential use: if you start a DEFERRED transaction with a query to find the IDs to delete, and it turns out there aren't any to delete, then your transaction never has to upgrade the lock and block others.

But at least you have the option to specify IMMEDIATE to call dibbs on being the next writer right then and there.

(8) By KIT.james (kjames3411) on 2022-10-06 12:35:59 in reply to 5 [link] [source]

Does upgrading a DEFERRED to an IMMEDIATE prevent deadlocks in all cases?

(9) By Gunter Hick (gunter_hick) on 2022-10-06 13:33:42 in reply to 8 [link] [source]

You cannot "upgrade" from DEFERRED to IMMEDIATE. You need to start the transaction either DEFERRED or IMMEDIATE (or EXCLUSIVE).

If you have only one database attached, then using IMMEDIATE transactions will prevent deadlocks by returning SQLITE_BUSY to the thread attempting to BEGIN IMMEDIATE while another write transaction is already underway.

I am not sure how SQLite handles more than one attached database where the order of ATTACH statements differs between connections. E.g. you have a connection #1 with database file A as "main" and B as "aux" and a connection #2 witha database file B as "main" and A as "aux". If both connections issue a BEGIN IMMEDIATE "at the same time", one might end up holding a lock on A and the other one on B with none backing down. So it would be prudent to ensure that databases are always attached in the same order.

(10) By ddevienne on 2022-10-06 14:06:41 in reply to 9 [link] [source]

ensure that databases are always attached in the same order

Or that SQLite uses an arbitrary deterministic order, independent from the attach order. But it's probably too late for that?

Kinda like https://en.cppreference.com/w/cpp/thread/lock does to lock several mutexes.

(12) By KIT.james (kjames3411) on 2022-10-07 12:07:22 in reply to 10 [link] [source]

Thank you all, really interested to the answer to those questions.

Although I think I will do my best to avoid multiple ATTACH in the future from now.

(6) By Gunter Hick (gunter_hick) on 2022-10-05 15:19:17 in reply to 4 [link] [source]

DEFERRED is quite useful if you are running in JOURNAL mode, there is one writer and that writer does not know beforehand if any updates are required. That way, all the reader processes/threads may continue on their merry way, pausing only when the writer does actually have something to write.

Running in the newer WAL mode removes the interference between readers and the single writer, so DEFERRED is no longer necessary.

Running more than one writer requires planning to avoid deadlocks. Using IMMEDIATE being one of the recommendations.

(3) By Richard Damon (RichardDamon) on 2022-10-05 11:30:25 in reply to 1 [link] [source]

SQLite detects this deadlock and the process that is trying to update its lock gets an IMMEDIATE SQLITE_BUSY as SQLite knows that its wait can't succeed without someone aborting their action.

(7) By anonymous on 2022-10-05 17:39:21 in reply to 1 [link] [source]

Thank you, everyone, for your answers and links to the relevant documentation!

Given these answers, may I suggest adding something like the following paragraph to the "DEFERRED, IMMEDIATE, and EXCLUSIVE transactions" section of https://www.sqlite.org/lang_transaction.html?

Lock contention that cannot be resolved via a busy timeout of any length can occur if a DEFERRED transaction starts with a read statement and later executes a write statement, because another process might be executing a write statement in between these two, in which case the first process has a read lock that can't be upgraded to a write lock until the second process completes its write, but that process can't complete its write until the first process releases its read lock. In this case, SQLite detects this condition and immediately returns SQLITE_BUSY to the first process, but applications that don't have a graceful way to handle a SQLITE_BUSY for such cases should use BEGIN IMMEDIATE for transactions that start with a read and later execute a write.

(11) By Rowan Worth (sqweek) on 2022-10-07 03:11:54 in reply to 7 [link] [source]

Is that not already covered by the paragraph in section 2.1?

If a write statement occurs while a read transaction is active, then the read transaction is upgraded to a write transaction if possible. If some other database connection has already modified the database or is already in the process of modifying the database, then upgrading to a write transaction is not possible and the write statement will fail with SQLITE_BUSY.

It could perhaps be clarified that the busy handler/busy timeout is not involved in this scenario, since this process will never be able to proceed until it relinquishes its read lock to allow the writing process to finish its turn.

(13) By anonymous on 2022-10-07 12:11:16 in reply to 11 [link] [source]

This raises a question: how is the calling code supposed to distinguish a SQLITE_BUSY that can be dealt with retries, from one that can't be dealt with in that way?

Should a different return value be provided, to let the caller know which is which?

(14) By anonymous on 2022-10-07 13:03:12 in reply to 13 [link] [source]

The calling code is supposed to remember whether or not it started a deferred transaction.

(15) By Keith Medcalf (kmedcalf) on 2022-10-07 14:35:50 in reply to 13 [link] [source]

The calling code has no idea. However the programmer who wrote the code ought to know what it is doing.

(16.1) By Rowan Worth (sqweek) on 2022-10-07 16:30:29 edited from 16.0 in reply to 13 [link] [source]

I don't think it's a bad idea, although at this point it would have to be implemented as an extended error code to avoid backwards compatibility issues.

Although I think most use cases are satisfied by setting the busy timeout to an appropriate value and letting it handle the retries. That way the application only sees SQLITE_BUSY when (a) it is impossible to proceed or (b) retry attempts have exceeded the timeout. In what circumstances would you want to treat SQLITE_BUSY differently under this setup? If the answer is "keep retrying when it can proceed" then the solution is to increase the busy timeout. Eventually you presumably hit a point where the timeout becomes unpalatable and thus once SQLITE_BUSY is returned it is time to give up.

For more subtle logic an application can install its own busy handler callback instead of using a simple timeout, and so there is another perspective here which crowns the busy handler as the canonical API for announcing and handling "retriable" lock contention. From this perspective it may actually make the API more confusing to add an additional error code as this would provide a second (less-optimal) way to do the same thing.

(edit: the crucial subtlety here is that the busy handler is never invoked in the case of an SQLITE_BUSY which cannot be dealt with by retrying)

(18) By anonymous on 2022-10-17 16:28:37 in reply to 11 [link] [source]

It could perhaps be clarified that the busy handler/busy timeout is not involved in this scenario

Yes, this is what I was confused about until it got answered/clarified in this thread. When I initially read both sections 2.1 and 2.2, what I saw was:

[For BEGIN DEFERRED, subsequent] write statements will upgrade the transaction to a write transaction if possible, or return SQLITE_BUSY.

...

BEGIN IMMEDIATE might fail with SQLITE_BUSY if another write transaction is already active on another database connection.

So I thought that the only difference was whether you preferred to handle a SQLITE_BUSY during the BEGIN statement, or whether you preferred to handle it during the first write statement. I did not understand, until thinking it through a bunch more and posting this thread, that it's not just about when the SQLITE_BUSY is returned, but about whether it can get resolved via a busy handler/timeout.

(17) By Donal Fellows (dkfellows) on 2022-10-10 13:33:11 in reply to 1 [link] [source]

Is this the correct understanding of DEFERRED transaction behavior? And if so, does this basically mean that application code shouldn't use deferred transactions for transactions that involve a read operation followed by application code followed by a write operation?

It's a correct understanding, except that one of the transactions gets an error immediately as it is able to determine that there's no way that it will ever be able to proceed to write with the view it has of the DB at the start of the transaction it began.

It means that if you (the application programmer) have a transaction that starts out doing reads but then possibly needs to do a write, you should to use an immediate transaction for that anyway. Like that, you will wait for the troublesome lock to clear. Getting this wrong is a particularly insidious bug when you're doing the same upgrading transactions on multiple threads at once, as some threads will succeed and others will fail arbitrarily and it is hard to get good visibility of why this is so. Doing better than this would require a much more complex lock implementation inside SQLite (row level locking without a server process to mediate is going to be nasty to code up in a small space).

Alternatively, you can try restarting the transaction that failed (presumably in immediate mode so it waits until the other thread/process clears). Assuming you designed your code to be able to do that...