Skip to content

Commit ced67f6

Browse files
committed
PHPLIB-468: Implement Convenient API for Transactions
1 parent c69a715 commit ced67f6

File tree

2 files changed

+162
-0
lines changed

2 files changed

+162
-0
lines changed

src/Operation/WithTransaction.php

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
<?php
2+
3+
namespace MongoDB\Operation;
4+
5+
use Exception;
6+
use MongoDB\Driver\Exception\RuntimeException;
7+
use MongoDB\Driver\Session;
8+
use function call_user_func;
9+
use function time;
10+
11+
/**
12+
* @internal
13+
*/
14+
class WithTransaction
15+
{
16+
/** @var callable */
17+
private $callback;
18+
19+
/** @var array */
20+
private $transactionOptions;
21+
22+
/**
23+
* @see Session::startTransaction for supported transaction options
24+
*
25+
* @param callable $callback A callback that will be invoked within the transaction
26+
* @param array $transactionOptions Additional options that are passed to Session::startTransaction
27+
*/
28+
public function __construct(callable $callback, array $transactionOptions = [])
29+
{
30+
$this->callback = $callback;
31+
$this->transactionOptions = $transactionOptions;
32+
}
33+
34+
/**
35+
* Execute the operation in the given session
36+
*
37+
* This helper takes care of retrying the commit operation or the entire
38+
* transaction if an error occurs.
39+
*
40+
* If the commit fails because of an UnknownTransactionCommitResult error, the
41+
* commit is retried without re-invoking the callback.
42+
* If the commit fails because of a TransientTransactionError, the entire
43+
* transaction will be retried. In this case, the callback will be invoked
44+
* again. It is important that the logic inside the callback is idempotent.
45+
*
46+
* In case of failures, the commit or transaction are retried until 120 seconds
47+
* from the initial call have elapsed. After that, no retries will happen and
48+
* the helper will throw the last exception received from the driver.
49+
*
50+
* @see Client::startSession
51+
*
52+
* @param Session $session A session object as retrieved by Client::startSession
53+
* @return void
54+
* @throws RuntimeException for driver errors while committing the transaction
55+
* @throws Exception for any other errors, including those thrown in the callback
56+
*/
57+
public function execute(Session $session)
58+
{
59+
$startTime = time();
60+
61+
while (true) {
62+
$session->startTransaction($this->transactionOptions);
63+
64+
try {
65+
call_user_func($this->callback, $session);
66+
} catch (Exception $e) {
67+
if ($session->isInTransaction()) {
68+
$session->abortTransaction();
69+
}
70+
71+
if ($e instanceof RuntimeException &&
72+
$e->hasErrorLabel('TransientTransactionError') &&
73+
! $this->isTransactionTimeLimitExceeded($startTime)
74+
) {
75+
continue;
76+
}
77+
78+
throw $e;
79+
}
80+
81+
if (! $session->isInTransaction()) {
82+
// Assume callback intentionally ended the transaction
83+
return;
84+
}
85+
86+
while (true) {
87+
try {
88+
$session->commitTransaction();
89+
} catch (RuntimeException $e) {
90+
if ($e->getCode() !== 50 /* MaxTimeMSExpired */ &&
91+
$e->hasErrorLabel('UnknownTransactionCommitResult') &&
92+
! $this->isTransactionTimeLimitExceeded($startTime)
93+
) {
94+
// Retry committing the transaction
95+
continue;
96+
}
97+
98+
if ($e->hasErrorLabel('TransientTransactionError') &&
99+
! $this->isTransactionTimeLimitExceeded($startTime)
100+
) {
101+
// Restart the transaction, invoking the callback again
102+
continue 2;
103+
}
104+
105+
throw $e;
106+
}
107+
108+
// Commit was successful
109+
break;
110+
}
111+
112+
// Transaction was successful
113+
break;
114+
}
115+
}
116+
117+
/**
118+
* Returns whether the time limit for retrying transactions in the convenient transaction API has passed
119+
*
120+
* @param int $startTime The time the transaction was started
121+
* @return bool
122+
*/
123+
private function isTransactionTimeLimitExceeded($startTime)
124+
{
125+
return time() - $startTime >= 120;
126+
}
127+
}

src/functions.php

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,13 @@
1717

1818
namespace MongoDB;
1919

20+
use Exception;
2021
use MongoDB\BSON\Serializable;
2122
use MongoDB\Driver\Server;
2223
use MongoDB\Driver\Session;
2324
use MongoDB\Exception\InvalidArgumentException;
25+
use MongoDB\Exception\RuntimeException;
26+
use MongoDB\Operation\WithTransaction;
2427
use ReflectionClass;
2528
use ReflectionException;
2629
use function end;
@@ -338,3 +341,35 @@ function create_field_path_type_map(array $typeMap, $fieldPath)
338341

339342
return $typeMap;
340343
}
344+
345+
/**
346+
* Execute a callback within a transaction in the given session
347+
*
348+
* This helper takes care of retrying the commit operation or the entire
349+
* transaction if an error occurs.
350+
*
351+
* If the commit fails because of an UnknownTransactionCommitResult error, the
352+
* commit is retried without re-invoking the callback.
353+
* If the commit fails because of a TransientTransactionError, the entire
354+
* transaction will be retried. In this case, the callback will be invoked
355+
* again. It is important that the logic inside the callback is idempotent.
356+
*
357+
* In case of failures, the commit or transaction are retried until 120 seconds
358+
* from the initial call have elapsed. After that, no retries will happen and
359+
* the helper will throw the last exception received from the driver.
360+
*
361+
* @see Client::startSession
362+
* @see Session::startTransaction for supported transaction options
363+
*
364+
* @param Session $session A session object as retrieved by Client::startSession
365+
* @param callable $callback A callback that will be invoked within the transaction
366+
* @param array $transactionOptions Additional options that are passed to Session::startTransaction
367+
* @return void
368+
* @throws RuntimeException for driver errors while committing the transaction
369+
* @throws Exception for any other errors, including those thrown in the callback
370+
*/
371+
function with_transaction(Session $session, callable $callback, array $transactionOptions = [])
372+
{
373+
$operation = new WithTransaction($callback, $transactionOptions);
374+
$operation->execute($session);
375+
}

0 commit comments

Comments
 (0)