Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[11.x] Add ability to configure SQLite busy_timeout, journal_mode, and synchronous pragmas #52052

Merged
merged 8 commits into from
Jul 10, 2024

Conversation

bakerkretzmar
Copy link
Contributor

@bakerkretzmar bakerkretzmar commented Jul 7, 2024

This PR adds optional busy_timeout, journal_mode, and synchronous configuration options to allow setting the corresponding pragmas on SQLite database connections. Together, these three configuration options can improve SQLite's performance enormously.

Background

This SQLite configuration:

PRAGMA busy_timeout = 5000;
PRAGMA journal_mode = WAL;
PRAGMA synchronous = NORMAL;

is becoming the widely recommended default for modern apps because it is orders of magnitude faster than the default setup and far less prone to SQLITE_BUSY errors.

  • journal_mode configures how transactions are implemented. The default setting, DELETE, uses a rollback journal. Since SQLite 3.7 a WAL mode is available that uses a write ahead log, which is significantly faster and allows concurrent database reads and writes in more scenarios.
  • busy_timeout configures how long SQLite will wait when a table is locked before returning a SQLITE_BUSY error. The default is 0, which means any attempted concurrent writes will fail immediately. Setting it to a positive integer will make SQLite wait that many milliseconds if the database is locked, which in most cases means that operations will complete with a negligible delay instead of erroring.
  • synchronous configures how often SQLite flushes the contents of the database to the disk. The default, FULL, writes data to disk more or less immediately when it changes. The NORMAL mode syncs data when necessary but less often, and in combination with WAL mode is much faster, fully consistent, and highly durable. NORMAL is SQLite's recommended setting when used with WAL mode.

Rails and Django both currently support some or all of this functionality. They both have a timeout configuration option, Rails 7.1 defaults to WAL mode and NORMAL, and Django 5.1 (releasing in August) is going to add configuration fields for setting transaction modes and other custom pragmas:

More info:

Implementation

I based this on the existing code for setting the foreign_keys pragma, it works exactly the same way except that the new options accept custom values instead of just being on or off.

busy_timeout and synchronous are per-connection settings and have to be set every time Laravel connects to a SQLite database (like the existing foreign_keys pragma), so it makes sense to store them in the config like foreign_keys. journal_mode is a persistent setting and technically only has to be set once, but the implementation is much simpler and more consistent if we just set it on every connection too.

The new configuration options all default to null so that existing apps will be completely unaffected by this change.

Alternatives

How I'm currently doing this:

// in AppServiceProvider.php

public function boot(): void
{
    Event::listen(function (ConnectionEstablished $event) {
        if ($event->connection instanceof SQLiteConnection) {
            $event->connection->statement(<<<SQL
                PRAGMA busy_timeout = 5000;
                PRAGMA journal_mode = WAL;
                PRAGMA synchronous = NORMAL;
                SQL);
        }
    });
}

Future Scope

I'm going to work on a PR for immediate transactions too, which also combine really well with these settings but will have to be implemented differently.

@bakerkretzmar bakerkretzmar changed the title [11.x] Add ability to configure SQLite busy_timeout [11.x] Add ability to configure SQLite busy_timeout, journal_mode, and synchronous pragmas Jul 7, 2024
@osbre
Copy link
Contributor

osbre commented Jul 8, 2024

Excuse my ignorance, but aren't we supposed to set journal_mode once for the database file rather than configuring it on every connection? Doesn't SQLite persist it?

@bakerkretzmar
Copy link
Contributor Author

bakerkretzmar commented Jul 8, 2024

Excuse my ignorance, but aren't we supposed to set journal_mode once for the database file rather than configuring it on every connection? Doesn't SQLite persist it?

Yeah good question, I addressed that in my notes above. We can just set it once but I don't know if we're necessarily "supposed to." I'm assuming that the overhead of setting it to the same value repeatedly is completely negligible but I can do some more research, I haven't seen any mention of that being an issue and it's what Rails currently does. I figured keeping the code here simple and consistent was more important.

@bakerkretzmar
Copy link
Contributor Author

And actually it's only wal and delete modes that persist, none of the others, so technically if you're setting it to anything else (highly unlikely but possible) it is required for every new connection.

@taylorotwell taylorotwell merged commit 217789d into laravel:11.x Jul 10, 2024
28 checks passed
@bakerkretzmar bakerkretzmar deleted the sqlite-busy-timeout branch July 10, 2024 21:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants