Problem/Motivation

By default, Drupal uses the REPEATABLE READ transaction isolation level - this results in lots of deadlock errors.

Drupal.org and many other high traffic sites use READ COMMITTED, so default to that instead. Drupal commerce has been recommended READ COMMITTED since before 2014.

Steps to reproduce

In some circumstances, drush cr + loading a single page in a browser can be enough to trigger a deadlock.

Proposed resolution

Do 2 things:

  1. Add the possibility for siteowners to set the transaction isolation level in the database settings array in setttings.php;
  2. Change the default transaction isolation level for new sites from REPEATABLE READ to READ COMMITTED.

Remaining tasks

TBD

User interface changes

In the database configuration for MySQL, MariaDB or equivalent databases in the installation proces can now the transaction isolation level be set.

API changes

The transaction isolation level can be set in the database settings array in setttings.php

Data model changes

None

Release notes snippet

Drupal now sets the READ COMMITTED transaction isolation level by default for new installs on MySQL. This level has already been recommended for several years and is configurable as before in the database connection settings.

CommentFileSizeAuthor
#248 interdiff.1650930.243-248.txt10.63 KBlongwave
#248 1650930-248.patch17.99 KBlongwave
#243 interdiff-241-243.txt690 bytesgidarai
#243 1650930-243.patch18.85 KBgidarai
#242 1650930-240.patch18.89 KBharivansh
#242 interdiff_1650930-239-1650930-240.txt1.02 KBharivansh
#239 interdiff-237-239.txt9.73 KBgidarai
#239 1650930-239.patch18.9 KBgidarai
#237 interdiff-236-237.txt4.18 KBgidarai
#237 1650930-237.patch26.13 KBgidarai
#236 interdiff-235-236.txt9.84 KBgidarai
#236 1650930-236.patch26.68 KBgidarai
#235 interdiff-233-235.txt1.15 KBgidarai
#235 1650930-235.patch20.84 KBgidarai
#233 1650930-233.patch20.08 KBkarishmaamin
#229 interdiff-228-229.txt683 bytesjungle
#229 1650930-229.patch22.46 KBjungle
#228 1650930-228.patch22.96 KBjungle
#226 1650930-nr-bot.txt86 bytesneeds-review-queue-bot
#225 1650930-225.patch23.29 KBacbramley
#224 1650930-nr-bot.txt86 bytesneeds-review-queue-bot
#223 1650930-223.patch23.29 KBacbramley
#222 1650930-nr-bot.txt86 bytesneeds-review-queue-bot
#221 interdiff_218-221.txt2.24 KBsahil.goyal
#221 1650930-221.patch23.29 KBsahil.goyal
#218 interdiff_217_218.txt1.73 KBgidarai
#218 1650930-218.patch23.23 KBgidarai
#217 1650930-217.patch21.42 KBgidarai
#217 interdiff_216_217.txt3 KBgidarai
#216 interdiff_214_216.txt9.78 KBgidarai
#216 1650930-216.patch21.38 KBgidarai
#214 1650930-214.patch11.81 KBdaffie
#214 interdiff-1650930-203-214.txt785 bytesdaffie
#203 1650930-203.patch11.05 KBdaffie
#197 1650930-197.patch12.47 KBmkalkbrenner
#193 1650930-193.patch13.86 KBmkalkbrenner
#187 1650930-187.patch12.81 KBmkalkbrenner
#184 1650930-184.patch5.08 KBmkalkbrenner
#182 interdiff_169-182.txt2.65 KBpooja saraah
#182 1650930-182.patch15.37 KBpooja saraah
#169 1650930-169-D10.patch15.29 KBdaffie
#167 1650930-167-D10.patch15.27 KBdaffie
#166 interdiff-1650930-161-166.txt3.13 KBdaffie
#166 1650930-166-D9.patch15.58 KBdaffie
#166 1650930-166-D10.patch16.57 KBdaffie
#161 interdiff-1650930-159-161.txt7 KBdaffie
#161 1650930-161.patch15.93 KBdaffie
#159 interdiff_155-159.txt7.25 KBmurilohp
#159 1650930-159.patch13.35 KBmurilohp
#155 interdiff_149-155.txt7.68 KBmurilohp
#155 1650930-155.patch8.74 KBmurilohp
#149 interdiff-1650930-145-149.txt1.14 KBJeroenT
#149 1650930-149.patch7.39 KBJeroenT
#146 1650930-145.patch6.69 KBDieterHolvoet
#144 1650930-144.patch6.66 KBJeroenT
#144 interdiff-142-144.txt714 bytesJeroenT
#142 interdiff-1650930-140-142.txt4.85 KBJeroenT
#142 1650930-142-test-only.patch3.36 KBJeroenT
#142 1650930-142.patch5.8 KBJeroenT
#140 1650930-140-tests-only.patch3.17 KBJeroenT
#140 interdiff-1650930-138-140.txt3.97 KBJeroenT
#140 1650930-140.patch4.73 KBJeroenT
#138 1650930-138.patch1.08 KBSuresh Prabhu Parkala
#135 1650930_135.patch1.52 KBtimohuisman
#131 1650930_131.patch1.56 KBmkalkbrenner
#128 Bildschirmfoto 2021-06-11 um 08.10.31.png74.08 KBmkalkbrenner
#127 1650930_configurable.patch1.65 KBmkalkbrenner
#126 1650930_126.patch609 bytesmkalkbrenner
#124 1650930_124.patch0 bytesmkalkbrenner
#122 1650930_122.patch572 bytesmkalkbrenner
#110 drupal-1650930-110.patch992 bytesprudloff
#109 reroll_diff_1650930_107-109.txt1.04 KBankithashetty
#109 1650930-109.patch1.07 KBankithashetty
#107 1650930_MariaDB.patch1.17 KBmkalkbrenner
#105 1650930-read-commited-transactions-105.patch1.09 KBmhavelant
#103 1650930-103.patch951 byteshideaway
#102 1650930-102.patch1.08 KBanmolgoyal74
#99 1650930-99.patch0 bytesanmolgoyal74
#96 1650930-mr109diff-94.patch1.24 KBelgandoz
#84 1650930-default-mysql-transactions-84-D9.patch1.01 KBsokru
#80 1650930-79.patch1 KBshahankitb1982
#79 1650930-78.patch1 KBshahankitb1982
#67 1650930-67.patch848 bytesklausi
#63 1650930-63.patch710 bytesklausi
cache_set_transaction.patch887 bytesdeviantintegral
#20 drupal-1650930-20-read-committed.patch2.76 KBmikeytown2

Issue fork drupal-1650930

Command icon Show commands

Start within a Git clone of the project using the version control instructions.

Or, if you do not have SSH keys set up on git.drupalcode.org:

Support from Acquia helps fund testing for Drupal Acquia logo

Comments

deviantintegral’s picture

Status: Active » Needs review
tim.plunkett’s picture

Priority: Major » Normal
Status: Needs review » Needs work
Issue tags: +Needs tests

Isn't this rather edge-case-y for a major? Apologies in advance if I'm wrong.

Otherwise, this is in line with other uses of db_transaction().

CNW for tests.

Damien Tournoud’s picture

Status: Needs work » Postponed (maintainer needs more info)

If for some reason the db_merge() query fails, the current transaction is rolled back.

It should not. We start a transaction in MergeQuery::execute(). Not sure what you are describing, but it's definitely not the expected behavior.

Do you have a way to reliably reproduce the issue?

deviantintegral’s picture

I've got some code that generates nodes but it's currently tied to OG and devel. It looks like the rollback is actually happening in the cache_set() call in field_attach_load(), and not in node_save. But you're right, reading the code it does look like only the merge on the cache table should be rolled back and not the whole node object. I'll see if I can get this down to a test case and find what exception is causing the rollback.

deviantintegral’s picture

Title: Don't roll back current transactions of DatabaseBackend::set() fails » Use READ COMMITTED for MySQL transactions
Component: cache system » database system
Status: Postponed (maintainer needs more info) » Needs work

So further investigation lead me to the the real source of the exception, which was a deadlock on the cache table. By default, MySQL is using next-row locking caused by the FOR UPDATE cause in the MergeQuery execute method. By settings the isolation level to READ COMMITTED, we only lock the single row. This can apparently cause problems if you are using replication and are *not* using row locking, but my understanding is that using row locking is pretty much required for Drupal installations.

The attached patch only sets the isolation level for MySQL as READ COMMITTED is the default in PostgreSQL according to http://www.postgresql.org/docs/9.1/static/sql-set-transaction.html.

Haven't tested this yet, and still need a test case.

chx’s picture

Well, if you have MySQL 5.1 then sure http://harrison-fisk.blogspot.ca/2009/02/my-favorite-new-feature-of-mysq... you want READ COMMITTED. But we require http://drupal.org/requirements/ only 5.0.15. Is http://ronaldbradford.com/blog/understanding-mysql-innodb-transaction-is... this situation a problem?

Damien Tournoud’s picture

stewartsmith’s picture

I wouldn't go READ_COMMITTED for InnoDB, the default (REPEATABLE READ) is WAY more tested. That being said, if you just want READ COMMITTED for a particular operation, just using it for that operation should be fine.

Damien Tournoud’s picture

Things have evolved since last year. It is now clear that running a reasonably busy Drupal 7 website under REPEATABLE READ is just suicide. Drupal.org (and several other busy sites that I know of) are successfully running on READ COMMITTED.

I'm marking #2164849: Enforce READ COMMITTED transaction isolation level as a duplicate, here is what I said there:

Our merge query implementation and (part of) the lock implementation) only work properly under READ COMMITTED transaction isolation. We could decide to enforce this, but in the meantime, just set transaction-isolation=READ-COMMITTED in your MySQL server configuration, or add the runtime mentioned by @gielfeldt above.

I know several high-performance sites (including Drupal.org) that are using this successfully.

mikeytown2’s picture

Recently switched to READ COMMITTED as well and things are looking a lot better on our setup.

From my last comment on that dup thread:
Thinking we should set this as the default for all new Drupal installs on MySQL.

$databases['default']['default']['init_commands'] = array(
  'isolation' => "SET SESSION tx_isolation='READ-COMMITTED'"
);

Do we want to put the code in the install form (install_settings_form) as a hidden field or drop it in at a later stage (install_settings_form_submit)?

andypost’s picture

+++ b/core/lib/Drupal/Core/Cache/DatabaseBackend.php
@@ -139,6 +139,13 @@ class DatabaseBackend implements CacheBackendInterface {
+      $transaction = db_transaction();

this should use injected $this->connection

vinmassaro’s picture

We tested READ-COMMITTED level and it eliminated deadlocks but we noticed when using replication, it's possible that replication falls behind and gets out of sync without being able to catch up. Has anyone else run into this? I'm not sure if this situation has been considered based on wanting to make this a default in #10. Thanks.

mikeytown2’s picture

We have 1 slave for replication and have not see any issues with it falling behind so far.

vinmassaro’s picture

In a replicated MySQL environment, using READ-COMMITTED isolation requires using row-based replication and it's critical to have PRIMARY or UNIQUE keys in all your tables that you change. When you have InnoDB tables without primary keys, and use ROW based replication then you'll hit this bug: http://bugs.mysql.com/bug.php?id=53375. We observed large replication delays and it was just when load testing a single database that was set to READ-COMMITTED (one of many on the database server). We found some tables without primary keys:

taxonomy_index
form_builder_cache
config_builder_index
zipcodes
location_instance
nodequeue_types
nodequeue_roles
nodequeue_nodes
mikeytown2’s picture

Query to find tables without a primary or unique key from the comments of this article http://datacharmer.blogspot.com/2011/09/finding-tables-without-primary-k...

SELECT 
  tables.table_schema, 
  tables.table_name,
  -- tables.table_rows,
  tables.engine
FROM information_schema.tables AS tables
LEFT JOIN (
  SELECT table_schema, table_name
  FROM information_schema.statistics
  GROUP BY table_schema, table_name, index_name
  HAVING
  SUM(
    CASE
    WHEN non_unique = 0
    AND nullable != 'YES' THEN 1
    ELSE 0
    END
  ) = COUNT(*)
) AS statistics
  ON tables.table_schema = statistics.table_schema
  AND tables.table_name = statistics.table_name
WHERE statistics.table_name IS NULL
AND tables.table_type = 'BASE TABLE'
AND tables.engine != 'PERFORMANCE_SCHEMA'
-- AND tables.table_rows > 0

Tables that have issues on my site:

flag_types
taxonomy_index
views_data_export_object_cache
wysiwyg_user
zendesk_users

taxonomy_index is in core so that should be fixed. Running this query on this table shows the duplicate entries

SELECT 
  ti.counter,
  taxonomy_index.*
FROM taxonomy_index AS taxonomy_index
INNER JOIN (
  SELECT 
    COUNT(*) AS counter,
    ti.*
  FROM taxonomy_index AS ti
  GROUP BY ti.nid, ti.tid
  HAVING counter > 1
  ORDER BY counter DESC, ti.nid ASC
) AS ti
  ON ti.nid = taxonomy_index.nid
  AND ti.tid = taxonomy_index.tid

On my database, by looking at the created column it appears that this is an error as the created column has the same date across all rows where the nid and tid are the same.

In this case for the core table using ALTER IGNORE TABLE seems like the best way forward.

mikeytown2’s picture

Issue summary: View changes

Also noted that I keep getting deadlocks on the cache_field when cid = field_info_types:en. I created an issue for this #2193149: Deadlocks occur in cache_field table..

Also noted that the semaphore table has issues as well. Using a MEMORY table and everything seems good once I switched to BTREE #1898204-17: Do not use InnoDB for the semaphore table, use Memory

mikeytown2’s picture

With the 2 changes in the above issue, it has been 24 hours since the last deadlock according to SHOW ENGINE innodb STATUS which is a first for us in a long time.

andypost’s picture

So what is a proposed solution here?
Seems better to add this to default database settings line #10 suggests

mikeytown2’s picture

I would like to go forward with #10; if no one has any suggestions on where to put this I'll put in in the advanced options section inside DatabaseTasks::getFormOptions

Also noted #2222635: Waiting for table metadata lock on cache_field table. With this issue I'm opting for table rename without the drop.

mikeytown2’s picture

Status: Needs work » Needs review
FileSize
2.76 KB

Patch needs a little bit of cleanup and we might only want to show that field when the database is of the type mysql.

Status: Needs review » Needs work

The last submitted patch, 20: drupal-1650930-20-read-committed.patch, failed testing.

mikeytown2’s picture

Version: 8.x-dev » 7.x-dev

Made patch against 7.x; will flip back to 8.x once the test passes.

mikeytown2’s picture

Status: Needs work » Needs review
mikeytown2’s picture

Version: 7.x-dev » 8.x-dev
Status: Needs review » Needs work
Issue tags: +needs forwardport to D8
danblack’s picture

while adding an initial connection string as a configuration option may seem reasonable I've got two reservations
a) The mysql default of REPEATABLE-READ makes more sense, especially in the merge function, so i'd suggest a blank value here as default.
b) as a global setting you don't know what assumption other modules/core bits have assumed (but they shouldn't assume),
b) Isn't setting this on a DB global place like my.cnf more appropriate (sure its not accessible if you don't manage the server).

