jenkins-bot has submitted this change and it was merged. Change subject: Avoid deadlock patterns in cx_corpora updates ......................................................................
Avoid deadlock patterns in cx_corpora updates Bug: T134245 Change-Id: I975a2f4698ec0b7045222bcd43df3fac00dcc9bb (cherry picked from commit 31a5bc98b63312ea6d91eae88855837db5dd9160) --- M api/ApiContentTranslationSave.php M includes/Translation.php M includes/TranslationStorageManager.php M includes/TranslationUnit.php 4 files changed, 62 insertions(+), 27 deletions(-) Approvals: Thcipriani: Looks good to me, approved jenkins-bot: Verified diff --git a/api/ApiContentTranslationSave.php b/api/ApiContentTranslationSave.php index 26491bd..15f3000 100644 --- a/api/ApiContentTranslationSave.php +++ b/api/ApiContentTranslationSave.php @@ -56,7 +56,7 @@ $this->translation = $this->saveTranslation( $params ); $translationUnits = $this->getTranslationUnits( $params['content'] ); - $this->saveTranslationUnits( $translationUnits ); + $this->saveTranslationUnits( $translationUnits, $this->translation ); $validationResults = $this->validateTranslationUnits( $translationUnits ); $translationId = $this->translation->getTranslationId(); @@ -126,6 +126,11 @@ return $results; } + /** + * @param array $params + * @return Translation + * @throws UsageException + */ protected function saveTranslation( array $params ) { $translation = Translation::find( $params['from'], @@ -211,9 +216,14 @@ return $translationUnits; } - protected function saveTranslationUnits( $translationUnits ) { + /** + * @param $translationUnits + * @param Translation $translation Recently saved parent translation object + */ + protected function saveTranslationUnits( $translationUnits, Translation $translation ) { + $newTranslation = $translation->lastSaveWasCreate(); foreach ( $translationUnits as $translationUnit ) { - TranslationStorageManager::save( $translationUnit ); + TranslationStorageManager::save( $translationUnit, $newTranslation ); } } diff --git a/includes/Translation.php b/includes/Translation.php index b947bfe..fe4400d 100644 --- a/includes/Translation.php +++ b/includes/Translation.php @@ -5,6 +5,8 @@ namespace ContentTranslation; class Translation { + private $lastSaveWasCreate = false; + function __construct( $translation ) { $this->translation = $translation; } @@ -78,8 +80,6 @@ * translation exists and chooses either of create or update actions. */ public function save() { - $freshTranslation = false; - $existingTranslation = Translation::find( $this->translation['sourceLanguage'], $this->translation['targetLanguage'], @@ -88,6 +88,7 @@ if ( $existingTranslation === null ) { $this->create(); + $this->lastSaveWasCreate = true; } else { $options = []; if ( $existingTranslation->translation['status'] === 'deleted' ) { @@ -97,9 +98,17 @@ } $this->translation['id'] = $existingTranslation->getTranslationId(); $this->update( $options ); + $this->lastSaveWasCreate = false; } } + /** + * @return bool Whether the last save() call on this object instance made a new row + */ + public function lastSaveWasCreate() { + return $this->lastSaveWasCreate; + } + /* * @param string $sourceLanguage * @param string $targetLanguage diff --git a/includes/TranslationStorageManager.php b/includes/TranslationStorageManager.php index d6f310a..fdcd1a0 100644 --- a/includes/TranslationStorageManager.php +++ b/includes/TranslationStorageManager.php @@ -76,29 +76,46 @@ * If the record exist, update it, otherwise create. * * @param TranslationUnit $translationUnit + * @param bool $newTranslation Whether these are for a brand new Translation record */ - public static function save( TranslationUnit $translationUnit ) { - $db = Database::getConnection( DB_MASTER ); + public static function save( TranslationUnit $translationUnit, $newTranslation ) { + $dbw = Database::getConnection( DB_MASTER ); - $db->doAtomicSection( __METHOD__, function ( $db ) use ( $translationUnit ) { - $conditions = [ - 'cxc_translation_id' => $translationUnit->getTranslationId(), - 'cxc_section_id' => $translationUnit->getSectionId(), - 'cxc_origin' => $translationUnit->getOrigin() - ]; - // (At least attempt to) avoid inserting duplicate records in case - // of race condition between the select query and the insert query, - // resulting duplicate record error. - $options = [ 'FOR UPDATE' ]; + $dbw->doAtomicSection( + __METHOD__, + function ( \IDatabase $dbw ) use ( $translationUnit, $newTranslation ) { + if ( $newTranslation ) { + // T134245: brand new translations can also insert corpora data in the same + // request. The doFind() query uses only a subset of a unique cx_corpora index, + // causing SH gap locks. Worse, is that the leftmost values comes from the + // auto-incrementing translation_id. This puts gap locks on the range of + // (MAX(cxc_translation_id),+infinity), which could make the whole API prone + // to deadlocks and timeouts. Bypass this problem by remembering if the parent + // translation row is brand new and skipping doFind() in such cases. + $existing = false; + } else { + // Update the lastest row if there is one instead of making a new one + $conditions = [ + 'cxc_translation_id' => $translationUnit->getTranslationId(), + 'cxc_section_id' => $translationUnit->getSectionId(), + 'cxc_origin' => $translationUnit->getOrigin() + ]; + // Note that the only caller of this method will have already locked the + // parent Translation row, serializing simultaneous duplicate submissions at + // this point. Without that row lock, the two transaction might both acquire + // SH gap locks in doFind() and then deadlock in create() trying to get IX gap + // locks (if no duplicate rows were found). + $options = [ 'FOR UPDATE' ]; + $existing = self::doFind( $dbw, $conditions, $options, __METHOD__ ); + } - $existing = self::doFind( $db, $conditions, $options, __METHOD__ ); - - if ( $existing ) { - self::update( $db, $translationUnit, $existing->getTimestamp() ); - } else { - self::create( $db, $translationUnit ); + if ( $existing ) { + self::update( $dbw, $translationUnit, $existing->getTimestamp() ); + } else { + self::create( $dbw, $translationUnit ); + } } - } ); + ); } /** @@ -121,7 +138,7 @@ return self::doFind( $db, $conditions, [], __METHOD__ ); } - private static function doFind( $db, $conditions, $options, $method ) { + private static function doFind( \IDatabase $db, $conditions, $options, $method ) { $options['ORDER BY'] = 'cxc_timestamp DESC'; $row = $db->selectRow( 'cx_corpora', '*', $conditions, $method, $options ); @@ -131,6 +148,5 @@ } return null; - } } diff --git a/includes/TranslationUnit.php b/includes/TranslationUnit.php index 08f9238..2efd2ee 100644 --- a/includes/TranslationUnit.php +++ b/includes/TranslationUnit.php @@ -37,7 +37,7 @@ } /** - * @param stdClass $row + * @param \stdClass $row * @return TranslationUnit */ public static function newFromRow( $row ) { -- To view, visit https://gerrit.wikimedia.org/r/305188 To unsubscribe, visit https://gerrit.wikimedia.org/r/settings Gerrit-MessageType: merged Gerrit-Change-Id: I975a2f4698ec0b7045222bcd43df3fac00dcc9bb Gerrit-PatchSet: 1 Gerrit-Project: mediawiki/extensions/ContentTranslation Gerrit-Branch: wmf/1.28.0-wmf.14 Gerrit-Owner: KartikMistry <kartik.mis...@gmail.com> Gerrit-Reviewer: Aaron Schulz <asch...@wikimedia.org> Gerrit-Reviewer: Thcipriani <tcipri...@wikimedia.org> Gerrit-Reviewer: jenkins-bot <> _______________________________________________ MediaWiki-commits mailing list MediaWiki-commits@lists.wikimedia.org https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits