TTransactional.php 3.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106
  1. <?php
  2. declare(strict_types=1);
  3. /*
  4. * @copyright 2022 Christoph Wurst <christoph@winzerhof-wurst.at>
  5. *
  6. * @author 2022 Christoph Wurst <christoph@winzerhof-wurst.at>
  7. *
  8. * @license GNU AGPL version 3 or any later version
  9. *
  10. * This program is free software: you can redistribute it and/or modify
  11. * it under the terms of the GNU Affero General Public License as
  12. * published by the Free Software Foundation, either version 3 of the
  13. * License, or (at your option) any later version.
  14. *
  15. * This program is distributed in the hope that it will be useful,
  16. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  17. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  18. * GNU Affero General Public License for more details.
  19. *
  20. * You should have received a copy of the GNU Affero General Public License
  21. * along with this program. If not, see <http://www.gnu.org/licenses/>.
  22. */
  23. namespace OCP\AppFramework\Db;
  24. use OC\DB\Exceptions\DbalException;
  25. use OCP\DB\Exception;
  26. use OCP\IDBConnection;
  27. use Throwable;
  28. use function OCP\Log\logger;
  29. /**
  30. * Helper trait for transactional operations
  31. *
  32. * @since 24.0.0
  33. */
  34. trait TTransactional {
  35. /**
  36. * Run an atomic database operation
  37. *
  38. * - Commit if no exceptions are thrown, return the callable result
  39. * - Revert otherwise and rethrows the exception
  40. *
  41. * @template T
  42. * @param callable $fn
  43. * @psalm-param callable():T $fn
  44. * @param IDBConnection $db
  45. *
  46. * @return mixed the result of the passed callable
  47. * @psalm-return T
  48. *
  49. * @throws Exception for possible errors of commit or rollback or the custom operations within the closure
  50. * @throws Throwable any other error caused by the closure
  51. *
  52. * @since 24.0.0
  53. * @see https://docs.nextcloud.com/server/latest/developer_manual/basics/storage/database.html#transactions
  54. */
  55. protected function atomic(callable $fn, IDBConnection $db) {
  56. $db->beginTransaction();
  57. try {
  58. $result = $fn();
  59. $db->commit();
  60. return $result;
  61. } catch (Throwable $e) {
  62. $db->rollBack();
  63. throw $e;
  64. }
  65. }
  66. /**
  67. * Wrapper around atomic() to retry after a retryable exception occurred
  68. *
  69. * Certain transactions might need to be retried. This is especially useful
  70. * in highly concurrent requests where a deadlocks is thrown by the database
  71. * without waiting for the lock to be freed (e.g. due to MySQL/MariaDB deadlock
  72. * detection)
  73. *
  74. * @template T
  75. * @param callable $fn
  76. * @psalm-param callable():T $fn
  77. * @param IDBConnection $db
  78. * @param int $maxRetries
  79. *
  80. * @return mixed the result of the passed callable
  81. * @psalm-return T
  82. *
  83. * @throws Exception for possible errors of commit or rollback or the custom operations within the closure
  84. * @throws Throwable any other error caused by the closure
  85. *
  86. * @since 27.0.0
  87. */
  88. protected function atomicRetry(callable $fn, IDBConnection $db, int $maxRetries = 3): mixed {
  89. for ($i = 0; $i < $maxRetries; $i++) {
  90. try {
  91. return $this->atomic($fn, $db);
  92. } catch (DbalException $e) {
  93. if (!$e->isRetryable() || $i === ($maxRetries - 1)) {
  94. throw $e;
  95. }
  96. logger('core')->warning('Retrying operation after retryable exception.', [ 'exception' => $e ]);
  97. }
  98. }
  99. }
  100. }