Skip to content

Commit 298837f

Browse files
committed
(fix): retry transient transaction failures in Mongo adapter
1 parent 5608004 commit 298837f

1 file changed

Lines changed: 50 additions & 22 deletions

File tree

src/Database/Adapter/Mongo.php

Lines changed: 50 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,12 @@
1212
use Utopia\Database\DateTime;
1313
use Utopia\Database\Document;
1414
use Utopia\Database\Exception as DatabaseException;
15+
use Utopia\Database\Exception\Authorization as AuthorizationException;
16+
use Utopia\Database\Exception\Conflict as ConflictException;
1517
use Utopia\Database\Exception\Duplicate as DuplicateException;
18+
use Utopia\Database\Exception\Limit as LimitException;
19+
use Utopia\Database\Exception\Relationship as RelationshipException;
20+
use Utopia\Database\Exception\Restricted as RestrictedException;
1621
use Utopia\Database\Exception\Structure as StructureException;
1722
use Utopia\Database\Exception\Timeout as TimeoutException;
1823
use Utopia\Database\Exception\Transaction as TransactionException;
@@ -132,33 +137,56 @@ public function withTransaction(callable $callback): mixed
132137
return $callback();
133138
}
134139

135-
try {
136-
$this->startTransaction();
137-
$result = $callback();
138-
$this->commitTransaction();
139-
return $result;
140-
} catch (\Throwable $action) {
140+
$sleep = 50_000; // 50 milliseconds
141+
$retries = 2;
142+
143+
for ($attempts = 0; $attempts <= $retries; $attempts++) {
141144
try {
142-
$this->rollbackTransaction();
143-
} catch (\Throwable) {
144-
// Throw the original exception, not the rollback one
145-
// Since if it's a duplicate key error, the rollback will fail,
146-
// and we want to throw the original exception.
147-
} finally {
148-
// Ensure state is cleaned up even if rollback fails
149-
if ($this->session) {
150-
try {
151-
$this->client->endSessions([$this->session]);
152-
} catch (\Throwable $endSessionError) {
153-
// Ignore errors when ending session during error cleanup
145+
$this->startTransaction();
146+
$result = $callback();
147+
$this->commitTransaction();
148+
return $result;
149+
} catch (\Throwable $action) {
150+
try {
151+
$this->rollbackTransaction();
152+
} catch (\Throwable) {
153+
// Throw the original exception, not the rollback one
154+
// Since if it's a duplicate key error, the rollback will fail,
155+
// and we want to throw the original exception.
156+
} finally {
157+
// Ensure state is cleaned up even if rollback fails
158+
if ($this->session) {
159+
try {
160+
$this->client->endSessions([$this->session]);
161+
} catch (\Throwable $endSessionError) {
162+
// Ignore errors when ending session during error cleanup
163+
}
154164
}
165+
$this->inTransaction = 0;
166+
$this->session = null;
155167
}
156-
$this->inTransaction = 0;
157-
$this->session = null;
158-
}
159168

160-
throw $action;
169+
if (
170+
$action instanceof DuplicateException ||
171+
$action instanceof RestrictedException ||
172+
$action instanceof AuthorizationException ||
173+
$action instanceof RelationshipException ||
174+
$action instanceof ConflictException ||
175+
$action instanceof LimitException
176+
) {
177+
throw $action;
178+
}
179+
180+
if ($attempts < $retries) {
181+
\usleep($sleep * ($attempts + 1));
182+
continue;
183+
}
184+
185+
throw $action;
186+
}
161187
}
188+
189+
throw new TransactionException('Failed to execute transaction');
162190
}
163191

164192
public function startTransaction(): bool

0 commit comments

Comments
 (0)