-
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
base: master
Are you sure you want to change the base?
Conversation
case 0: | ||
case PDO_SQLITE_ATTR_TRANSACTION_MODE_DEFERRED: | ||
begin_statement = "BEGIN DEFERRED TRANSACTION"; | ||
break; |
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.
The 0
case is when no transaction mode has been set, so we use the default (deferred).
Is it a safe assumption that driver_data
is always zero-initialized? In the code I could find, I saw that driver_data
is allocated with pecalloc()
.
ext/pdo_sqlite/sqlite_driver.c
Outdated
case PDO_SQLITE_ATTR_TRANSACTION_MODE: | ||
zend_long mode; | ||
if (H->transaction_mode == 0) { | ||
mode = PDO_SQLITE_ATTR_TRANSACTION_MODE_DEFERRED; | ||
} else { | ||
mode = H->transaction_mode; | ||
} | ||
ZVAL_LONG(return_value, mode); | ||
break; |
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.
Same as my previous review https://github.com/php/php-src/pull/19317/files#r2243162264, this depends on driver_data
being zero-initialized.
@kocsismate @SakiTakamachi Will this make it into 8.5 if merged before the feature freeze? |
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.