Skip to content

Conversation

@abnegate
Copy link
Member

@abnegate abnegate commented Sep 16, 2025

Summary by CodeRabbit

  • Bug Fixes

    • Improved database reliability by automatically reconnecting after a lost connection and retrying the operation when safe.
    • Preserves transaction integrity by surfacing errors during active transactions rather than retrying.
    • Adds warning logs for visibility into reconnection attempts and failures.
  • Tests

    • Added unit test to verify retry behavior after a lost database connection, ensuring the operation succeeds following a successful reconnect.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Sep 16, 2025

Walkthrough

Introduces resilient error handling in the PDO wrapper to detect lost connections, log, conditionally reconnect, and retry non-transactional calls. Updates unit tests to validate the retry-on-lost-connection behavior using mocks and reflection to simulate failure on first call and success after reconnection.

Changes

Cohort / File(s) Summary
PDO wrapper: lost-connection handling
src/Database/PDO.php
Wraps dynamic PDO calls in try-catch; on lost-connection errors, logs via Console, checks transaction state, attempts reconnect, and retries if not in a transaction; otherwise rethrows. Adds Console import. Non-connection errors rethrow. No public API signature changes.
Unit tests for retry flow
tests/unit/PDOTest.php
Activates and rewrites test to simulate a lost connection on first call and success after reconnect; injects mocked inner \PDO, stubs reconnect, and asserts a single retry succeeds. Adds testLostConnectionRetriesCall.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor Caller
  participant Wrapper as PDO Wrapper
  participant PDO as Inner \PDO
  participant Conn as Connection
  participant Log as Console

  Caller->>Wrapper: invoke dynamic PDO method (e.g., query)
  Wrapper->>PDO: call method
  PDO-->>Wrapper: Throwable (lost connection)
  Wrapper->>Conn: hasError(e)?
  Conn-->>Wrapper: true
  Wrapper->>Wrapper: inTransaction?
  alt In transaction
    Wrapper-->>Caller: rethrow error
  else Not in transaction
    Wrapper->>Log: warn(lost connection, will reconnect)
    Wrapper->>Wrapper: reconnect()
    Wrapper->>PDO: retry method
    PDO-->>Wrapper: result
    Wrapper-->>Caller: result
  end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

A hiccup in the tunnel—plink! it falls,
I twitch my ears, then bolster broken calls.
Hop, reconnect, and try once more,
The query finds its burrowed door.
One bounce back, no transaction fright—
Carrots secured, the tests delight. 🥕✨

Pre-merge checks and finishing touches

❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 25.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
Title Check ❓ Inconclusive The title "1.x" is just a branch/version label and does not describe the changes in this PR, which implement resilient reconnection logic in the PDO wrapper and enable a lost-connection unit test; it is therefore too vague for a reviewer to understand the primary change at a glance. Because it conveys no meaningful information about the contents of the changeset, the title is inconclusive with respect to summarizing the work. The current title should be replaced with a short, specific sentence describing the main change. Rename the PR to a concise, descriptive title that summarizes the main change, for example "PDO: retry on lost DB connection and add unit test" or "Add resilient PDO reconnection handling and test"; include the issue/PR number or brief context if helpful, and avoid branch names or non-descriptive labels like "1.x".
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
✨ Finishing touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch 1.x

Tip

👮 Agentic pre-merge checks are now available in preview!

Pro plan users can now enable pre-merge checks in their settings to enforce checklists before merging PRs.

  • Built-in checks – Quickly apply ready-made checks to enforce title conventions, require pull request descriptions that follow templates, validate linked issues for compliance, and more.
  • Custom agentic checks – Define your own rules using CodeRabbit’s advanced agentic capabilities to enforce organization-specific policies and workflows. For example, you can instruct CodeRabbit’s agent to verify that API documentation is updated whenever API schema files are modified in a PR. Note: Upto 5 custom checks are currently allowed during the preview period. Pricing for this feature will be announced in a few weeks.

Please see the documentation for more information.

Example:

reviews:
  pre_merge_checks:
    custom_checks:
      - name: "Undocumented Breaking Changes"
        mode: "warning"
        instructions: |
          Pass/fail criteria: All breaking changes to public APIs, CLI flags, environment variables, configuration keys, database schemas, or HTTP/GraphQL endpoints must be documented in the "Breaking Change" section of the PR description and in CHANGELOG.md. Exclude purely internal or private changes (e.g., code not exported from package entry points or explicitly marked as internal).

Please share your feedback with us on this Discord post.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@abnegate abnegate marked this pull request as ready for review September 16, 2025 13:33
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (3)
src/Database/PDO.php (1)

6-6: Prefer PSR‑3 logger injection over static Console usage.

Static Console writes to stdout and couples the library to CLI. Consider accepting an optional Psr\Log\LoggerInterface (default NullLogger) and log via it.

tests/unit/PDOTest.php (2)

44-81: Make the “lost connection” trigger deterministic against Connection::hasError().

Using new \Exception("Lost connection") depends on detector heuristics. Throw a driver-typical PDOException (e.g., “server has gone away”) to avoid false negatives.

