• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

Freegle / Iznik / 4361

16 Apr 2026 08:33AM UTC coverage: 64.234%. First build
4361

Pull #105

circleci

edwh
feat: connection-level deadlock retry with exponential backoff

Add DeadlockRetryConnection — a custom MySqlConnection that
automatically retries deadlocked statements at autocommit level with
exponential backoff (100ms, 200ms, 400ms). Inside explicit transactions,
deadlocks propagate immediately.

This makes ALL database operations deadlock-resilient transparently.
Remove retryOnDeadlock() from TransactionPolicy and simplify
EmailTracking.createForEmail() back to a plain self::create().

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Pull Request #105: fix: batch deadlock resilience, GitHub token auth, and collation migration

8625 of 9992 branches covered (86.32%)

Branch coverage included in aggregate %.

30 of 55 new or added lines in 4 files covered. (54.55%)

71072 of 114080 relevant lines covered (62.3%)

11.04 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

10.71
/iznik-batch/app/Database/DeadlockRetryConnection.php
1
<?php
2

3
namespace App\Database;
4

5
use App\Support\TransactionPolicy;
6
use Closure;
7
use Illuminate\Database\MySqlConnection;
8
use Illuminate\Database\QueryException;
9
use Illuminate\Support\Facades\Log;
10

11
/**
12
 * MySQL connection that automatically retries deadlocked statements at autocommit level.
13
 *
14
 * On a Galera cluster, concurrent single-statement operations (INSERTs, UPDATEs)
15
 * can deadlock even without explicit transactions. At autocommit level, each
16
 * statement is atomic — a deadlocked statement was never applied, so retrying
17
 * is always safe.
18
 *
19
 * Inside explicit transactions, Laravel's parent class already throws immediately
20
 * (the caller must retry the entire transaction, not individual statements).
21
 */
22
class DeadlockRetryConnection extends MySqlConnection
23
{
24
    protected int $deadlockMaxRetries = 3;
25

26
    protected int $deadlockBaseDelayMs = 100;
27

28
    /**
29
     * Handle a query exception.
30
     *
31
     * Extends parent to retry deadlocks at autocommit level with exponential
32
     * backoff, in addition to the existing lost-connection retry.
33
     */
34
    protected function handleQueryException(QueryException $e, $query, $bindings, Closure $callback)
1✔
35
    {
36
        // Parent checks $this->transactions >= 1 and throws — we mirror that.
37
        if ($this->transactions >= 1) {
1✔
38
            throw $e;
1✔
39
        }
40

NEW
41
        if (TransactionPolicy::isDeadlock($e)) {
×
NEW
42
            return $this->retryDeadlockedStatement($e, $query, $bindings, $callback);
×
43
        }
44

45
        // Fall through to parent's lost-connection retry.
NEW
46
        return $this->tryAgainIfCausedByLostConnection(
×
NEW
47
            $e, $query, $bindings, $callback
×
NEW
48
        );
×
49
    }
50

51
    /**
52
     * Retry a deadlocked statement with exponential backoff.
53
     */
NEW
54
    protected function retryDeadlockedStatement(QueryException $e, $query, $bindings, Closure $callback)
×
55
    {
NEW
56
        for ($attempt = 1; $attempt <= $this->deadlockMaxRetries; $attempt++) {
×
NEW
57
            $delayMs = $this->deadlockBaseDelayMs * pow(2, $attempt - 1);
×
58

NEW
59
            Log::warning('DeadlockRetryConnection: retrying deadlocked statement', [
×
NEW
60
                'query' => $query,
×
NEW
61
                'attempt' => $attempt,
×
NEW
62
                'max_retries' => $this->deadlockMaxRetries,
×
NEW
63
                'delay_ms' => $delayMs,
×
NEW
64
            ]);
×
65

NEW
66
            usleep($delayMs * 1000);
×
67

68
            try {
NEW
69
                return $this->runQueryCallback($query, $bindings, $callback);
×
NEW
70
            } catch (QueryException $retryException) {
×
NEW
71
                if (!TransactionPolicy::isDeadlock($retryException)) {
×
NEW
72
                    throw $retryException;
×
73
                }
NEW
74
                $e = $retryException;
×
75
            }
76
        }
77

NEW
78
        Log::error('DeadlockRetryConnection: deadlock persisted after all retries', [
×
NEW
79
            'query' => $query,
×
NEW
80
            'attempts' => $this->deadlockMaxRetries,
×
NEW
81
        ]);
×
82

NEW
83
        throw $e;
×
84
    }
85
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc