-
Notifications
You must be signed in to change notification settings - Fork 7.9k
Implement GH-8967: Add PDO_SQLITE_ATTR_TRANSACTION_MODE #19317
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
Conversation
@kocsismate @SakiTakamachi Will this make it into 8.5 if merged before the feature freeze? |
@stancl perhaps this is worth posting on the PHP Internals mailing list? I would very much like for this feature to land in the next release, and I think this is a marked improvement to PDO. |
This comment was marked as outdated.
This comment was marked as outdated.
Tangentially related to this, I've opened a PR for Laravel to work around this, if you'd like to chime in @stancl . |
What is the problem with using: $this->getPdo()->exec("BEGIN IMMEDIATE TRANSACTION"); |
Most frameworks use |
I’ve reviewed the PRs along with nearly all of your comments and discussions and wanted to share a few thoughts: Point 1I don’t think we can get anything simpler or clearer than: $this->getPdo()->exec("BEGIN IMMEDIATE TRANSACTION"); Compared to what this PR suggests: $pdo->setAttribute($pdo::ATTR_TRANSACTION_MODE, $pdo::ATTR_TRANSACTION_MODE_IMMEDIATE); Point 2Laravel already has a dedicated class for SQLite Point 3If IMMEDIATE should be the default mode for SQLite, then this seems more like a SQLite-level concern rather than something to solve by introducing new PDO attributes. ConclusionThe existing Laravel PR already covers this behavior in a cleaner and more appropriate way, so this PR seems unnecessary and can be closed. |
Sorry but you're completely missing the point here to be honest. |
Can you clarify how the changes introduced in the Laravel PR would look if this PR is merged? |
This is a PR to php-src. You're hyperfocused on Laravel and don't seem to know how PDO attributes are used. Please keep the discussion on topic. To answer your points briefly:
This PR allows anyone, using any framework, to change the SQLite transaction mode. It is a matter of adding a single line to a config file in any framework. The alternative — your suggestion — is that every PHP framework on earth should have to make the change Laravel made in the PR linked above. That change is a hack which shouldn't be needed. There's no reason why frameworks should be using |
@SakiTakamachi Can you have a look? Also @php/release-managers-85, since OP asked whether this may still target 8.5. https://externals.io/message/128446 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not a PDO expert and not an SQLite expert either. This seems to be mostly right, I didn't see any obvious conceptual problems.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM, however... 10 years ago I would have probably pushed something similar to pdo_pgsql without worries (before feature freeze)... but now I'd be much more reluctant without a proper RFC process.
Then again, it's up to the RMs
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Some optional suggestions, but LGTM.
RM wise: have too little expertise to judge the impact but I'm leaning towards being ok with having this in 8.5. But I'd be grateful for your take @SakiTakamachi |
I'll look it tomorrow :) |
f8baad4
to
b8b5283
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks goot to me, thanks!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
RM approval, technical review not performed
This commit implements phpGH-8967. SQLite supports multiple transaction modes. These include: - DEFERRED (default) only acquires a lock when you start a read/write - IMMEDIATE acquires a reserved lock - EXCLUSIVE acquires an exclusive lock (stricter than immediate) In WAL mode IMMEDIATE and EXCLUSIVE are identical. One reason for wanting to specify a transaction mode is that SQLite doesn't respect busy_timeout when a DEFERRED transaction tries to upgrade a read lock to a write lock. Normally if you try to acquire a lock and have busy_timeout configured, SQLite will wait for that period until giving up and erroring out (SQLITE_BUSY). With DEFERRED, if you have a transaction that first reads and there's a concurrent writer while it's trying to upgrade to a write lock, you will immediately get SQLITE_BUSY regardless of your busy_timeout. Prior to this commit, the only available workarounds were: - Using $pdo->exec("BEGIN IMMEDIATE TRANSACTION") instead of $pdo->beginTransaction() - Doing a dummy write at the start of each transaction so you don't get stuck with a read lock Both of those aren't very usable, especially in a framework context where the user doesn't have complete control over how transactions are started. To address that, this commit adds four class constants to Pdo\Sqlite: - ATTR_TRANSACTION_MODE -- a new attribute - TRANSACTION_MODE_DEFERRED = 0 - TRANSACTION_MODE_IMMEDIATE = 1 - TRANSACTION_MODE_EXCLUSIVE = 2 These can be used as: $pdo->setAttribute( $pdo::ATTR_TRANSACTION_MODE, $pdo::TRANSACTION_MODE_IMMEDIATE );
b8b5283
to
585ab46
Compare
Given the approvals, I've merged master and added NEWS/UPGRADING. Will merge when CI passes. |
@TimWolla Do you want me to squash and rebase (in place of the master merge) again so the commit is merged with the original message? Or you can just copy it when squash-merging I guess. |
I'll handle the rest during merging, thank you! |
Thanks a lot! |
Resolves #8967
First time contributing to this repo, so please review carefully. Especially the use of zvals and the placement of new constants.
This PR lets the user select which transaction mode
Pdo\Sqlite
should be using. When a transaction mode is not specified, SQLite defaults to DEFERRED transactions which cause issues with locks in concurrent requests. See https://www.sqlite.org/lang_transaction.htmlWhen you start a deferred transaction, the lock is only acquired when you actually interact with the database. If you have a transaction that first reads and only then tries to write, it will start by acquiring a shared lock and only when you try to write does it try to upgrade to an exclusive lock. However, this logic of upgrading locks doesn't really respect the
busy_timeout
pragma, so it just immediately fails withSQLITE_BUSY
.On the other hand, when you start an immediate transaction, you don't have to worry about getting
SQLITE_BUSY
unless another transaction holds a lock for longer than thebusy_timeout
. So the new transaction just waits for a bit rather than immediately failing. This is the behavior you want in production.If you look at the linked issue, I include a simple reproduction using
pcntl_fork()
that uses two concurrent processes. If you run the reproduction just asphp test.php
, one of the transactions fails withSQLITE_BUSY
. If you run it asphp test.php immediate
, the code does not use$pdo->beginTransaction()
and instead uses$pdo->exec('begin immediate transaction')
. With that, the second transaction simply waits for the first one to release the lock.The problem with starting transactions using a custom statement you
exec()
is that this is simply not an option in many cases, frameworks prefer calling$pdo->beginTransaction()
. I've had to solve this in an application of mine by creating a wrapper around the framework's transaction function that first writes to a dummy table and only then executes the callback. That way the transaction always starts with a write lock and doesn't run into the issue of trying to promote a lock. But this doesn't work in every scenario, for instance when the framework's own logic heavily uses transactions that you have no way of customizing with your own wrappers. Not like the wrappers are a proper solution anyway.PDO attributes are a perfect fit for this because you can simply set the attribute and no other logic has to change. Most frameworks let you set these in a config file as well.
The only thing I'm not sure about is how to structure these attributes. The transaction mode has 3 possible values. We need to represent those somehow. We could take the transaction mode string but then we'd need to deal with managing the lifetime of that string allocation. We could use boolean attributes like
ATTR_TRANSACTION_MODE_DEFERRED => true
but then we'd need to handle invalid states like two of these attributes being set at once. For that reason I went with one attribute as the "key" and 3 attributes as the "values". These are just class constants onPdo\Sqlite
.I've included very basic tests (in the
pdo_sqlite/tests/subclasses
directory which I think is correct since this interacts withPdo\Sqlite
). They test the attribute userland API and then thatbeginTransaction()
actually respects the transaction mode using a simplified reproduction of the lock contention scenario in my original comment. It uses named in-memory DBs (that way separately created PDO instances can use the same memory region) and infers the transaction mode used in$pdo
based on the error message$pdo2
gets (or doesn't get) when it tries to start an immediate transaction.