Skip to content

Return COMMIT errors from PostgreSQL ExecTx and Close conn on BeginTx error - #452

Open
chlunde wants to merge 1 commit into
crossplane-contrib:masterfrom
chlunde:extx-commit-error
Open

chlunde wants to merge 1 commit into
crossplane-contrib:masterfrom
chlunde:extx-commit-error

Conversation

@chlunde

@chlunde chlunde commented Sep 6, 2026

Copy link
Copy Markdown
Collaborator

Description of your changes

There were two subtle bugs in ExecTx, the return value from Commit was assigned to err, which would only work if err was a named return variable. Additionally, the *sql.Conn handle including a goroutine would leak if BeginTx failed, like if there's an issue with the providerconfig.

To add testing we extract the main code to execTx and test using sqlmock. To remove the bug and make the code easier to understand and more standard, follow the code structure more like used in stdlib docs: https://pkg.go.dev/database/sql#Tx.Prepare

Fixes issue found by agent inspection.

I have:

  • Read and followed Crossplane's contribution process.
  • Run make reviewable to ensure this PR is ready for review.

How has this code been tested

Unit test, and also manual testing in e2e (not added here to keep runtime low) with a trigger causing the COMMIT to fail. It didn't return any error without this fix, synced became true.

ExecTx commits inside a deferred closure that assigns to err. With an
unnamed result the assignment lands after `return err` has copied the
value, so a failed COMMIT is reported as success. The reconciler then
records a grant, schema, default privilege or role setting that the
server rolled back.

Commit at the end and defer only Rollback, the shape from the
database/sql docs. Nothing assigns to the result in a defer, so the bug
cannot recur. Move the transaction body into execTx, which takes a
*sql.DB, so sqlmock can drive it.

Closing the *sql.DB moves in front of BeginTx. Before, Close lived in
the closure deferred only after BeginTx succeeded, so a failed BeginTx
(bad credentials, unreachable host, cancelled context) leaked the pool
on every reconcile.

Testing: TestExecTx drives execTx with sqlmock through Begin, Exec,
later-Exec and Commit failures. The Commit case is the regression:

    m.ExpectBegin()
    m.ExpectExec("GRANT USAGE").WillReturnResult(ok)
    m.ExpectCommit().WillReturnError(errBoom)
    // want errBoom; the previous code returned nil

Reproduced on postgres:18 through the e2e harness. A GRANT cannot fail
at COMMIT by itself, so an event trigger writes a row whose deferred
constraint trigger raises when the transaction commits:

    CREATE EVENT TRIGGER audit_ddl ON ddl_command_end
      WHEN TAG IN ('GRANT') EXECUTE FUNCTION audit_ddl();
      -- audit_ddl(): INSERT INTO ddl_audit(tag) VALUES (tg_tag)

    CREATE CONSTRAINT TRIGGER reject_at_commit AFTER INSERT ON ddl_audit
      DEFERRABLE INITIALLY DEFERRED FOR EACH ROW
      EXECUTE FUNCTION reject_at_commit();
      -- reject_at_commit(): RAISE EXCEPTION 'extx: COMMIT rejected'

A Grant of USAGE on a schema in that database then reconciles as:

    BEGIN; REVOKE ...; GRANT USAGE ON SCHEMA extx_schema TO extx_role;
    COMMIT;  -- ERROR: extx: COMMIT rejected

Before: ExecTx returned nil, Synced=True, the privilege was absent and
Create re-ran every poll. After: Synced=False with reason ReconcileError
and the COMMIT error in the message, on both the cluster-scoped and the
namespaced pass.

Signed-off-by: Carl Henrik Lunde <chlunde@ifi.uio.no>
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.

1 participant