-
-
Notifications
You must be signed in to change notification settings - Fork 4.2k
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
fix(mimetype): Fix aborted transaction on PostgreSQL when storing mimetype #40203
Conversation
Fixes nextcloud#40064. This could be fixed by adding a rollback and starting a new transaction before the SELECT query, but in this case that would have the same effect as not using one. See https://dev.mysql.com/doc/refman/8.0/en/innodb-autocommit-commit-rollback.html and https://www.postgresql.org/docs/7.1/sql-begin.html#R1-SQL-BEGIN-1 Signed-off-by: Lucas Azevedo <lhs_azevedo@hotmail.com>
One small thing that I would like to propose as a follow up is to extract the insert/select to another method to avoid the possibility of having an undefined Show codeprotected function store($mimetype) {
$mimetypeId = $this->storeOrRetrieve($mimetype);
$this->mimetypes[$mimetypeId] = $mimetype;
$this->mimetypeIds[$mimetype] = $mimetypeId;
return $mimetypeId;
}
protected function storeOrRetrieve($mimetype) {
try {
$insert = $this->dbConnection->getQueryBuilder();
$insert->insert('mimetypes')
->values([
'mimetype' => $insert->createNamedParameter($mimetype)
])
->executeStatement();
return $insert->getLastInsertId();
} catch (DbalException $e) {
if ($e->getReason() !== DBException::REASON_UNIQUE_CONSTRAINT_VIOLATION) {
throw $e;
}
$qb = $this->dbConnection->getQueryBuilder();
$qb->select('id')
->from('mimetypes')
->where($qb->expr()->eq('mimetype', $qb->createNamedParameter($mimetype)));
$result = $qb->executeQuery();
$id = $result->fetchOne();
$result->closeCursor();
if ($id === false) {
throw new \Exception("Database threw an unique constraint on inserting a new mimetype, but couldn't return the ID for this very mimetype");
}
return (int) $id;
}
} |
The motivation for #35744 was to fix a read after write issue with async replicas, as the So while the handling of the unique constraint violation can probably live outside the transaction, it seems important to me that both the EDIT: Also if we were to wrap the whole thing with two levels of transactions (very probably a bad idea), we might need #39589 first. |
Signed-off-by: Lucas Azevedo <lhs_azevedo@hotmail.com>
Sounds good to me, it fixes the issue and avoid the read-after-write. Thank you, I'll have replication in mind for the next PRs.
Exactly, server/lib/public/AppFramework/Db/TTransactional.php Lines 66 to 69 in 050c6d5
|
/backport to stable27 |
CI failure unrelated |
Summary
In
OC\Files\Type\Loader::store()
we're executing aSELECT
query after an expected failed mimetypeINSERT
in the same transaction. MySQL implicitly rollbacks12 the transaction after the unique constraint violation error, but PostgreSQL does not3 and expects a explicit rollback. That's why we getting theERROR: current transaction is aborted, commands ignored until end of transaction block
error.A possible fix is to rollback before the SELECT statement. We also need to restart the transaction as
atomic()
will try to commit the results after calling our callback and PostgreSQL will complain if there isn't one active (MySQL won't).While this works, we're now executing a single statement in each transaction and this is what the db does by default (see autocommit 45), so we might as well not use the transaction at all and this is what I did. @tcitworld I haven't found a good reason to keep this transaction, but I still don't know the codebase in depth so would you mind checking if my proposal is correct?
Lastly, a valid concern would be the case where another connection erases the mimetype between the
INSERT
attempt and theSELECT
query. Unfortunately transactions alone won't be enough (on MySQL at least, see isolation levels 67). This should be extremely rare, so the exception thrown at the end of the method looks like a good enough solution for me.Checklist
Screenshots before/after for front-end changesDocumentation (manuals or wiki) has been updated or is not requiredFootnotes
https://dev.mysql.com/doc/refman/8.0/en/innodb-error-handling.html ↩
https://www.burnison.ca/notes/fun-mysql-fact-of-the-day-implicit-rollbacks ↩
https://www.postgresql.org/docs/current/tutorial-transactions.html ↩
https://dev.mysql.com/doc/refman/8.0/en/innodb-autocommit-commit-rollback.html ↩
https://www.postgresql.org/docs/7.1/sql-begin.html#R1-SQL-BEGIN-1 ↩
https://dev.mysql.com/doc/refman/8.0/en/innodb-transaction-isolation-levels.html ↩
https://www.postgresql.org/docs/current/transaction-iso.html ↩