-
Notifications
You must be signed in to change notification settings - Fork 55
feat: bound MySQL writes with session lock-wait timeout #892
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -32,6 +32,7 @@ public function setTimeout(int $milliseconds, string $event = Database::EVENT_AL | |
|
|
||
| $this->timeout = $milliseconds; | ||
|
|
||
| // Bound reads: the max_execution_time optimizer hint only applies to SELECTs. | ||
| $this->before($event, 'timeout', function ($sql) use ($milliseconds) { | ||
| return \preg_replace( | ||
| pattern: '/SELECT/', | ||
|
|
@@ -40,6 +41,13 @@ public function setTimeout(int $milliseconds, string $event = Database::EVENT_AL | |
| limit: 1 | ||
| ); | ||
| }); | ||
|
|
||
| // Bound writes: hints are ignored by INSERT/UPDATE/DDL, so cap how long a | ||
| // statement waits on row (InnoDB) and metadata locks before failing fast. | ||
| // This is a connection-scoped floor: Pool re-applies it on each checkout, | ||
| // so it tracks the latest timeout without a paired reset to leak through. | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This line now mutates connection session state, but the pooled adapter does not delegate |
||
| $seconds = \max(1, (int) \ceil($milliseconds / 1000)); | ||
| $this->getPDO()->exec("SET SESSION innodb_lock_wait_timeout = {$seconds}, SESSION lock_wait_timeout = {$seconds}"); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This
Comment on lines
+49
to
+50
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. These session variables are changed on the connection, but ArtifactsRepro: PHP script emulating setTimeout + clearTimeout PDO flow
Repro: failing test output showing session variables stuck at 1s after clearTimeout()
Comment on lines
36
to
+50
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The
Comment on lines
+47
to
+50
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This comment relies on the pool reapplying the timeout on checkout, but ArtifactsRepro: PHP script demonstrating pool timeout state drift with mock adapters
|
||
| } | ||
|
|
||
| /** | ||
|
|
@@ -162,6 +170,11 @@ protected function processException(PDOException $e): \Exception | |
| return new TimeoutException('Query timed out', $e->getCode(), $e); | ||
| } | ||
|
|
||
| // Lock wait timeout (blocked write released by innodb_lock_wait_timeout/lock_wait_timeout) | ||
| if ($e->getCode() === 'HY000' && isset($e->errorInfo[1]) && $e->errorInfo[1] === 1205) { | ||
| return new TimeoutException('Query timed out', $e->getCode(), $e); | ||
| } | ||
|
|
||
| // Functional index dependency | ||
| if ($e->getCode() === 'HY000' && isset($e->errorInfo[1]) && $e->errorInfo[1] === 3837) { | ||
| return new DependencyException('Attribute cannot be deleted because it is used in an index', $e->getCode(), $e); | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This statement repeats
SESSIONin the second assignment. MySQL scopes following unqualified assignments from the latest scope keyword, so this can fail before the timeout is installed. WhensetTimeout()is called, callers can receive a SQL syntax error instead of getting a bounded query timeout.