Browse Source

enh: Provide atomicRetry method to retry transactions if possible

Signed-off-by: Julius Härtl <jus@bitgrid.net>
Julius Härtl 1 year ago
parent
commit
050c6d53b3
1 changed files with 37 additions and 0 deletions
  1. 37 0
      lib/public/AppFramework/Db/TTransactional.php

+ 37 - 0
lib/public/AppFramework/Db/TTransactional.php

@@ -25,9 +25,11 @@ declare(strict_types=1);
 
 namespace OCP\AppFramework\Db;
 
+use OC\DB\Exceptions\DbalException;
 use OCP\DB\Exception;
 use OCP\IDBConnection;
 use Throwable;
+use function OCP\Log\logger;
 
 /**
  * Helper trait for transactional operations
@@ -66,4 +68,39 @@ trait TTransactional {
 			throw $e;
 		}
 	}
+
+	/**
+	 * Wrapper around atomic() to retry after a retryable exception occurred
+	 *
+	 * Certain transactions might need to be retried. This is especially useful
+	 * in highly concurrent requests where a deadlocks is thrown by the database
+	 * without waiting for the lock to be freed (e.g. due to MySQL/MariaDB deadlock
+	 * detection)
+	 *
+	 * @template T
+	 * @param callable $fn
+	 * @psalm-param callable():T $fn
+	 * @param IDBConnection $db
+	 * @param int $maxRetries
+	 *
+	 * @return mixed the result of the passed callable
+	 * @psalm-return T
+	 *
+	 * @throws Exception for possible errors of commit or rollback or the custom operations within the closure
+	 * @throws Throwable any other error caused by the closure
+	 *
+	 * @since 27.0.0
+	 */
+	protected function atomicRetry(callable $fn, IDBConnection $db, int $maxRetries = 3): mixed {
+		for ($i = 0; $i < $maxRetries; $i++) {
+			try {
+				return $this->atomic($fn, $db);
+			} catch (DbalException $e) {
+				if (!$e->isRetryable() || $i === ($maxRetries - 1)) {
+					throw $e;
+				}
+				logger('core')->warning('Retrying operation after retryable exception.', [ 'exception' => $e ]);
+			}
+		}
+	}
 }