If you want to select a transaction isolation level it probably should be specified in the db_transaction arguments (it won't nest however).

My gut feel is the db_merge function should be done at SERIALIZABLE isolation level to prevent updates between the select and insert implementation in merge. (and this function should be implemented as a INSERT OR REPLACE for mysql/sqlite where keys are primary or unique keys).

mikeytown2’s picture

From http://www.mysqlperformanceblog.com/2013/12/12/one-more-innodb-gap-lock-...

So you see how easy it is to cause a deadlock, so make sure to avoid this situation – if a non-INSERT write operation is more likely to not match any row because the INSERT part is yet to come on the transaction, don’t do it or use REPLACE INTO or use READ-COMMITTED transaction isolation.

Replace does sound like it could work and it looks like it was never discussed in #937284: DEADLOCK errors on MergeQuery INSERT due to InnoDB gap locking when condition in SELECT ... FOR UPDATE results in 0 rows

db_merge requires stronger isolation than REPEATABLE READ in order to fix deadlocks or did I miss something? http://www.ovaistariq.net/597/understanding-innodb-transaction-isolation... If you could explain this recommendation in detail and weigh the pro's and con's that would be ideal.

Thanks for creating #2232037: taxonomy_index should have a primary key :) Also feel free to give advice on these sets of recommendations: https://groups.drupal.org/node/415883

danblack’s picture

it seems the db cache was worked around in #1167144: Make cache backends responsible for their own storage that performs retries on exceptions. The deadlock was most likely an insert of two identical cache entries at the same time. This will be fixed in #2222635: Waiting for table metadata lock on cache_field table once the cid (or hash there of) isn't a primary (and therefore unique) key.

In essence deadlocks are a consequence of databases not being able to meet the ACID requirements within the relaxed to strict requirements that the isolation level provides. The way to deal with the deadlock really needs to be handled by the application and in the case of cache inserts a retry is appropriate.

I'd suggest the transaction isolation level should be an argument to the db_transaction object that reverts on commit.

mikeytown2’s picture

Issue tags: +database cache
pounard’s picture

I suggest we lower the default transaction level at the BDTNG level as a first step and start thinking how about we need ACID in Drupal as a follow-up. We have to take into account that Drupal database schema is stupid (very very stupid) and doesn't comply to any king of consistency since it doesn't even have any FOREIGN KEY constraints. It doesn't have any constraints at all except PRIMARY KEYs and some sporadic UNIQUE KEY. Let's avoid stupid deadlocks.

danblack’s picture

> I suggest we lower the default transaction level at the BDTNG level

From what I've seen of the code it seems to me making a lot of assumptions that a SELECT statement is a perfect consideration of a following UPDATE (e.g. db_merge) and as such assuming a default REPEATABLE-READ is in effect.

Helper fixes that allow things to be fixed after careful consideration:

> Drupal database schema is stupid (very very stupid) and doesn't comply to any king of consistency since it doesn't even have any FOREIGN KEY constraints.

Ok, lets find the instances of these, add a key and and put them in a transaction so at least its an all or nothing change.

> It doesn't have any constraints at all except PRIMARY KEYs and some sporadic UNIQUE KEY. Let's avoid stupid deadlocks.

If you define the constraints of PRIMARY and UNIQUE you shouldn't be surprised if occasionally parallel requests cause inserts of the same primary/unique key. Its not the database problem to say which is right or wrong. It just accepts one and rejects another with an exception.

So lets also avoid dumb deadlock detection and processing (core/lib/Drupal/Core/Cache/DatabaseBackend.php:set where its retried without realizing that its happening because the cid is unique and two threads failed a cache hit at the start and separately created a new entry after generating the same data. The right behaviour here is to catch the deadlock exception and ignore it and not retry a set (are the cache entries really any different?). Alternately #2222635-14: Waiting for table metadata lock on cache_field table that has cidhash as non unique).

pounard’s picture

I'm not really sure you actually understood what I said.

regilero’s picture

just a link to https://drupal.org/node/1800286#comment-8672597, where a test is made of parallel DELETE+INSERT on REPEATABLE READ -> deadlock. And delete+insert is the default mode for drupal field updates...

mikeytown2’s picture

In reply to #14/#15 this has been committed to D8 and D7 patch is waiting #610076: Add a primary key to the {taxonomy_index} table and prevent duplicates being inserted

tatyana’s picture

Does anyone have any experience running a commerce site with READ COMMITTED?

Damien Tournoud’s picture

@tatyana: we have been recommending READ COMMITTED for a very long time. Virtually all the large users of Drupal Commerce are running under READ COMMITTED.

tatyana’s picture

Great, thanks for the confirmation

mikeytown2’s picture

Getting a Fatal error: Unsupported operand types

Adding this to database.inc seems to fix it

    if (!is_array($connection_options['init_commands'])) {
      unset($connection_options['init_commands']);
      $connection_options['init_commands'] = array();
    }

This needed to be adjusted in install.core.inc as well (missing the list dir).
Fatal error: Call to undefined function list_extract_allowed_values()

    include_once DRUPAL_ROOT . '/modules/field/modules/list/list.module';
    $init_commands = list_extract_allowed_values($init_commands, 'list_text', TRUE);
catch’s picture

Priority: Normal » Major

It's an edge case but one that lots and lots of sites run into quite frequently, and it's a fatal when you do.

colan’s picture

So if you're using replication, it looks like you'd need these three (3) settings. The additional two (2) on the end are optional.

# Lock only index records, not the gaps before them.
# https://dev.mysql.com/doc/refman/5.6/en/set-transaction.html
transaction-isolation = READ-COMMITTED

# With READ COMMITTED, you must use row-based binary logging.
# https://dev.mysql.com/doc/refman/5.6/en/binary-log-setting.html
binlog_format = row

# Eliminate a race condition with row-based replication.
# https://dev.mysql.com/doc/refman/5.6/en/innodb-auto-increment-configurable.html
innodb_autoinc_lock_mode = 2

# Uncomment this to shrink the amount of data written to the log.
# https://dev.mysql.com/doc/refman/5.6/en/replication-options-binary-log.html#sysvar_binlog_row_image 
#binlog_row_image = minimal

# Uncomment this to see the queries not normally available in row format.
# Useful for debugging.
# https://dev.mysql.com/doc/refman/5.6/en/replication-options-binary-log.html#sysvar_binlog_rows_query_log_events
#binlog_rows_query_log_events = TRUE

It looks like these settings need to be on both master & slaves as per a MySQL Server Asynchronous Replication Example.

References:

I'd add this to the wiki page, but Fixes for MySQL Deadlocks in D7 isn't a wiki page.

mikeytown2’s picture

Add it as a comment; unfortunately I can't change it to a wiki. Thanks for doing more research into this :)

morgantocker’s picture

For some MySQL version context, we proposed changing the default to READ-COMMITTED, but later withdrew the proposal based on feedback received. Here is my blog post in it: http://www.tocker.ca/2015/01/14/proposal-to-change-replication-and-innod...

Dimitri has also published benchmarks on isolation level impact. Repeatable-read performs better with short transactions/queries:
http://dimitrik.free.fr/blog/archives/2015/02/mysql-performance-impact-o...

(I'm still a fan of read-committed.. just noting this change isn't as straight forward beneficial as it seems. Dan's comments about db_merge are also true: there are probably other reapeatable-read assumptions being made.)

gopisathya’s picture

We recently encountered “Deadlock found when trying to get lock; try restarting transaction: DELETE FROM {cache_field}” in production.
we run following as part of deployment process
> drush updb -y
> drush fra -y
> drush cc all

We use replication.

Is putting following READ-COMMITTED setting in settings.php is safe in production environment (or) we should do at mysql server level as mention by https://www.drupal.org/node/1650930#comment-9407649

$databases['default']['default']['init_commands'] = array(
'isolation' => "SET SESSION tx_isolation='READ-COMMITTED'"
);

rodrigoeg’s picture

We have also found this issue on Drupal 8 (we tested using 8.0.2), on user registration flow (URL: /user/register or using directly the User functions in core).
The deadlock occurs with the 'users_field_data' table.

In our initial tests, these deadlocks have stopped after changing to 'READ-COMMITTED'.

catch’s picture

Version: 8.0.x-dev » 8.1.x-dev

Drupal 8.0.6 was released on April 6 and is the final bugfix release for the Drupal 8.0.x series. Drupal 8.0.x will not receive any further development aside from security fixes. Drupal 8.1.0-rc1 is now available and sites should prepare to update to 8.1.0.

Bug reports should be targeted against the 8.1.x-dev branch from now on, and new development or disruptive changes should be targeted against the 8.2.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

DamienMcKenna’s picture

Version: 8.1.x-dev » 8.2.x-dev

Bumping to 8.2.x.

Version: 8.2.x-dev » 8.3.x-dev

Drupal 8.2.0-beta1 was released on August 3, 2016, which means new developments and disruptive changes should now be targeted against the 8.3.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Version: 8.3.x-dev » 8.4.x-dev

Drupal 8.3.0-alpha1 will be released the week of January 30, 2017, which means new developments and disruptive changes should now be targeted against the 8.4.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Version: 8.4.x-dev » 8.5.x-dev

Drupal 8.4.0-alpha1 will be released the week of July 31, 2017, which means new developments and disruptive changes should now be targeted against the 8.5.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Version: 8.5.x-dev » 8.6.x-dev

Drupal 8.5.0-alpha1 will be released the week of January 17, 2018, which means new developments and disruptive changes should now be targeted against the 8.6.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Wim Leers’s picture

FYI: MySQL made READ-COMMITTED its new default in version 5.7.

mvc’s picture

@Wim Leers: https://dev.mysql.com/doc/refman/5.7/en/innodb-transaction-isolation-lev... says the default is still REPEATABLE READ.

pounard’s picture

Lowering the default transaction level per default is often a bad idea, in theory, if database were fast enough, it should always remain isoled serialisable. I experienced very bad behaviors with commerce due to read commited level that left lots of data in an incoherent state. It should be left to each site developer or infrastructure administrator to decide such a dangerous choice.

Anonymous’s picture

Wim Leers’s picture

#54: you're right, I misread http://www.tocker.ca/2015/01/14/proposal-to-change-replication-and-innod...! That was only a proposal, it was not actually applied.

#55: Very interesting remark! Because Damien Tournoud wrote at #1650930-35: Use READ COMMITTED by default for MySQL transactions:

we have been recommending READ COMMITTED for a very long time. Virtually all the large users of Drupal Commerce are running under READ COMMITTED.

(He wrote that precisely 4 years ago today!)

Version: 8.6.x-dev » 8.7.x-dev

Drupal 8.6.0-alpha1 will be released the week of July 16, 2018, which means new developments and disruptive changes should now be targeted against the 8.7.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

mkalkbrenner’s picture

We ran into these Deadlocks. I think that READ COMMITTED should be the default isolation level for drupal in general.
In fact sub-systems like Redis, Memcache, APCU, Solr, elastic search, ... are defacto opperating in a "READ COMMITTED" mode and therefore MySQL / MariaDB should be configured consistently. Any code in drupal that relies on REPEATABLE READ should be considered to be wrong.

pounard’s picture

> In fact sub-systems like Redis, Memcache, APCU, Solr, elastic search, ... are defacto opperating in a "READ COMMITTED" mode

Those are not RDBMS, lowering the default transaction level may also cause data inconsistency issues.

mkalkbrenner’s picture

If you run REPEATABLE READ you get a completely different behavior if you store a cache entry during a transaction within the database or for example apcu (which the core enables automatically for some caches if available). With READ COMMITTED you get the same behavior. That's why I argue that any code that relies on REPEATABLE READ needs to be considered to be wrong.

Before Drupal switched to InnoDB and used MyISAM, we defacto had something like READ COMMITTED, too (READ UNCOMMITTED to be precise).

The Deadlock we face at the moment causes a data loss unless you write some code that re-runs the latest request.

catch’s picture

Title: Use READ COMMITTED for MySQL transactions » Use READ COMMITTED by default for MySQL transactions
Status: Postponed » Active

We made significant changes to router rebuilding, such that I'm not sure #2572283: Neither REPEATABLE READ nor READ COMMITTED transaction isolation levels are always appropriate is an issue any longer. So unpostponing this.

klausi’s picture

Status: Active » Needs review
FileSize
710 bytes

Can we just set the default init_commands like this?

mradcliffe’s picture

I think that the approach taken in #2572283: Neither REPEATABLE READ nor READ COMMITTED transaction isolation levels are always appropriate to improve transaction support in our abstraction layer is better than explicitly setting an isolation level. We would want to allow more complex approaches to transactions as it is really, really limited at the moment, and it makes sense to me to allow developers to change the behavior based on complex database models.

mkalkbrenner’s picture

it makes sense to me to allow developers to change the behavior based on complex database models.

@mradcliffe I disgree. As I already stated in #61, any contrib or core module that requires REPEATABLE READ does something wrong and the code isn't portable to another database or cache backends anymore.

If your custom module requires it, you already can set it using the existing low level mechanisms.

Therefore I don't think that an API like proposed in #2572283: Neither REPEATABLE READ nor READ COMMITTED transaction isolation levels are always appropriate should be introduced.

mradcliffe’s picture

I disagree with the following part of your statement that "code isn't portable to another database", @mkalkbrenner. And the differences with transaction isolation level behavior is precisely WHY a developer may want READ COMMITTED over REPEATABLE READ (or SERIALIZABLE over REPEATABLE READ). Furthermore the entire point of a database abstraction layer (DBAL) is to have a high-level implementation to give us tools to do so in code rather than mucking about directly in SQL. In my opinion this leads to a maintainability nightmare of SQL scripts in custom projects.

The API in #2572283: Neither REPEATABLE READ nor READ COMMITTED transaction isolation levels are always appropriate also does more than just introduce isolation levels, but attempts to bring Drupal's transaction handling in par with other DBAL such as Doctrine. Because we don't have robust transaction handling we ended up with this weird Storage API that explicitly depends on implicit transaction commits, which made it necessary to write a stupid hack around it to get things to work in the pgsql driver.

A robust transaction interface that allows for better transaction commits, rollbacks and isolation levels

1. Makes it easier for core and contrib. database drivers to implement transaction isolation levels i.e. improves our core/contrib db drivers.
2. Improves Developer Experience by allowing custom development to use a common interface i.e. improves maintainability.
3. Helps put our current DBAL on a better playing field i.e. improves marketability.

I don't see how not providing a common interface for developers helps out in the long term.

It is a bit off-topic, and so I think that we should add the #2572283 as a follow-up todo in the patch in #63.

klausi’s picture

Good idea, added the @todo.

The test run took 48 minutes vs. 49 minutes on 8.7.x-dev, so we might see a tiny performance improvement with this :-)

bojanz’s picture

We still tell all Commerce sites to run under "READ COMMITTED" (and there's even a patch for adding a warning when it doesn't: #2733675: Warning when mysql is not set to READ-COMMITTED). It's also the first step of every performance engagement we do for a client.

Can't comment on the problems hinted at in #55, we haven't encountered them so far.

effulgentsia’s picture

I think this issue could use a new summary.

In particular, a list of pros/cons based on what we currently know about Drupal sites running READ COMMITTED or having tried to do so but switched back due to problems with it. For example:

  • drupal.org and Commerce sites (both are cases of write-heavy sites) seem to be good success stories so far. Any other examples where it's working out well?
  • Does it cause known problems for sites that are read-heavy but not write-heavy? Are there any specific examples of sites for which READ COMMITTED leads to data integrity failures?

Version: 8.7.x-dev » 8.8.x-dev

Drupal 8.7.0-alpha1 will be released the week of March 11, 2019, which means new developments and disruptive changes should now be targeted against the 8.8.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Gogowitsch’s picture

Good news: I applied the patch from #67 to my CI setup and ran the Bash script from the related issue #2833539. The script is just a quick way to reproduce an otherwise intermittent issue. With the Bash script, I used to get tons of these errors "1213 Deadlock found when trying to get lock; try restarting transaction: INSERT INTO {node_field_data}". Thanks to the patch, they are gone. I switched between vanilla Drupal 8.6.10 and with patch applied multiple times and found that the patch consistently fixed my issues.

Without the patch, our CI pipeline quickly fails. With the patch, it consistently succeeds.

My setup: Ubuntu 18.04.1, Drupal 8.6.10 running within Docker via Lando v3.0.0-rc.13, MySQL 5.7.25 within Docker, all with default settings.

catch’s picture

Priority: Major » Critical

Does it cause known problems for sites that are read-heavy but not write-heavy?

I don't see how this could be an issue, the transaction level is going to have less impact the less writes there are.

I deliberately did not get this on my local machine, and I run into it on almost every cache clear. Bumping to critical since it's a fatal error during normal site operation that affects regular users.

Version: 8.8.x-dev » 8.9.x-dev

Drupal 8.8.0-alpha1 will be released the week of October 14th, 2019, which means new developments and disruptive changes should now be targeted against the 8.9.x-dev branch. (Any changes to 8.9.x will also be committed to 9.0.x in preparation for Drupal 9’s release, but some changes like significant feature additions will be deferred to 9.1.x.). For more information see the Drupal 8 and 9 minor version schedule and the Allowed changes during the Drupal 8 and 9 release cycles.

apaderno’s picture

Issue tags: -needs forwardport to D8 +needs forward port to Drupal 8
Ghost of Drupal Past’s picture

For other interested parties reading this issue and wondering about taxonomy_index: in D8 it has a primay key. It was added in 1d198fbd68 in 2014 from issue #2232037: taxonomy_index should have a primary key.

mike82’s picture

For MySQL 8 it needs to be transaction_isolation instead of tx_isolation. Works fine for me.

neclimdul’s picture

+++ b/core/lib/Drupal/Core/Database/Driver/mysql/Connection.php
@@ -461,6 +461,11 @@ public static function open(array &$connection_options = []) {
+      // Set a low transaction level because otherwise busy Drupal sites run
+      // into transaction deadlocks.
+      // @todo Support more fine grained transaction isolation levels in
+      // https://www.drupal.org/project/drupal/issues/2572283
+      'isolation' => "SET SESSION tx_isolation='READ-COMMITTED'",

Should we code this directly in the connection calls if it will break some binary log modes? I don't know the answer but I worry there might be some unhappy people that apply an update and suddenly their site is completely broken.

shahankitb1982’s picture

N/A

shahankitb1982’s picture

For path #67. If there is an older MySQL version then it is fine.

For the newer MySQL 8 version it should be

transaction_isolation

So check first in MySQL

mysql> show variables like 'tx_isolation';
Empty set (0.00 sec)

If above result is empty then use

show variables like 'transaction_isolation'

Accordingly changes needed. Thanks

/drupal/web/core/lib/Drupal/Core/Database/Driver/mysql/Connection.php

    $version_server = $pdo->getAttribute(\PDO::ATTR_SERVER_VERSION);

    $transaction = 'transaction_isolation';
    if (version_compare($version_server, '8.0.11', '<')) {
      $sql_mode .= ',NO_AUTO_CREATE_USER';
      $transaction = 'tx_isolation';
    }
    $connection_options['init_commands'] += [
      'sql_mode' => "SET sql_mode = '$sql_mode'",
      // Set a low transaction level because otherwise busy Drupal sites run
      // into transaction deadlocks.
      // @todo Support more fine grained transaction isolation levels in
      // https://www.drupal.org/project/drupal/issues/2572283
      'isolation' => "SET SESSION ".$transaction."='READ-COMMITTED'",
    ];
shahankitb1982’s picture

Kris77’s picture

I have this error with migrate process: "Drupal\Core\Database\DatabaseExceptionWrapper: SQLSTATE[40001]: Serialization failure: 1213 Deadlock found when trying to get lock; try restarting transaction: INSERT INTO {key_value_expire} (name, collection, value) VALUES (:db_insert_placeholder_0, :db_insert_placeholder_1, :db_insert_placeholder_2); Array...

Can I try patch in #67 or in #81?

I'm using Drupal 8.8.

Thanks...

Version: 8.9.x-dev » 9.1.x-dev

Drupal 8.9.0-beta1 was released on March 20, 2020. 8.9.x is the final, long-term support (LTS) minor release of Drupal 8, which means new developments and disruptive changes should now be targeted against the 9.1.x-dev branch. For more information see the Drupal 8 and 9 minor version schedule and the Allowed changes during the Drupal 8 and 9 release cycles.

rgpublic’s picture

Hmm. Don't why, but since I added "transaction-isolation = READ-COMMITTED" our MariaDB 10.4 Galera cluster starts crashing every other day. Stuff like this is suddenly appearing in the logs and is then repeating endlessly while the DB won't accept any new connections:

Mai 29 11:33:58 yak1 mysqld[2479]: Mutex at 0x55c2e3150200, Mutex DICT_SYS created dict0dict.cc:824, lock var 2
Mai 29 11:33:58 yak1 mysqld[2479]: 2020-05-29 11:33:58 0 [Note] InnoDB: A semaphore wait:
Mai 29 11:33:58 yak1 mysqld[2479]: --Thread 139726468044544 has waited at dict0dict.cc line 880 for 41.00 seconds the semaphore:
Mai 29 11:33:58 yak1 mysqld[2479]: Mutex at 0x55c2e3150200, Mutex DICT_SYS created dict0dict.cc:824, lock var 2
Mai 29 11:33:58 yak1 mysqld[2479]: 2020-05-29 11:33:58 0 [Note] InnoDB: A semaphore wait:
Mai 29 11:33:58 yak1 mysqld[2479]: --Thread 139726627870464 has waited at dict0dict.cc line 8

I never had such a problem with the default ("REPEATABLE-READ"). I've removed the READ-COMMITTED for now. Is this really safe to enable also for Galera clusters?

pounard’s picture

Why not handling the transaction properly instead ? If cache, key-value store or other bits are identified as often being the cause of transaction deadlock, the proper SQL way of doing this probably is to let them isolated in REPEATABLE READ or SERIALIZABLE, do a SELECT FOR UPDATE when updating (and prey for your RDBMS to properly do predictive locking when inserting), and retry the transactions when they deadlock or fail at COMMIT. It would be easy to implement for cache or key-value if they are in standalone transactions, it would be extremely difficult to implement when they happen in a larger transaction that does business stuf (entity save I guess would be the most common scenario). It might worth the shot to investigate this thought.

catch’s picture

We also discussed using a separate database connection for cache/lock etc. in #2347867: Race conditions with lock/cache using non-database storage - add a non-transactional database connection.

thursday_bw’s picture

Status: Needs review » Needs work

Having read the last few comments, this shold be on needs work as there is deep discussion and heady questions following the latest patch.

Ghost of Drupal Past’s picture

Status: Needs work » Needs review

While I do not want to play issue ping-pong there's nothing to work on here, a decision needs to be made..

Did we discuss whether Galera is in scope? This patch, after all, just provides a default. We always provided a sensible default for most of Drupal users and provided override/extension points as necessary. Which this patch also does. Is it worth dropping / holding up this patch for Galera users? Mind you: I am neither for nor against it. I am just asking the question.

pfrenssen’s picture

I think checking the version number to determine whether to use tx_isolation or transaction_isolation might be brittle. A more reliable way to know which of the two variables is supported by a given MySQL-alike database is to query the available variables:

SHOW VARIABLES WHERE variable_name IN ("tx_isolation", "transaction_isolation");

Version: 9.1.x-dev » 9.2.x-dev

Drupal 9.1.0-alpha1 will be released the week of October 19, 2020, which means new developments and disruptive changes should now be targeted for the 9.2.x-dev branch. For more information see the Drupal 9 minor version schedule and the Allowed changes during the Drupal 9 release cycle.

super_romeo made their first commit to this issue’s fork.

super_romeo’s picture

Rerolled for D9.1.

jukka792’s picture

I tried to apply patch #84 to Drupal 9.1.2 but got composer error:

Could not apply patch! Skipping. The error was: Cannot apply patch https://www.drupal.org/files/issues/2020-05-04/1650930-default-mysql-tra...

The reason why tried to apply is that my migrate says sometimes:
SQLSTATE[40001]: Serialization failure: 1213 Deadlock found when trying to get lock; try restarting transaction

If I remember right, this patch used to work on some older drupal 9 or 8.

elgandoz’s picture

@jukka792, you have to use the diff from the merge request !109, from the GitLab repo. You can simply go to the merge request and download as .diff, and then change the extension with .patch.
For simplicity i attached the file here from #94, which I can confirm it applies on D 9.1.2.

neclimdul’s picture

Status: Needs review » Needs work

It may apply to 9.2 but it doesn't apply to 9.2 because of #3185231: Use combination SQL modes instead of explicit modes so it does need a reroll.

neclimdul’s picture

I still don't like that this can break replicated sites out of the blue because of a log mode setting that could be hard to change. I guess a site can get around that by adding the following to settings.php but I'm not sure how for them to get from a white screen of death to finding this hack.

$databases['default']['default']['init_commands']['isolation'] = '';
anmolgoyal74’s picture

Status: Needs work » Needs review
FileSize
0 bytes

Re-rolled for 9.2.x

JeroenT’s picture

Status: Needs review » Needs work
JeroenT’s picture

Issue tags: +Needs reroll
anmolgoyal74’s picture

Status: Needs work » Needs review
FileSize
1.08 KB
hideaway’s picture

Reroll of #67 for Drupal core 9.1.x version.

mhavelant’s picture

Status: Needs review » Needs work

$version_server = $pdo->getAttribute(\PDO::ATTR_SERVER_VERSION);
This returns the wrong version for Azure Managed MySQL instances and connections fail as `NO_AUTO_CREATE_USER` was removed in MySQL 8.

See "Azure Database for MySQL server" reports wrong database version.

$this->connection->query('SELECT VERSION()')->fetchColumn();
This used as the fix in the issue above to fix this.

mhavelant’s picture

Here's an updated version (mainly for 9.1.x). I also removed NO_AUTO_CREATE_USER as it was not in 9.1.x either. If that's required I can add it again.

mkalkbrenner’s picture

+++ b/core/lib/Drupal/Core/Database/Driver/mysql/Connection.php
@@ -206,8 +206,22 @@ public static function open(array &$connection_options = []) {
+ if (version_compare($server_version, '5.7.20', '<')) {

For MySQL 8 it needs to be transaction_isolation instead of tx_isolation. Works fine for me.

MariaDB still uses tx_isolation. And its Version numbers are higher, 10.x and above.
So this needs to be respected here.

apaderno’s picture

Status: Needs review » Needs work
ankithashetty’s picture

prudloff’s picture

FileSize
992 bytes

Here is a reroll for 8.9 in case someone needs it.

Version: 9.2.x-dev » 9.3.x-dev

Drupal 9.2.0-alpha1 will be released the week of May 3, 2021, which means new developments and disruptive changes should now be targeted for the 9.3.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

mkalkbrenner’s picture

Status: Needs review » Reviewed & tested by the community
Issue tags: -needs forward port to Drupal 8, -database cache

We ran this patch since years on various sites with different Drupal versions. In our use-cases we have massive drush commands with transactions and disabled memory caches. Without this patch you might corrupt the data because you miss the updated cache entries from other processes or page requests.

daffie’s picture

Status: Reviewed & tested by the community » Needs work
Issue tags: -Needs tests +Needs manual testing
  1. +++ b/core/lib/Drupal/Core/Database/Driver/mysql/Connection.php
    @@ -187,8 +187,23 @@ public static function open(array &$connection_options = []) {
    +    static $server_version;
    +
    +    if (!$server_version) {
    +      $server_version = $pdo->query('SELECT VERSION()')->fetchColumn();
    +    }
    

    Having this code in the database open function is something I really do not like. Yes, we had that in the past and I was very happy that we could remove it.

  2. +++ b/core/lib/Drupal/Core/Database/Driver/mysql/Connection.php
    @@ -187,8 +187,23 @@ public static function open(array &$connection_options = []) {
    +    if (stripos($server_version, 'Maria') || version_compare($server_version, '5.7.20', '<')) {
    +      // This was deprecated in MySQL 5.7.20 and removed in 8. But MariaDB still
    +      // uses tx_isolation.
    +      $txn_isolation = 'tx_isolation';
    +    }
    

    Do we really need to do 2 different things here? One thing for MySQL and one for MariaDB. Can we set them both?

  3. +++ b/core/lib/Drupal/Core/Database/Driver/mysql/Connection.php
    @@ -187,8 +187,23 @@ public static function open(array &$connection_options = []) {
    +    if (stripos($server_version, 'Maria') || version_compare($server_version, '5.7.20', '<')) {
    

    The minimum required version for MySQL is now 5.7.8 and we are here testing for 5.7.20. Maybe the minimum required version for MySQL should become 5.7.20 for D10. Then this check can be removed in D10.

  4. +++ b/core/lib/Drupal/Core/Database/Driver/mysql/Connection.php
    @@ -187,8 +187,23 @@ public static function open(array &$connection_options = []) {
    +      'isolation' => "SET SESSION " . $txn_isolation . " = 'READ-COMMITTED'",
    

    I understand that for most sites out there, this change will be an improvement. I just worry about the sites for which this change will result into errors or very slow sites. Especially for the ones where there is very limited knowledge about Drupal. Hoping that this change will not result in a clusterfuck.

  5. +++ b/core/lib/Drupal/Core/Database/Driver/mysql/Connection.php
    @@ -187,8 +187,23 @@ public static function open(array &$connection_options = []) {
    +      // This was deprecated in MySQL 5.7.20 and removed in 8. But MariaDB still
    +      // uses tx_isolation.
    

    Could we add some links to the MySQL and MariaDB documentation about this settings.

  6. Changing the tag "needs tests" to "needs manual testing"
  7. The IS needs to be updated and a CR is needed as we are changing the default transaction isolation from "REPEATABLE READ" to "READ COMMITTED".
mkalkbrenner’s picture

Wow, more concerns then lines of code ;-)

+++ b/core/lib/Drupal/Core/Database/Driver/mysql/Connection.php
@@ -187,8 +187,23 @@ public static function open(array &$connection_options = []) {
+ if (stripos($server_version, 'Maria') || version_compare($server_version, '5.7.20', '<')) {

The minimum required version for MySQL is now 5.7.8 and we are here testing for 5.7.20. Maybe the minimum required version for MySQL should become 5.7.20 for D10. Then this check can be removed in D10.

I agree with this.

+++ b/core/lib/Drupal/Core/Database/Driver/mysql/Connection.php
@@ -187,8 +187,23 @@ public static function open(array &$connection_options = []) {
+ static $server_version;
+
+ if (!$server_version) {
+ $server_version = $pdo->query('SELECT VERSION()')->fetchColumn();
+ }

Having this code in the database open function is something I really do not like. Yes, we had that in the past and I was very happy that we could remove it.

+++ b/core/lib/Drupal/Core/Database/Driver/mysql/Connection.php
@@ -187,8 +187,23 @@ public static function open(array &$connection_options = []) {
+ if (stripos($server_version, 'Maria') || version_compare($server_version, '5.7.20', '<')) {
+ // This was deprecated in MySQL 5.7.20 and removed in 8. But MariaDB still
+ // uses tx_isolation.
+ $txn_isolation = 'tx_isolation';
+ }

Do we really need to do 2 different things here? One thing for MySQL and one for MariaDB. Can we set them both?

This could only be achieved when we split the drivers. Or let the MariaDB driver extend the MySQL driver. I wonder if this would be a smart decision anyway as both systems started to diverge.

daffie’s picture

This could only be achieved when we split the drivers. Or let the MariaDB driver extend the MySQL driver. I wonder if this would be a smart decision anyway as both systems started to diverge.

This is something that should be done in a new major release.

mkalkbrenner’s picture

This is something that should be done in a new major release.

I agree.

So my suggestion is this:

  • Introduce a dedicated MariaDB driver that simply extends the MySQL driver (in the first step)
  • Introduce the transaction level as (advanced) configuration option of both drivers
  • Select READ COMMITTED as default for new installations
  • Don't set READ COMMITTED for existing installations automatically. People could manually switch to it.
  • Write a a Release Note about it and mention that it is recommended to switch from the MySQL driver to the MariaDB driver if the site runs on MariaDB

How do we get an approval on this approach from the maintainers. I don't won't spent any time on this when it takes 7 more years for an approval.

andypost’s picture

daffie’s picture

@mkalkbrenner: I have asked @catch on Slack if he could give this issue some guidance, as he is one of the core release managers. He can make the decision. When it is a very big decision, he will bring it to the core committer meeting. @catch is also a core committer. Drupal 10 is to be released in the summer of 2022. It is very likely to be fixed in that release. When we get the ok and do the work.

catch’s picture

Question on the patch first:

+    $txn_isolation = 'transaction_isolation';
+
+    if (stripos($server_version, 'Maria') || version_compare($server_version, '5.7.20', '<')) {
+      // This was deprecated in MySQL 5.7.20 and removed in 8. But MariaDB still
+      // uses tx_isolation.
+      $txn_isolation = 'tx_isolation';
+    }

It seems to me that both MySQL and MariaDB support the syntax:

SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED.

https://mariadb.com/kb/en/set-transaction/

https://dev.mysql.com/doc/refman/8.0/en/set-transaction.html

Is there a reason we can't use that? Then the discussion about separate drivers and versions might be moot.

If that's not possible:

1. I'm not sure about creating a new driver for this issue, it's cleaner when you look at it, but for example if it does turn out there's a cross-platform way to set this, then we'd have an unnecessary driver sitting around. Much easier to remove an if statement than deprecate a database driver and get everyone to migrate back.

2. If we do need to create a new driver, then that could (and should, so that people can change that before doing a major upgrade) happen in a minor release. The only thing we'd have to restrict to a major release, is removing MariaDB support from the MySQL driver.

3. I don't think the variable name changing is a pressing reason to increase the MySQL minimum version. Obviously we could remove the check if we do though.

daffie’s picture

I have talked to @catch on Slack and he said: "I don't think it needs a committers meeting yet - if we do the $settings option, we can have a follow-up to discuss actually changing the default altogether. I think we should change the default for brand new sites, just not existing ones."

Lets do that.

Removing the tags: ''Drupal 10" and "needs release managers review".

catch’s picture

fwiw for changing the default, I think it comes under the category of 'disruptive bugfix' - i.e. it would have to be either a minor or major release, and probably requires some warning. It's likely to fix issues on far more sites than it breaks, but it's possible it'll break someone's site somewhere.

One option might be:

1. Add the option in settings, defaulting to the current
2. On sites where the default isn't set, show a notice in the status report that the site is using the default, and it's going to change to READ COMMITTED in Drupal X.x
3. Once we change the default, show a notice in the status report that the site is using the default and that the default is READ COMMITTED.

But we should move this over to the follow-up once it's created.

mkalkbrenner’s picture

daffie’s picture

Status: Needs review » Needs work

Patch does not apply.

mkalkbrenner’s picture

Status: Needs work » Needs review
FileSize
0 bytes
apaderno’s picture

Status: Needs review » Needs work
mkalkbrenner’s picture

mkalkbrenner’s picture

mkalkbrenner’s picture

Using the patch in #127 the transaction isolation level is now configurable during the site installation or in settings.php:

$databases['default']['default'] = [
  'database' => 'db',
  'username' => 'db',
  'password' => 'db',
  'host' => 'localhost,
  'driver' => 'mysql',
  'port' => 3306,
  'prefix' => '',
  'isolation_level' => 'READ COMMITTED',
];

daffie’s picture

  1. +++ b/core/lib/Drupal/Core/Database/Driver/mysql/Install/Tasks.php
    @@ -175,6 +175,17 @@ public function getFormOptions(array $database) {
    +    $form['advanced_options']['isolation_level'] = [
    

    I think that for this form element the average sites owner shall needs some info about the options. Which should he/she select and what is the default and what are the differences. But not too much. Something sensible.

  2. +++ b/core/lib/Drupal/Core/Database/Driver/mysql/Install/Tasks.php
    @@ -175,6 +175,17 @@ public function getFormOptions(array $database) {
    +        'READ UNCOMMITTED' => 'READ UNCOMMITTED',
    

    Why did you add this option. I am not saying that you are wrong just curious.

We are gong to need some tests to make sure that the patch does what we want it to do:
1. That when we have an existing site and the setting isolation_level is not set, that it is using the isolation level REPEATABLE READ (query the database);
2. When we install a new site the key isolation_level is set in settings.php and its value is READ COMMITTED;
3. When we install a new site that it is using the isolation level READ COMMITTED (query the database);
4. When we have an existing sites and the key isolation_level is set in settings.php, that it is using the isolation level that is the same as in settings.php. Test for every possibility (at least for READ COMMITTED and REPEATABLE READ) (query the database).

catch’s picture

I don't think we should offer this as an option on the form at all - default to READ COMMITTED, those who need something else can also edit settings.php

mkalkbrenner’s picture

Status: Needs work » Needs review
FileSize
1.56 KB

Like this?

daffie’s picture

Status: Needs review » Needs work

I think that is what @catch wants. For the tests, you could look at the ones in the directory core/tests/Drupal/FunctionalTests/Installer/.

shaktik’s picture

frequently getting errors while creating node and edit.

(
    [:db_condition_placeholder_0] => 4xx-response
)
" at /var/www/d8/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorage.php line 829" while reading response header from upstream, client: 123.XX.79.XX, server: example.com, request: "POST /node/4045123/edit?destination=/admin/content HTTP/1.1", upstream: "fastcgi://unix:/var/run/php/php7.2-fpm.sock:", host: "13.XX.XX.123, referrer: "http://13.XX.XX.123/node/4045123/edit?destination=/admin/content"
2021/08/05 16:03:56 [error] 23273#23273: *9266876 FastCGI sent in stderr: "PHP message: Uncaught PHP Exception Drupal\Core\Entity\EntityStorageException: "SQLSTATE[HY000]: General error: 1205 Lock wait timeout exceeded; try restarting transaction: UPDATE {cachetags} SET invalidations=invalidations + 1
WHERE tag = :db_condition_placeholder_0; Array
(
    [:db_condition_placeholder_0] => node_list
)
" at /var/www/example.com/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorage.php line 829" while reading response header from upstream, client: 123.XX.79.XX, server: example.com, request: "POST /admin/structure/entityqueue/asia_news/asia_news HTTP/1.1", upstream: "fastcgi://unix:/var/run/php/php7.2-fpm.sock:", host: "", referrer: "http://13.XX.XX.123/admin/structure/entityqueue/asia_news/asia_news"
Sseto’s picture

On my D8 site, I had the same issue with deadlock and I added 'isolation_level' => 'READ COMMITTED', to my settings.php and it worked!

timohuisman’s picture

We use composer patches to apply patches. Merge request patch urls are updated for every commit on the branch, which makes it a moving target. Attached is a patch containing the current state of the merge request (https://git.drupalcode.org/project/drupal/-/merge_requests/109.patch).

Youcanlearnit’s picture

#122 didn't work and #124 is empty
Is there a patch to 9.2.5 ?

jukka792’s picture

I am getting this error with D9.2.6 and patch #109
Drupal\Core\Entity\EntityStorageException: SQLSTATE[HY000]: General error: 1205 Lock wait timeout exceeded; try restarting transaction: INSERT INTO "cache_entity"

I am deleting media content with multiple browser tabs (bulk delete)

Suresh Prabhu Parkala’s picture

Status: Needs work » Needs review
FileSize
1.08 KB

Re-rolled patch. Please review.

neclimdul’s picture

Status: Needs review » Needs work

still no settings.php control.

JeroenT’s picture

#131 adds the possibility to set the isolation_level in settings.php.

I tried to add test coverage that checks if the isolation_level is correctly added to the database settings when a new Drupal site is installed and when a site is installed with existing database settings.

Let's see what the testbot thinks.

The last submitted patch, 140: 1650930-140.patch, failed testing. View results

The last submitted patch, 142: 1650930-142.patch, failed testing. View results

Version: 9.3.x-dev » 9.4.x-dev

Drupal 9.3.0-rc1 was released on November 26, 2021, which means new developments and disruptive changes should now be targeted for the 9.4.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

DieterHolvoet’s picture

I rerolled the patch for 9.4.x.

Status: Needs review » Needs work

The last submitted patch, 146: 1650930-145.patch, failed testing. View results

catch’s picture

Issue summary: View changes
Issue tags: +Bug Smash Initiative

Looks like a few more tests need updating to deal with the new form element.

I started on updating the issue summary.

jibran’s picture

Issue tags: +Needs change record
  1. +++ b/core/modules/mysql/src/Driver/Database/mysql/Connection.php
    @@ -219,10 +219,13 @@ public static function open(array &$connection_options = []) {
    +        // @todo Switch to 'READ COMMITTED' for Drupal 10, see https://www.drupal.org/node/1650930
    

    This comment is more than 80 lines.

  2. +++ b/core/modules/mysql/src/Driver/Database/mysql/Install/Tasks.php
    @@ -175,6 +175,12 @@ public function getFormOptions(array $database) {
    +    // Add the isolation_level option to settings.php.
    +    $form['isolation_level'] = [
    +      '#type' => 'value',
    +      '#default_value' => empty($database['isolation_level']) ? 'READ COMMITTED' : $database['isolation_level'],
    +    ];
    

    Given it is just a value now we need to update the screenshot in the IS.
    Also, do we need to add this option to default.settings.php as per the comment here?
    I think we should add a change notice about this change.

daffie’s picture

Status: Needs review » Needs work
Issue tags: -Needs manual testing
  1. +++ b/core/modules/mysql/src/Driver/Database/mysql/Install/Tasks.php
    @@ -175,6 +175,12 @@ public function getFormOptions(array $database) {
    +      '#default_value' => empty($database['isolation_level']) ? 'READ COMMITTED' : $database['isolation_level'],
    

    Should we not add more options here? There are 4 optuions according to MySQL. See: https://dev.mysql.com/doc/refman/8.0/en/innodb-transaction-isolation-lev...
    Now new sites are forced to use "READ COMMITTED" and existing sites are defaulted to "REPEATABLE READ". I would like to give sitebuilders more options.

  2. +++ b/core/modules/mysql/src/Driver/Database/mysql/Connection.php
    @@ -219,10 +219,13 @@ public static function open(array &$connection_options = []) {
    +        // @todo Switch to 'READ COMMITTED' for Drupal 10, see https://www.drupal.org/node/1650930
    +        'isolation_level' => 'REPEATABLE READ',
    

    To much indentation (2 spaces).

  3. +++ b/core/tests/Drupal/FunctionalTests/Installer/InstallerIsolationLevelExistingDatabaseSettingsTest.php
    @@ -0,0 +1,49 @@
    +    $connection_info['default']['isolation_level'] = 'REPEATABLE READ';
    

    Existing sites do not have this line in their setttings.php, therefor needs this line be removed.

  4. +++ b/core/tests/Drupal/FunctionalTests/Installer/InstallerIsolationLevelExistingDatabaseSettingsTest.php
    @@ -0,0 +1,49 @@
    +    $this->assertStringContainsString("'isolation_level' => 'REPEATABLE READ',", $contents);
    

    This line should test that the "isolation_level" is not set in the settings.php.

    What needs to be added is to check the transaction isolation from the database. It should be "REPEATABLE READ". The query should be: SELECT @@SESSION.transaction_isolation. See: https://dev.mysql.com/doc/refman/5.7/en/set-transaction.html

  5. +++ b/core/modules/mysql/src/Driver/Database/mysql/Install/Tasks.php
    --- /dev/null
    +++ b/core/tests/Drupal/FunctionalTests/Installer/InstallerIsolationLevelExistingDatabaseSettingsTest.php
    
    +++ b/core/tests/Drupal/FunctionalTests/Installer/InstallerIsolationLevelExistingDatabaseSettingsTest.php
    --- /dev/null
    +++ b/core/tests/Drupal/FunctionalTests/Installer/InstallerIsolationLevelNoDatabaseSettingsTest.php
    

    Both tests are MySQL only and therefor need to be moved to the mysql module.

We have now tests and therefor we no longer need manual testing.

daffie’s picture

Component: database system » mysql db driver
Issue summary: View changes
Issue tags: -Needs issue summary update, -Needs change record

Updated the IS and created the CR.

daffie’s picture

Issue summary: View changes
daffie’s picture

Issue summary: View changes
murilohp’s picture

Status: Needs work » Needs review
FileSize
8.74 KB
7.68 KB

Hey! I tried to address both #150 and #151 here, so regarding #150:

  1. Fixed! I tried to follow the current pattern that we have for @todos in the code, please check: LibraryDiscoveryParser.php#L166 for more details(3 spaces when we break the line).
  2. We still need to update the screenshot on the IS, but I added a new comment at sites/default/default.settings.php documenting the new setting isolation_level

I think we should add a change notice about this change.

Thanks to @daffie, we have now a CR!

Moving to #151:

  1. I think it would be nice to add this option on the form too, but following what @catch said on #130, the READ COMMITTED option will be the default and now the users can change this on settings.php, to help the users I followed the suggestion on #150.2 and documented this new option at default.settings.php, we just need to update the IS removing the screenshot.
  2. I think it's fixed, please check what I said about it on #150.1
  3. Fixed!
  4. Fixed! I changed the assert to validate if the isolation_level is not set in the settings.php. I also added a test to validate the transaction_isolation, thanks for the query!
  5. Fixed! I moved the tests into the MySQL module

Thanks!

Status: Needs review » Needs work

The last submitted patch, 155: 1650930-155.patch, failed testing. View results

daffie’s picture

@murilohp: Thank you for working on this.

  1. Every change you make to sites/default/default.settings.php you also need to make to core/assets/scaffold/files/default.settings.php
  2. +++ b/core/modules/mysql/src/Driver/Database/mysql/Install/Tasks.php
    @@ -175,6 +175,12 @@ public function getFormOptions(array $database) {
    +    $form['isolation_level'] = [
    +      '#type' => 'value',
    +      '#default_value' => empty($database['isolation_level']) ? 'READ COMMITTED' : $database['isolation_level'],
    +    ];
    

    This form needs to have all 4 possible transaction isolation level options in it. We should also add a text with something like: "The recommended option is 'READ COMMITTED'. Existing sites without this setting will default to 'REPEATABLE READ'. THe other 2 options are available, but not supported. Use them at your own risk. For more info: https://dev.mysql.com/doc/refman/5.7/en/innodb-transaction-isolation-lev...."

  3. +++ b/core/modules/mysql/src/Driver/Database/mysql/Connection.php
    @@ -219,10 +219,14 @@ public static function open(array &$connection_options = []) {
         $connection_options['init_commands'] += [
           'sql_mode' => "SET sql_mode = 'ANSI,TRADITIONAL'",
    +      'isolation' => 'SET SESSION TRANSACTION ISOLATION LEVEL ' . $connection_options['isolation_level'],
         ];
    

    I think we should do a test to see if the set isolation level is one of the 4 possible options. If not, we should throw an new DatabaseTransactionIsolationLevelException. Just before the selected code.

  4. +++ b/core/modules/mysql/tests/src/Functional/InstallerIsolationLevelExistingDatabaseSettingsTest.php
    @@ -0,0 +1,55 @@
    +    $this->assertStringNotContainsString("'isolation_level' => 'REPEATABLE READ',", $contents);
    

    Checking for isolation_level is enough. We are testing that the isolation_level key is not set.

  5. +++ b/core/modules/mysql/tests/src/Functional/InstallerIsolationLevelExistingDatabaseSettingsTest.php
    @@ -0,0 +1,55 @@
    +    unset($connection_info['default']['pdo'], $connection_info['default']['init_commands']);
    ...
    +    $this->settings['databases']['default'] = (object) [
    

    The part , $connection_info['default']['init_commands'] can be removed.

  6. +++ b/sites/default/default.settings.php
    @@ -138,6 +138,16 @@
    + * For MySQL users only, you can change the transaction isolation level using
    + * 'isolation_level' setting. By default, MySQL will use 'READ COMMITTED'.
    

    Can we change this text to: "For MySQL, MariaDB or equivalent databases can the option 'isolation_level' be set. The recommended option is 'READ COMMITTED'. Existing sites without this setting will default to 'REPEATABLE READ'. The other 2 options are READ UNCOMMITTED and SERIALIZABLE. They are available, but not supported. Use them at your own risk. For more info: https://dev.mysql.com/doc/refman/5.7/en/innodb-transaction-isolation-lev...."

  7. +++ b/sites/default/default.settings.php
    @@ -138,6 +138,16 @@
    + * $databases['default']['default']['isolation_level'] = 'REPEATABLE READ';
    + * $databases['default']['default']['isolation_level'] = 'READ UNCOMMITTED';
    + * $databases['default']['default']['isolation_level'] = 'SERIALIZABLE';
    

    We are missing the option "READ COMMITTED".

  8. +++ b/core/modules/mysql/src/Driver/Database/mysql/Connection.php
    @@ -219,10 +219,14 @@ public static function open(array &$connection_options = []) {
    +        // @todo Switch to 'READ COMMITTED' for Drupal 10
    +        //   see https://www.drupal.org/node/1650930.
    +        'isolation_level' => 'REPEATABLE READ',
    

    Too much indentation. Please remove 2 spaces from the start of every line.

catch’s picture

I'm not convinced it's useful to add this to the installer form, it seems like an invitation for people to shoot themselves in the foot. Just making sure we have decent documentation in default.settings.php seems enough here.

murilohp’s picture

Hey I addressed #157 on this new patch.

  1. Fixed! And thanks for the tip!
  2. As @catch said on #158, we just need to make a decent documentation in default.settings.php
  3. I think it's a great idea, I created DatabaseTransactionIsolationLevelException for this and a new kernel test to validate this exception.
  4. Now that we have 'isolation_level' string inside both sites/default/default.settings.php and core/assets/scaffold/files/default.settings.php as a comment, how do we handle this? I mean I think this test will fail here.
  5. Fixed!
  6. I updated the documentation but the link exceeds 80 characters, is that a problem?
  7. Added the option READ COMMITTED
  8. Fixed!

Leaving as NW to see the testbot here. Also, it would be nice to have your thoughts here @daffie.

daffie’s picture

Assigned: Unassigned » daffie

I am going to work on the tests.

daffie’s picture

andypost’s picture

It looks ready to go, failures are unrelated and known

+1 rtbc

mkalkbrenner’s picture

Status: Needs review » Reviewed & tested by the community
alexpott’s picture

Status: Reviewed & tested by the community » Needs work
  1. +++ b/core/assets/scaffold/files/default.settings.php
    @@ -138,6 +138,21 @@
    + * $databases['default']['default']['isolation_level'] = 'READ UNCOMMITTED';
    + * $databases['default']['default']['isolation_level'] = 'SERIALIZABLE';
    
    +++ b/sites/default/default.settings.php
    @@ -138,6 +138,21 @@
    + * $databases['default']['default']['isolation_level'] = 'READ UNCOMMITTED';
    + * $databases['default']['default']['isolation_level'] = 'SERIALIZABLE';
    

    Given these are not supported I would not have the specific PHP to set them here.

  2. +++ b/core/modules/mysql/src/Driver/Database/mysql/Connection.php
    @@ -219,10 +219,25 @@ public static function open(array &$connection_options = []) {
    +    $isolation_levels = [
    ...
    +      'REPEATABLE READ',
    +      'READ UNCOMMITTED',
    +      'SERIALIZABLE',
    +    ];
    +
    +    if (!in_array($connection_options['isolation_level'], $isolation_levels)) {
    +      throw new DatabaseTransactionIsolationLevelException($connection_options['isolation_level']);
    +    }
    

    Tempting to use a value enum here on Drupal 10. Then we wouldn't need the exception. It would mean a divergence between Drupal 9 and 10 but maybe that is okay.

  3. +++ b/core/modules/mysql/src/Driver/Database/mysql/Connection.php
    @@ -219,10 +219,25 @@ public static function open(array &$connection_options = []) {
         $connection_options['init_commands'] += [
           'sql_mode' => "SET sql_mode = 'ANSI,TRADITIONAL'",
    +      'isolation' => 'SET SESSION TRANSACTION ISOLATION LEVEL ' . $connection_options['isolation_level'],
         ];
    

    I've got a big concern here. This is going to result in unnecessary work on a lot of sites that are already using READ-COMMITTED via MySQL configuration. Are we sure that this is the correct approach here? I think we should use a different approach here: I think we should only set the isolation_level if it is set. Further more, and crucially, if this change goes in as planned it is going to change ALL isolation levels to REPEATABLE READ - which feels super wrong. Many sites already default to READ-COMMITTED via my.cnf or other configuration options.

mkalkbrenner’s picture

I've got a big concern here. This is going to result in unnecessary work on a lot of sites that are already using READ-COMMITTED via MySQL configuration. Are we sure that this is the correct approach here? I think we should use a different approach here: I think we should only set the isolation_level if it is set. Further more, and crucially, if this change goes in as planned it is going to change ALL isolation levels to REPEATABLE READ - which feels super wrong. Many sites already default to READ-COMMITTED via my.cnf or other configuration options.

You're right. So the correct fix will be to set READ-COMMITTED per default for Drupal 10. In Drupal 9 the isolation level should only be set if explicitly configured in settings.php?

daffie’s picture

Fixed all points from @catch in comment #164. @catch: Thank you for the review.

For D9 the isolation level is only set when if explicitly configured in settings.php

For D10 the 4 isolation levels are created without enumeration. The PHPCS process does not like PHP 8.1 enumeration. The testbot result is:

FILE: .../core/modules/mysql/src/Driver/Database/mysql/IsolationLevel.php
----------------------------------------------------------------------
FOUND 7 ERRORS AFFECTING 7 LINES
----------------------------------------------------------------------
  1 | ERROR | [x] Missing file doc comment
 18 | ERROR | [x] Line indented incorrectly; expected 0 spaces, found
    |       |     2
 21 | ERROR | [x] Line indented incorrectly; expected 0 spaces, found
    |       |     2
 26 | ERROR | [x] Line indented incorrectly; expected 0 spaces, found
    |       |     2
 31 | ERROR | [x] Line indented incorrectly; expected 0 spaces, found
    |       |     2
 36 | ERROR | [x] Line indented incorrectly; expected 0 spaces, found
    |       |     2
 38 | ERROR | [x] Line indented incorrectly; expected 2 spaces, found
    |       |     0
----------------------------------------------------------------------
PHPCBF CAN FIX THE 7 MARKED SNIFF VIOLATIONS AUTOMATICALLY
----------------------------------------------------------------------

The patch can be found here: #3264683-11: [IGNORE] Testing only

PHPCS is working on support for PHP 8.1 stuff: https://github.com/squizlabs/PHP_CodeSniffer/issues/3479.

@mkalkbrenner: Thank you for your input.

daffie’s picture

Forgot to remove core/modules/mysql/src/Driver/Database/mysql/IsolationLevel.php.

alexpott’s picture

  1. +++ b/core/modules/mysql/src/Driver/Database/mysql/Connection.php
    @@ -194,10 +194,16 @@ public static function open(array &$connection_options = []) {
    +    if (!$isolation_level = IsolationLevel::tryFrom($connection_options['isolation_level'])) {
    +      throw new DatabaseTransactionIsolationLevelException($connection_options['isolation_level']);
    +    }
    

    I think we can do $isolation_level = IsolationLevel::from()... that will throw an exception for us that probable is quite descriptive. That said given coder needs fixing for enums then perhaps we should drop that and covert to enums later.

  2. +++ b/core/modules/mysql/src/Driver/Database/mysql/Connection.php
    @@ -194,10 +194,16 @@ public static function open(array &$connection_options = []) {
         $connection_options['init_commands'] += [
           'sql_mode' => "SET sql_mode = 'ANSI,TRADITIONAL'",
    +      'isolation' => 'SET SESSION TRANSACTION ISOLATION LEVEL ' . $isolation_level->value,
         ];
    

    This still means for every connection to the db we have to send additional commands. Which will result in additional network traffic if the database is not local - I don't know if this matters...

daffie’s picture

For #168.1: As I explained in #166, we cannot do enumerations, because it will fail PHPCS.

For #168.2: I do not know how else to do it. I will look into it.

catch’s picture

I think we should open a 10.1 clean-up issue for enum, no need to add work to a decade old critical bug.

alexpott’s picture

Title: Use READ COMMITTED by default for MySQL transactions » PP-1: Use READ COMMITTED by default for MySQL transactions
Priority: Critical » Normal
Status: Needs review » Postponed

Discussed with @catch and we agreed to promote #2733675: Warning when mysql is not set to READ-COMMITTED to critical in-lieu of this one. And postpone this issue on that one.

Setting this on all databases is potentially a small performance regression on sites where it is already set and changing it automagically is a bit fraught as discussed above. So we should do #2733675: Warning when mysql is not set to READ-COMMITTED to alert people and give good instructions about ways to set it. And then come back to this issue once we've landed that one. Once we have this on the status report I don't think this issue is critical any more.

Steven McCoy’s picture

What would be involved in creating a patch that works for the current stable Drupal (9.39 at the time of writing)

Version: 9.4.x-dev » 9.5.x-dev

Drupal 9.4.0-alpha1 was released on May 6, 2022, which means new developments and disruptive changes should now be targeted for the 9.5.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

Freddy Rodriguez’s picture

Not sure if this module was meant to fix and optimize transactions in D7: https://www.drupal.org/project/apdqc
Is it worth migrating this to d9/d10?

daffie’s picture

Status: Postponed » Needs work
daffie’s picture

Title: PP-1: Use READ COMMITTED by default for MySQL transactions » Use READ COMMITTED by default for MySQL transactions
daffie’s picture

Priority: Normal » Major

Talked to @catch on Slack and this issue can become a major one.

claudiu.cristea made their first commit to this issue’s fork.

claudiu.cristea’s picture

Status: Needs work » Needs review

I've created a new MR (the old need base branch change by @super_romeo) on top of 9.5.x. Rerolled, #166.

Agree with @catch on #170, enum issue should be a follow-up

daffie’s picture

  1. In the file default.settings.php, we have the following text. As we are changing the default isolation level to "READ COMMITTED" is the text not correct anymore. My suggestion is to change the second line to "The default and recommended transaction isolation level for Drupal sites is 'READ COMMITTED'."
     * For MySQL, MariaDB or equivalent databases the 'isolation_level' option can
     * be set. The recommended transaction isolation level for Drupal sites is
     * 'READ COMMITTED'. The 'REPEATABLE READ' option is supported but can result
     * in deadlocks, the other two options are 'READ UNCOMMITTED' and 'SERIALIZABLE'.
     * They are available but not supported; use them at your own risk. For more
     * info:
     * https://dev.mysql.com/doc/refman/5.7/en/innodb-transaction-isolation-levels.html
     *
     * On your settings.php, change the isolation level:
     * @code
     * $databases['default']['default']['init_commands'] = [
     *   'isolation_level' => 'SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED',
     * ];
     * @endcode
    
  2. We have still the remark from @alexpott in #168.2. We can fix that according to https://dev.mysql.com/doc/refman/5.7/en/set-variable.html. When you scroll down to "Multiple variable assignment", we can create a single statement by:
    SET sql_mode = 'ANSI,TRADITIONAL', SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED
    
  3. The testbot fails on Drupal\Tests\mysql\Functional\InstallerIsolationLevelExistingSettingsTest

@claudiu.cristea: Thank you for working on this.

pooja saraah’s picture

Addressed the comment on #181
Attached Patch and interdiff

alexpott’s picture

I'm not convinced we should be fixing this issue. We have the warning and there are plenty of ways to set the default if you want to. Like you can change you MySQL server to default to 'READ COMMITTED' for all transactions. For example you could run SET GLOBAL TRANSACTION ISOLATION LEVEL READ COMMITTED and be done.

  1. +++ b/core/modules/mysql/src/Driver/Database/mysql/Connection.php
    @@ -219,10 +219,23 @@ public static function open(array &$connection_options = []) {
    +    $isolation_levels = [
    +      'READ COMMITTED',
    +      'REPEATABLE READ',
    +      'READ UNCOMMITTED',
    +      'SERIALIZABLE',
    +    ];
    +
    +    if (!in_array($connection_options['isolation_level'], $isolation_levels)) {
    +      throw new DatabaseTransactionIsolationLevelException($connection_options['isolation_level']);
    +    }
    +
    

    I don't think this check is actually worth it. If you set it to something bogus mysql is going to fail anyway. And this is run on every request. I'm not sure it is that useful.

  2. +++ b/core/modules/mysql/src/Driver/Database/mysql/Connection.php
    @@ -219,10 +219,23 @@ public static function open(array &$connection_options = []) {
    +      'sql_mode' => "SET sql_mode = 'ANSI,TRADITIONAL', SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED",
    +      'isolation' => 'SET SESSION TRANSACTION ISOLATION LEVEL ' . $connection_options['isolation_level'],
    

    Why are we setting it twice here? Once in the sql_mode and once in the isolation init command. I think the sql_mode changes are wrong. I think #181.2 is suggesting we make sql_mode two queries... hmmm. That'll mean using a semi-colon in the query. This will probably work because we are using \PDO::exec() directly here. Not sure if it is the right thing to do.

  3. +++ b/core/modules/mysql/src/Driver/Database/mysql/DatabaseTransactionIsolationLevelException.php
    @@ -0,0 +1,25 @@
    +class DatabaseTransactionIsolationLevelException extends \RuntimeException implements DatabaseException {
    

    As above I'm not sure this exception is actually worth it.

mkalkbrenner’s picture

Here's the patch against Drupal 9.4.1 as we need it. It addresses Alex' concerns in 183 I agree with.

I'm not convinced we should be fixing this issue. We have the warning and there are plenty of ways to set the default if you want to. Like you can change you MySQL server to default to 'READ COMMITTED' for all transactions. For example you could run SET GLOBAL TRANSACTION ISOLATION LEVEL READ COMMITTED and be done.

I'm still of the opinion the the default has to be changed and that the patch is needed to do that. The argument have been mentioned in this issue already.

daffie’s picture

Status: Needs review » Needs work

@mkalkbrenner: You patch is missing some code changes and it should be for D9.5. Also could you an interdiff file. It makes reviewing your changes a lot easier.

alexpott’s picture

+++ b/core/modules/mysql/src/Driver/Database/mysql/Connection.php
@@ -219,10 +219,11 @@ public static function open(array &$connection_options = []) {
-      'sql_mode' => "SET sql_mode = 'ANSI,TRADITIONAL'",
+      'sql_mode' => "SET sql_mode = 'ANSI,TRADITIONAL', SESSION TRANSACTION ISOLATION LEVEL " . $connection_options['isolation_level'],

+++ b/core/modules/mysql/src/Driver/Database/mysql/Install/Tasks.php
@@ -175,6 +175,12 @@ public function getFormOptions(array $database) {
+      '#default_value' => $database['isolation_level'] ?? 'READ COMMITTED',

You can't do this. SET variable and SET TRANSACTION do not mix.

We can do "SET sql_mode = 'ANSI,TRADITIONAL'; SET TRANSACTION ISOLATION LEVEL " . $connection_options['isolation_level'] - to make this a single query to send to the db server.

But I still feel that we shouldn't be setting the default here. I'm not sure that Drupal is the correct place to configure this. Hence we now have the warning.

mkalkbrenner’s picture

Your right. I forgot to add the additional tests.

The interdiff reports "Whitespace damage detected in input" against #182.

I assume that others are in the same situation as we are and therefore require a patch against Drupal 9.4 to fix their broken deployment because of the release of 9.4.x. So this is my priority. The patch for 9.5.x might follow later.

mkalkbrenner’s picture

But I still feel that we shouldn't be setting the default here. I'm not sure that Drupal is the correct place to configure this. Hence we now have the warning.

@alexpott see my comments #59 and #61.

alexpott’s picture

For me if #59 and #61 are reasons then we need to do this also for SQLite and Postgres or at least detail why this is not going to cause different behaviour for them which will be even more confusing than the current behaviour - which has been this way for years. I don't think the reasoning given here is good enough to change the default behaviour. I think we need to do #3291949: Improve the MySQL transaction isolation warning because we should not be producing warnings on things that are supported. I think there are good reasons why you might want READ COMMITTED - but there are also good reasons for REPEATABLE READ.

alexpott’s picture

Well look at that Postgres default is read committed... hmmm. That is interesting. SQLITE is a different beast altogether and does not have a read committed mode. So I guess we can argue that setting it to read committed is making our approach to db drivers more consistent. Another point in this changes favour is that Drupal core and all contrib is tested with READ COMMITTED because that's the default on DrupalCI.

What makes me nervous about this change is that there is some site out there which relies on the extra compliance of REPEATABLE READ for something financial and by making this change we introduce a very hard to solve and find bug.

daffie’s picture

What makes me nervous about this change is that there is some site out there which relies on the extra compliance of REPEATABLE READ for something financial and by making this change we introduce a very hard to solve and find bug.

We can make sure that this change is the first to be mentioned in the Drupal 9.5 release notes.

As an alternative we can make the change only for new sites and at some later date we can change the setting for existing sites.

mkalkbrenner’s picture

I just got a report from my colleague. Since we moved to Drupal 9.4.0 in dev we had to remove the patch from here because it didn't apply anymore. As a consequence our heavy migration scripts to migrate an old legacy system to Drupal ran into [error] Drupal\Core\Database\DatabaseExceptionWrapper: SQLSTATE[40001]: Serialization failure: 1213 Deadlock found when trying to get lock; multiple times.
Now with patch #187 applied it works again. We use MariaDB.

mkalkbrenner’s picture

alexpott’s picture

@mkalkbrenner the thing I don't get is why you wouldn't fix this at the database config level instead of needing a drupal patch. Like you can set this in your my.cnf so your database connections are in READ COMMITTED by default and you'll be good to go with no patch and less risk.

catch’s picture

The issue summary still says we'd set this for new sites only, I think changing it for existing sites should be another follow-up probably - should be a much smaller patch but with a lot more consequences.

mkalkbrenner’s picture

@mkalkbrenner the thing I don't get is why you wouldn't fix this at the database config level instead of needing a drupal patch. Like you can set this in your my.cnf so your database connections are in READ COMMITTED by default and you'll be good to go with no patch and less risk.

I'm still convinced that not using READ COMMITTED is a bug. I want to get rid of all these reports about dead locks or cache inconsistencies in contrib or core and the common advice to "clear your caches". It's not just about our current project at Jarltech.
New users should not search existing issues for a solution of a problem they run into (if they get more traffic). It should work out of the box.

daffie’s picture

@mkalkbrenner This is D9.5 and newer issue. Only bugfixes can go into a already released version (D9.4).

deviantintegral’s picture

I was curious what Drupal-focused hosting providers are using. It looks like Pantheon is using READ COMMITTED, and Acquia is using REPEATABLE READ. It doesn't look like Pantheon documents the setting anywhere. In that case, it seems to me that the risk of production regressions changing this is fairly low. Otherwise, I expect Pantheon would have documented this for customers migrating to them.

rgpublic’s picture

FWIW: I wrote 2 years ago in this thread that READ COMMITTED caused us problems on Galera. This seems to have resolved as in the meantime we were audacious enough to switch it back on and we are using READ COMMITTED for quite some time now without any further problems. So, basically good news on that front.

Interestingly, Galera is still warning about using this:

https://galeracluster.com/library/documentation/isolation-levels.html

(See the red box under "Understanding Isolation Levels")

I wonder what they mean with "In multi-master mode, however, you can only use the REPEATABLE-READ level.". We are on using multi-master mode and SHOW VARIABLES LIKE '%TX%' shows that READ-COMMITTED is enabled. Is this perhaps automatically ignored? Is this information still valid? Could there be any lurking dangers for data consistency that I'm not aware of?

At least I can say that it hasn't proven totally disastrous for us and we are running about 50 Drupal websites approx.

mkalkbrenner’s picture

It seems that the discussion came to an end as that the community agreed on READ COMMITTED:
#2733675: Warning when mysql is not set to READ-COMMITTED

So the remaining issue for Drupal 9.5 and 10.0 is whether to explicitly set the default or not.

And it would be great to ensure that the tests on drupal.org are using that mode!

Ghost of Drupal Past’s picture

So to sum up the problem here, as far as I understand these matters: READ COMMITTED can only be used safely if every table has a primary key because otherwise InnoDB might issue a full table scan on the slave for every row modified which can lead to abysmal slave performance or even a crash. See for example https://bugs.mysql.com/bug.php?id=53375 and https://bugs.mysql.com/bug.php?id=76252

Both MySQL and MariaDB can enforce PK: MariaDB 10.1 added https://mariadb.com/kb/en/innodb-system-variables/#innodb_force_primary_key but that's for InnoDB only and MySQL 8.0 added https://dev.mysql.com/doc/refman/8.0/en/server-system-variables.html#sys... . Drupal doesn't require MySQL 8.0 which is a pity for other (window function) reasons but I digress.

So I think we should have an issue which adds a warning to the status report if there are tables in the schema which doesn't have a primary key.

daffie’s picture

No interdiff file, because the old patch did not apply any more. Based on the patch from comment #182:

  • Removed the changes to default.settings.php as they where already added in #2733675: Warning when mysql is not set to READ-COMMITTED.
  • For the comment by @alexpott in #183.1: Removed the exception DatabaseTransactionIsolationLevelException and the related test modules/mysql/tests/src/Kernel/ConnectionTest. Also removed the part in modules/mysql/src/Driver/Database/mysql/Connection that was throwing the exception.
  • Add the option in the installer for selection the prefered transaction isolation level:
    +    $form['advanced_options']['isolation_level'] = [
    +      '#type' => 'select',
    +      '#title' => t('Transaction isolation level'),
    +      '#options' => [
    +        'READ COMMITTED' => t('Read Committed'),
    +        'REPEATABLE READ' => t('Repeatable Read'),
    +        'DO NOT SET BY DRUPAL' => t('Do not set by Drupal'),
    +      ],
    +      '#default_value' => $database['isolation_level'] ?? 'READ COMMITTED',
    +      '#description' => t('The recommended transaction isolation level is "READ COMMITTED". The "Do not set by Drupal" option will not get this setting as do existing sites. Sites without this setting will default to the database set transaction isolation level. Which when not set defaults to "REPEATABLE READ".'),
    +    ];
    

    Users can also select to not let the transaction isolation level be set by Drupal and set it manually in the database.

  • Changed that the default transaction isolation level is only be set for new sites. For existing sites will nothing change. Updated the added test for this.

@chx: Greart point. I will into it.

Status: Needs review » Needs work

The last submitted patch, 203: 1650930-203.patch, failed testing. View results

jedgar1mx’s picture

Anyone having issues installing this on 9.4.1? I keep getting Could not apply patch! error.

Steven McCoy’s picture

@jedgar1mx - I'm using the patch from #149 on 9.4.1

Version: 9.5.x-dev » 10.1.x-dev

Drupal 9.5.0-beta2 and Drupal 10.0.0-beta2 were released on September 29, 2022, which means new developments and disruptive changes should now be targeted for the 10.1.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

andypost’s picture

Only one test fails Drupal\Tests\mysql\Functional\RequirementsTest, for some reason the message is not displayed

acbramley’s picture

We were hitting deadlocks on cache_bootstrap writing the last_write_timestamp when we had >1 user while running a K6 load test that was creating an entity. While I'm surprised it's that easy to cause a deadlock, I can happily say that swapping to READ COMMITTED allowed us to bump up to over 100 concurrent.

The call stack for writing this key is a bit suspect however:

ChainedFastBackend.php:306, Drupal\Core\Cache\ChainedFastBackend->markAsOutdated()
ChainedFastBackend.php:247, Drupal\Core\Cache\ChainedFastBackend->invalidateTags()
CacheTagsInvalidator.php:35, Drupal\Core\Cache\CacheTagsInvalidator->invalidateTags()
Cache.php:111, Drupal\Core\Cache\Cache::invalidateTags()
EntityBase.php:531, Drupal\Core\Entity\EntityBase->invalidateTagsOnSave()
EntityBase.php:392, Drupal\Core\Entity\EntityBase->postSave()
ContentEntityBase.php:463, Drupal\Core\Entity\ContentEntityBase->postSave()

Why does it need to mark something outdated in cache_bootstrap when invalidating entity tags?

EDIT: It also does the same for cache_config and cache_discovery. None of these bins contain any of the tags being invalidated.

Jan-E’s picture

Why does it need to mark something outdated in cache_bootstrap when invalidating entity tags?

Could it be to mitigate https://www.drupal.org/project/drupal/issues/2833539 ?

acbramley’s picture

@Jan-E not sure what you mean? This invalidation is causing the deadlocks.

catch’s picture

@acbramley Cache tags can be used with any cache bin - for example the render cache bin will have more entity cache tags in it than the entity cache bin.

However, I don't think we need to invalidate the entire fast backend in that case, so I've opened #3334489: ChainedFastBackend invalidates all items when cache tags are invalidated to discuss more.

acbramley’s picture

@catch awesome! Thank you :)

daffie’s picture

Status: Needs work » Needs review
FileSize
785 bytes
11.81 KB

Fixed the Drupal\Tests\mysql\Functional\RequirementsTest.

gidarai’s picture

Added a test to check if all tables have a primary key (if not show a warning on the status report page)

gidarai’s picture

Added the patch and interdiff files (didn't add it before)

gidarai’s picture

Fixed code standards

gidarai’s picture

FileSize
23.23 KB
1.73 KB

Regenerated phpstan baseline

alexpott’s picture

Status: Needs review » Needs work
  1. +++ b/core/modules/mysql/src/Driver/Database/mysql/Install/Tasks.php
    @@ -175,6 +175,18 @@ public function getFormOptions(array $database) {
    +        'READ COMMITTED' => t('Read Committed'),
    +        'REPEATABLE READ' => t('Repeatable Read'),
    +        'DO NOT SET BY DRUPAL' => t('Do not set by Drupal'),
    

    $this->t() is available and preferred to t()

  2. +++ b/core/modules/mysql/src/Driver/Database/mysql/Install/Tasks.php
    @@ -175,6 +175,18 @@ public function getFormOptions(array $database) {
    +      '#description' => t('The recommended transaction isolation level is "READ COMMITTED". The "Do not set by Drupal" option will not get this setting as do existing sites. Sites without this setting will default to the database set transaction isolation level. Which when not set defaults to "REPEATABLE READ".'),
    

    I think we need to have a link to something that explains what this is and the text needs finessing to be more user friendly. I also think the text "Do not set by Drupal" needs works to. It's always better to have a positive than a negative. So something "Leave to database server configuration" would be better.

  3. +++ b/core/modules/mysql/mysql.install
    @@ -41,6 +41,31 @@ function mysql_requirements($phase) {
    +        'value' => (!$missing_primary_key) ? "Primary keys exist" : "Primary keys missing",
    

    Need to use translatable strings here. I'm not convinced this requirement should be shown if there are no missing primary keys. But also should we be doing this check if the isolation level is set to REPEATABLE-READ?

    Also if you do have tables without a primary key what can you do? If they are your custom modules then fine but if they are a contrib module or core you going to need to be told to create an issue.

larowlan’s picture

Also if you do have tables without a primary key what can you do? If they are your custom modules then fine but if they are a contrib module or core you going to need to be told to create an issue.

Yes, excellent point, I think we need to provide a way for the site owner to action this.

sahil.goyal’s picture

Status: Needs work » Needs review
FileSize
23.29 KB
2.24 KB

Addressed the comment #219 as commented some changes required, so upload patch against #218 and inter_diff file along with it. Please review.

needs-review-queue-bot’s picture

Status: Needs review » Needs work
FileSize
86 bytes

The Needs Review Queue Bot tested this issue. It no longer applies to Drupal core. Therefore, this issue status is now "Needs work".

Apart from a re-roll or rebase, this issue may need more work to address feedback in the issue or MR comments. To progress an issue, incorporate this feedback as part of the process of updating the issue. This helps other contributors to know what is outstanding.

Consult the Drupal Contributor Guide to find step-by-step guides for working with issues.

acbramley’s picture

Status: Needs work » Needs review
FileSize
23.29 KB

Reroll of #221, there was a slight fuzz on the phpstan baseline changes.

I think we are still missing all changes in #219.2 and 219.3

needs-review-queue-bot’s picture

Status: Needs review » Needs work
FileSize
86 bytes

The Needs Review Queue Bot tested this issue. It no longer applies to Drupal core. Therefore, this issue status is now "Needs work".

Apart from a re-roll or rebase, this issue may need more work to address feedback in the issue or MR comments. To progress an issue, incorporate this feedback as part of the process of updating the issue. This helps other contributors to know what is outstanding.

Consult the Drupal Contributor Guide to find step-by-step guides for working with issues.

acbramley’s picture

Status: Needs work » Needs review
FileSize
23.29 KB

Lets try that again....

needs-review-queue-bot’s picture

Status: Needs review » Needs work
FileSize
86 bytes

The Needs Review Queue Bot tested this issue. It no longer applies to Drupal core. Therefore, this issue status is now "Needs work".

Apart from a re-roll or rebase, this issue may need more work to address feedback in the issue or MR comments. To progress an issue, incorporate this feedback as part of the process of updating the issue. This helps other contributors to know what is outstanding.

Consult the Drupal Contributor Guide to find step-by-step guides for working with issues.

acbramley’s picture

Huh?! These patches are being rolled directly from 10.1.x-dev. I don't know how they keep failing to apply.

jungle’s picture

Rejected hunks, FYI

diff a/core/phpstan-baseline.neon b/core/phpstan-baseline.neon  (rejected hunks)
@@ -870,6 +870,11 @@ parameters:
                        count: 1
                        path: modules/block_content/tests/src/Functional/BlockContentTypeTest.php
 
+               -
+                       message: "#^Variable \\$goto in isset\\(\\) always exists and is not nullable\\.$#"
+                       count: 1
+                       path: modules/block_content/tests/src/Functional/PageEditTest.php
+
                -
                        message: "#^Call to deprecated constant REQUEST_TIME\\: Deprecated in drupal\\:8\\.3\\.0 and is removed from drupal\\:11\\.0\\.0\\. Use \\\\Drupal\\:\\:time\\(\\)\\-\\>getRequestTime\\(\\); $#"
                        count: 2
@@ -2175,11 +2180,6 @@ parameters:
                        count: 2
                        path: modules/system/tests/modules/entity_test/entity_test.install
 
-               -
-                       message: "#^Variable \\$goto in isset\\(\\) always exists and is not nullable\\.$#"
-                       count: 1
-                       path: modules/system/tests/src/Functional/Menu/AssertBreadcrumbTrait.php
-
                -
                        message: "#^Call to deprecated constant REQUEST_TIME\\: Deprecated in drupal\\:8\\.3\\.0 and is removed from drupal\\:11\\.0\\.0\\. Use \\\\Drupal\\:\\:time\\(\\)\\-\\>getRequestTime\\(\\); $#"
                        count: 1
@@ -3005,11 +3005,6 @@ parameters:
                        count: 12
                        path: tests/Drupal/KernelTests/Core/Cache/GenericCacheBackendUnitTestBase.php
 
-               -
-                       message: "#^Variable \\$expected_driver might not be defined\\.$#"
-                       count: 2
-                       path: tests/Drupal/KernelTests/Core/Database/DriverSpecificKernelTestBase.php
-
                -
                        message: "#^Relying on entity queries to check access by default is deprecated in drupal\\:9\\.2\\.0 and an error will be thrown from drupal\\:10\\.0\\.0\\. Call \\\\Drupal\\\\Core\\\\Entity\\\\Query\\\\QueryInterface\\:\\:accessCheck\\(\\) with TRUE or FALSE to specify whether access should be checked\\.$#"
                        count: 2

Rerolled from #223

jungle’s picture

Status: Needs work » Needs review
FileSize
22.46 KB
683 bytes
acbramley’s picture

Hmm, very weird. It looks like my local clone was stuck on a commit from Feb 7. Even updating the remote didn't work. Cloning fresh has fixed it, however #229 still doesn't apply

EDIT: git apply does work, patch -p1 doesn't
EDIT 2: Fixed by installing gpatch brew install gpatch

jungle’s picture

[DELETED]

smustgrave’s picture

Status: Needs review » Needs work
Issue tags: +Needs Review Queue Initiative

Patch #229 no longer cleanly applies to D10.

karishmaamin’s picture

Status: Needs work » Needs review
FileSize
20.08 KB

Re-rolled patch at #229 against 10.1.x.

apaderno’s picture

Status: Needs review » Needs work
gidarai’s picture

Fixed patch where custom commands were failing

gidarai’s picture

Added different warnings for different isolation levels and when primary keys are missing it shows the corresponding tables that do not have a primary key.

Updated the testIsolationLevelWarningNotDisplaying test to check if the proper warnings/errors are displaying on the status report page.

gidarai’s picture

daffie’s picture

  1. +++ b/core/modules/mysql/mysql.install
    @@ -31,13 +31,44 @@ function mysql_requirements($phase) {
    +      $missing_primary_key = FALSE;
    ...
    +          $missing_primary_key = TRUE;
    

    This variable is not necessary. We can also use empty($tables_missing_primary_key).

  2. +++ b/core/modules/mysql/mysql.install
    @@ -31,13 +31,44 @@ function mysql_requirements($phase) {
    +      $tables_missing_key = [];
    

    Can we rename this variable to $tables_missing_primary_key

  3. +++ b/core/modules/mysql/src/Driver/Database/mysql/Connection.php
    @@ -203,6 +203,15 @@ public static function open(array &$connection_options = []) {
    +    if (!empty($connection_options['isolation_level']) &&
    +      in_array($connection_options['isolation_level'],
    +        ['READ COMMITTED', 'REPEATABLE READ'], TRUE)) {
    

    Can we put these 3 lines on a single line. Can we test $connection_options['isolation_level'] with strtoupper(). Then when a user sets the variable with "read committed" it also works.

  4. +++ b/core/modules/mysql/tests/src/Functional/RequirementsTest.php
    @@ -81,7 +119,7 @@ public function testIsolationLevelWarningNotDisplaying() {
    diff --git a/core/modules/mysql/tests/src/Functional/mysql/PrimaryKeyExistsTest.php b/core/modules/mysql/tests/src/Functional/mysql/PrimaryKeyExistsTest.php
    

    This test is no longer necessary.

  5. +++ b/core/phpstan-baseline.neon
    @@ -2955,11 +2955,6 @@ parameters:
    -		-
    -			message: "#^Variable \\$expected_driver might not be defined\\.$#"
    -			count: 2
    -			path: tests/Drupal/KernelTests/Core/Database/DriverSpecificKernelTestBase.php
    -
    
    +++ b/core/tests/Drupal/KernelTests/Core/Database/DriverSpecificKernelTestBase.php
    @@ -31,6 +31,7 @@ protected function setUp(): void {
    +    $expected_driver = '';
    

    This change is a good one, only out of scope for this issue.

  6. +++ b/core/phpstan-baseline.neon
    @@ -2955,11 +2955,6 @@ parameters:
    diff --git a/core/tests/Drupal/FunctionalTests/Core/Database/DriverSpecificBrowserTestBase.php b/core/tests/Drupal/FunctionalTests/Core/Database/DriverSpecificBrowserTestBase.php
    
    +++ b/core/tests/Drupal/KernelTests/Core/Database/DriverSpecificKernelTestBase.php
    @@ -31,6 +31,7 @@ protected function setUp(): void {
    diff --git a/core/tests/Drupal/Tests/DriverSpecificTrait.php b/core/tests/Drupal/Tests/DriverSpecificTrait.php
    

    This change is also out of scope for this issue.

gidarai’s picture

gidarai’s picture

FileSize
18.91 KB
1.23 KB

Fix custom commands failing

gidarai’s picture

FileSize
18.85 KB
620 bytes

moved $isolation_level line up

harivansh’s picture

gidarai’s picture

@harivansh you seem to have made changes that will break the code as you're trying an array to string conversion. Furthermore the if statement checks if the missing primary key array is not empty to determine whether you are missing primary keys. Please take a better look at the issue next time before making changes and test them accordingly.

The last submitted patch, 242: 1650930-240.patch, failed testing. View results

daffie’s picture

Status: Needs review » Reviewed & tested by the community

All code changes look good to me.
Testing has been added.
The IS and the CR are in order.
For me it is RTBC.

larowlan’s picture

Status: Reviewed & tested by the community » Needs work
  1. +++ b/core/modules/mysql/mysql.install
    @@ -33,11 +33,41 @@ function mysql_requirements($phase) {
    +      $description = 'This site is using the database transaction level "READ COMMITTED".';
    ...
    +        $description = 'This site is using the database transaction level "READ COMMITTED". ' . $tables_missing_primary_key_text;
    ...
    +          $description .= ' ' . $tables_missing_primary_key_text;
    ...
    +        $description = 'This site is using the database transaction level "' . $isolation_level . '". This database transaction level is not supported by Drupal. The recommended database transaction level for Drupal is "READ COMMITTED".';
    

    Unless I'm mistaken, these strings cannot be translated - but should be able to be.

  2. +++ b/core/modules/mysql/mysql.install
    @@ -33,11 +33,41 @@ function mysql_requirements($phase) {
    +      elseif ($isolation_level == 'READ-UNCOMMITTED' || $isolation_level == 'SERIALIZED') {
    

    Should this just be else? do we need to specify the unsupported levels?

  3. +++ b/core/modules/mysql/src/Driver/Database/mysql/Install/Tasks.php
    @@ -174,6 +174,19 @@ public function getFormOptions(array $database) {
    +        'DO NOT SET BY DRUPAL' => $this->t('Do not set by Drupal'),
    

    I think this should be 'Do not set using Drupal' rather than by. Happy to hear otherwise/counter arguments.

  4. +++ b/core/modules/mysql/src/Driver/Database/mysql/Install/Tasks.php
    @@ -174,6 +174,19 @@ public function getFormOptions(array $database) {
    +      '#description' => $this->t('The recommended database transaction level for Drupal is "READ COMMITTED". By selecting the option "READ COMMITTED" Drupal will set the transaction level on every page request. If possible, we recommend to set database transaction level globally. In that case select the option "DO NOT SET BY DRUPAL". For more information, see the <a href=":performance_doc">setting MySQL transaction isolation level</a> page.', [
    

    DO NOT SET BY DRUPAL is the key, not the displayed user string, we should use the displayed user string

  5. +++ b/core/modules/mysql/tests/src/Functional/RequirementsTest.php
    @@ -64,12 +65,49 @@ public function testIsolationLevelWarningNotDisplaying() {
    +    // Check the message is not a warning.
    +    $this->drupalGet('admin/reports/status');
    +    $elements = $this->xpath('//details[@class="system-status-report__entry"]//div[contains(text(), :text)]', [
    +      ':text' => 'This site is using the database transaction level "READ COMMITTED". For this to work properly all tables should have a primary key. The following table(s) do no have a primary key:',
    +    ]);
    +    $this->assertCount(1, $elements);
    +    $this->assertStringStartsWith('READ-COMMITTED', $elements[0]->getParent()->getText());
    +    // Ensure it is an error.
    +    $this->assertStringContainsString('error', $elements[0]->getParent()->getParent()->find('css', 'summary')->getAttribute('class'));
    

    I think we should assert the output contains test_table_without_primary_key so we don't get a false positive here from some other future test module with a table that has no primary key

mstrelan’s picture

Re: 246.2

It can't be else because that would also catch $isolation_level == 'READ-COMMITTED' where there are no tables missing primary keys which should not be an error or warning.

longwave’s picture

Status: Needs work » Needs review
FileSize
17.99 KB
10.63 KB

Addressed #246. I made all the strings translatable, refactored the if statement in mysql_requirements(), simplified the descriptions a bit, changed the "Do not set by Drupal" text to "Use database default", and updated the tests to match.

daffie’s picture

Status: Needs review » Reviewed & tested by the community

All remarks of @larowlan have been addressed.
Back to RTBC.

  • catch committed 60bd67fa on 10.1.x
    Issue #1650930 by mkalkbrenner, gidarai, daffie, JeroenT, murilohp,...

  • catch committed dd368add on 10.1.x
    Issue #1650930 by mkalkbrenner, gidarai, daffie, JeroenT, murilohp,...
catch’s picture

Issue summary: View changes
Status: Reviewed & tested by the community » Fixed

Re-read the patch end to end and also the interdiff in #248. I think this is in a good spot, and it's good timing to get it into 10.1.x before the alpha.

Also double checked our docs for the connections options in default.settings.php, and those are still fine without any changes. This patch is just adding installer support and improved status report messages, everything else has been prepared in other issues in the meantime.

Fixed these two nits on commit:

  1. +++ b/core/modules/migrate_drupal_ui/tests/src/Functional/MigrateUpgradeTestBase.php
    @@ -301,6 +301,8 @@ protected function getCredentials() {
         $connection_options = array_intersect_key($connection_options, $form + $form['advanced_options']);
    +    // Remove isolation_level since that options is not configurable in the UI.
    +    unset($connection_options['isolation_level']);
    

    option

  2. +++ b/core/modules/migrate_drupal_ui/tests/src/Functional/d7/FilePathTest.php
    @@ -127,6 +127,8 @@ public function testFilePath(string $file_private_path, string $file_public_path
         $connection_options = array_intersect_key($connection_options, $form + $form['advanced_options']);
    +    // Remove isolation_level since that options is not configurable in the UI.
    +    unset($connection_options['isolation_level']);
    

    option

Did my best with commit credits, but that's challenging with 250 comments from dozens of people, so apologies for anything overlooked.

Committed/pushed to 10.1.x, thanks!

catch’s picture

Issue summary: View changes
longwave’s picture

Issue tags: +10.1.0 release notes

Tagging for release notes.

daffie’s picture

Great that this is fixed!
It took us only 10 years. ;-)

Thanks to everybody who made this happen!

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.

neclimdul’s picture

#3362726: READ COMMITTED requirement check doesn't account for database views

Filed a follow up because the requirements check doesn't take into account how mysql treats views as a table so registers warning/error if you have any in your database because a view can't have a primary key.

larowlan’s picture

Nicolas Bouteille’s picture

Hello everyone,

My website currently still uses the REPEATABLE READ transaction isolation level.
I would like to switch to READ COMMITED.
I read this documentation :
https://www.drupal.org/docs/getting-started/system-requirements/setting-...
From what I understand, for best performance "The preferred way to change the transaction isolation level" is to set the Global value on the server side with
SET GLOBAL transaction_isolation='READ-COMMITTED';
However, if I'm not mistaken, this global variable is shared between all databases of the server. Plus updating this variable needs to be done with a super user.
Where I work, we have multiple databases of different websites on the same server. Plus my database user cannot update this variable. So if I decided to update this variable globally, I would need to coordinate with all other websites and ask IT to do it for me.

For that reason, updating it in settings.php would be easier for me.
In the documentation, the syntax presented in settings.php is the following:

'init_commands' => [
    'isolation_level' => 'SET SESSION tx_isolation=\'READ-COMMITTED\'',
  ],

BUT there's a warning: "Adding the setting of the transaction isolation level to the init commands in the settings.php file has the disadvantage that on every page request the transaction isolation level is set. That is an extra database call for every page request!"
Since my goal here is to improve performances, after reading this, configuring this in settings.php was not an option for me anymore.

But then I realized that I actually read here that "The transaction isolation level can be set in the database settings array in setttings.php" and that "Drupal now sets the READ COMMITTED transaction isolation level by default for new installs on MySQL."

So I wondered… if setting the isolation level inside settings.php is bad for performance because it redefines the level for each db request, how come Drupal now sets it by default inside settings.php??

I actually created a brand new Drupal website recently, and it uses indeed READ COMMITED. So I went to the settings.php but to my surprise, the way the isolation level is defined is not

'init_commands' => [
    'isolation_level' => 'SET SESSION tx_isolation=\'READ-COMMITTED\'',
  ],

but simply :
'isolation_level' => 'READ COMMITTED',

Here are my many questions:

Is the documentation outdated?

About this new syntax ('isolation_level' => 'READ COMMITTED',): does it have better performance than the 'init_commands' SET SESSION tx_isolation=\'READ-COMMITTED\ syntax? Or is it just a simpler way to write it but in the end it does the exact same thing and has the same drawback of setting the isolation level for each session?

How bad is it to actually set the transaction isolation level on every page request?
Should I do all I can to set the transaction isolation level globally on my database server and avoid to set it in settings.php whenever possible? Or I should not care about this because I won't see a difference...?

In settings.php, is the parameter 'isolation_level' => 'READ COMMITTED', added to every new Drupal site? Or is there a check that is made to the server at site creation and 'isolation_level' => 'READ COMMITTED' is added to settings.php only if the global isolation level of the server is not already READ COMMITTED?

What happens if the parameter 'isolation_level' => 'READ COMMITTED' is still present on settings.php but the DB serveur global isolation level is updated to READ COMMITED later on. Will the isolation level still be redefined for each session no matter what, for nothing? In other words, once we set READ COMMITED globally on server side, should we remove the 'isolation_level' => 'READ COMMITTED' line from settings.php to improve performances?

Thank you in advance for your feedback :)