Apply this diff:

-            ->will($this->onConsecutiveCalls(
-                $this->throwException(new \Exception("Lost connection")),
-                $pdoStatementMock
-            ));
+            ->will($this->onConsecutiveCalls(
+                $this->throwException(new \PDOException('SQLSTATE[HY000]: General error: 2006 MySQL server has gone away')),
+                $pdoStatementMock
+            ));

44-81: Add coverage: no retry when in a transaction and for commit/rollBack.

Stub inTransaction() to true and assert:

  • reconnect() is called once,
  • query() is not retried,
  • the original exception is rethrown.
    Also add a test that commit() is not retried on lost connection.

I can draft these tests if you want.

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f499a46 and 066e2bd.

📒 Files selected for processing (2)
  • src/Database/PDO.php (2 hunks)
  • tests/unit/PDOTest.php (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
src/Database/PDO.php (4)
src/Database/Connection.php (2)
  • Connection (7-38)
  • hasError (22-37)
src/Database/Adapter.php (1)
  • inTransaction (366-369)
src/Database/Adapter/SQL.php (1)
  • reconnect (153-156)
src/Database/Database.php (1)
  • reconnect (1225-1228)
tests/unit/PDOTest.php (1)
src/Database/PDO.php (1)
  • PDO (13-143)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Setup & Build Docker Image

Comment on lines +45 to +65
try {
return $this->pdo->{$method}(...$args);
} catch (\Throwable $e) {
if (Connection::hasError($e)) {
Console::warning('[Database] ' . $e->getMessage());
Console::warning('[Database] Lost connection detected. Reconnecting...');

$inTransaction = $this->pdo->inTransaction();

// Attempt to reconnect
$this->reconnect();

// If we weren't in a transaction, also retry the query
// In a transaction we can't retry as the state is attached to the previous connection
if (!$inTransaction) {
return $this->pdo->{$method}(...$args);
}
}

throw $e;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Avoid unsafe retries; guard inTransaction() and never retry commit/rollBack.

  • Don’t retry stateful methods like commit/rollBack; risk of double-commit or inconsistent state.
  • inTransaction() can itself throw on a dead handle; default conservatively (no retry) if it does.

Apply this diff inside the catch block:

         } catch (\Throwable $e) {
             if (Connection::hasError($e)) {
                 Console::warning('[Database] ' . $e->getMessage());
                 Console::warning('[Database] Lost connection detected. Reconnecting...');
-
-                $inTransaction = $this->pdo->inTransaction();
+                // Methods we will never retry to avoid duplicating side effects.
+                $methodLower = \strtolower($method);
+                $isRetryableMethod = !\in_array($methodLower, ['commit', 'rollback'], true);
+
+                // Safely detect transaction state; if uncertain, treat as in-transaction (no retry).
+                $inTransaction = true;
+                try {
+                    $inTransaction = $this->pdo->inTransaction();
+                } catch (\Throwable $_) {
+                    // keep $inTransaction = true
+                }
 
                 // Attempt to reconnect
                 $this->reconnect();
 
                 // If we weren't in a transaction, also retry the query
                 // In a transaction we can't retry as the state is attached to the previous connection
-                if (!$inTransaction) {
+                if ($isRetryableMethod && !$inTransaction) {
                     return $this->pdo->{$method}(...$args);
                 }
             }
 
             throw $e;
         }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
try {
return $this->pdo->{$method}(...$args);
} catch (\Throwable $e) {
if (Connection::hasError($e)) {
Console::warning('[Database] ' . $e->getMessage());
Console::warning('[Database] Lost connection detected. Reconnecting...');
$inTransaction = $this->pdo->inTransaction();
// Attempt to reconnect
$this->reconnect();
// If we weren't in a transaction, also retry the query
// In a transaction we can't retry as the state is attached to the previous connection
if (!$inTransaction) {
return $this->pdo->{$method}(...$args);
}
}
throw $e;
}
try {
return $this->pdo->{$method}(...$args);
} catch (\Throwable $e) {
if (Connection::hasError($e)) {
Console::warning('[Database] ' . $e->getMessage());
Console::warning('[Database] Lost connection detected. Reconnecting...');
// Methods we will never retry to avoid duplicating side effects.
$methodLower = \strtolower($method);
$isRetryableMethod = !\in_array($methodLower, ['commit', 'rollback'], true);
// Safely detect transaction state; if uncertain, treat as in-transaction (no retry).
$inTransaction = true;
try {
$inTransaction = $this->pdo->inTransaction();
} catch (\Throwable $_) {
// keep $inTransaction = true
}
// Attempt to reconnect
$this->reconnect();
// If we weren't in a transaction, also retry the query
// In a transaction we can't retry as the state is attached to the previous connection
if ($isRetryableMethod && !$inTransaction) {
return $this->pdo->{$method}(...$args);
}
}
throw $e;
}

@abnegate abnegate merged commit 732ffef into main Sep 16, 2025
15 checks passed
@abnegate abnegate deleted the 1.x branch September 16, 2025 13:44
@abnegate abnegate restored the 1.x branch September 16, 2025 13:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants