TTransactional.php 2.4 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788
  1. <?php
  2. declare(strict_types=1);
  3. /**
  4. * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
  5. * SPDX-License-Identifier: AGPL-3.0-or-later
  6. */
  7. namespace OCP\AppFramework\Db;
  8. use OC\DB\Exceptions\DbalException;
  9. use OCP\DB\Exception;
  10. use OCP\IDBConnection;
  11. use Throwable;
  12. use function OCP\Log\logger;
  13. /**
  14. * Helper trait for transactional operations
  15. *
  16. * @since 24.0.0
  17. */
  18. trait TTransactional {
  19. /**
  20. * Run an atomic database operation
  21. *
  22. * - Commit if no exceptions are thrown, return the callable result
  23. * - Revert otherwise and rethrows the exception
  24. *
  25. * @template T
  26. * @param callable $fn
  27. * @psalm-param callable():T $fn
  28. * @param IDBConnection $db
  29. *
  30. * @return mixed the result of the passed callable
  31. * @psalm-return T
  32. *
  33. * @throws Exception for possible errors of commit or rollback or the custom operations within the closure
  34. * @throws Throwable any other error caused by the closure
  35. *
  36. * @since 24.0.0
  37. * @see https://docs.nextcloud.com/server/latest/developer_manual/basics/storage/database.html#transactions
  38. */
  39. protected function atomic(callable $fn, IDBConnection $db) {
  40. $db->beginTransaction();
  41. try {
  42. $result = $fn();
  43. $db->commit();
  44. return $result;
  45. } catch (Throwable $e) {
  46. $db->rollBack();
  47. throw $e;
  48. }
  49. }
  50. /**
  51. * Wrapper around atomic() to retry after a retryable exception occurred
  52. *
  53. * Certain transactions might need to be retried. This is especially useful
  54. * in highly concurrent requests where a deadlocks is thrown by the database
  55. * without waiting for the lock to be freed (e.g. due to MySQL/MariaDB deadlock
  56. * detection)
  57. *
  58. * @template T
  59. * @param callable $fn
  60. * @psalm-param callable():T $fn
  61. * @param IDBConnection $db
  62. * @param int $maxRetries
  63. *
  64. * @return mixed the result of the passed callable
  65. * @psalm-return T
  66. *
  67. * @throws Exception for possible errors of commit or rollback or the custom operations within the closure
  68. * @throws Throwable any other error caused by the closure
  69. *
  70. * @since 27.0.0
  71. */
  72. protected function atomicRetry(callable $fn, IDBConnection $db, int $maxRetries = 3): mixed {
  73. for ($i = 0; $i < $maxRetries; $i++) {
  74. try {
  75. return $this->atomic($fn, $db);
  76. } catch (DbalException $e) {
  77. if (!$e->isRetryable() || $i === ($maxRetries - 1)) {
  78. throw $e;
  79. }
  80. logger('core')->warning('Retrying operation after retryable exception.', [ 'exception' => $e ]);
  81. }
  82. }
  83. }
  84. }