Skip to content

ext/pdo: persistent_shutdown run while the connection is still shared. Only call it for the last PDO object, like the rollback before it. - #24465

Open
devnexen wants to merge 4 commits into
php:PHP-8.6from
devnexen:gh24461
Open

devnexen wants to merge 4 commits into
php:PHP-8.6from
devnexen:gh24461

Conversation

@devnexen

Copy link
Copy Markdown
Member

No description provided.

Only call it for the last PDO object, like the rollback before it.
…and plans.

Fix php#24461

Keep the rest of DISCARD ALL, and reset the connection if it fails.
@devnexen devnexen changed the title Gh24461 ext/pdo: persistent_shutdown run while the connection is still shared. Only call it for the last PDO object, like the rollback before it. Oct 20, 2026
…repared.

Track prepared statement names on the handle and close the leftovers.
@devnexen
devnexen marked this pull request as ready for review October 20, 2026 21:44
@KentarouTakeda

Copy link
Copy Markdown
Contributor

Thank you. I had reached the same conclusion after looking at the issue.

Although I haven’t reviewed every single change, the approach to handling multiple persistent connections and the criteria for determining which state to discard both make sense to me.

@ArtUkrainskiy ArtUkrainskiy left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

There's one leak with libpq < 17. If a statement is closed inside an aborted transaction, DEALLOCATE fails and the statement stays on the server. The new reset doesn't clean it up anymore, DISCARD ALL did. Fix and repro inline, tests pass with ASan otherwise.

Comment thread ext/pdo_pgsql/pgsql_statement.c Outdated
Comment on lines +193 to +198
PQclear(res);
}

if (H->prepared_stmts) {
zend_hash_str_del(H->prepared_stmts, S->stmt_name, strlen(S->stmt_name));
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

If the close fails, the name is still removed here, so the reset never closes that statement. I'd only remove it when the close worked:

Suggested change
PQclear(res);
}
if (H->prepared_stmts) {
zend_hash_str_del(H->prepared_stmts, S->stmt_name, strlen(S->stmt_name));
}
if (H->prepared_stmts && PQresultStatus(res) == PGRES_COMMAND_OK) {
zend_hash_str_del(H->prepared_stmts, S->stmt_name, strlen(S->stmt_name));
}
PQclear(res);
}

This prints 1 now and 0 with the change:

$opts = [PDO::ATTR_PERSISTENT => true, PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_EMULATE_PREPARES => false];
$pdo = new PDO($dsn, null, null, $opts);
$pdo->beginTransaction();
$stmt = $pdo->prepare("SELECT 'leak'");
$stmt->execute();
try { $pdo->exec('SELECT 1/0'); } catch (PDOException $e) {}
$stmt = null;
$pdo->rollBack();
$pdo = null;
$pdo = new PDO($dsn, null, null, $opts);
echo $pdo->query("SELECT count(*) FROM pg_prepared_statements WHERE statement = 'SELECT ''leak'''")->fetchColumn();

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

applied your suggestions let me know if all work out for you

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thanks, works for me!

Co-authored-by: Artem Ukrainskiy <art.ukrainskiy@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

pdo_pgsql: DISCARD ALL on persistent connection cleanup drops PgBouncer's prepared statements (+52% CPU per request)

3 participants