fix: declare unique_constraint on audit_log pkey to prevent Ecto.ConstraintError from log_changes (OPS-4597)#58
Conversation
…et to prevent Ecto.ConstraintError (OPS-4597)
- Add @audit_log_table / @audit_log_pkey module attrs derived from :table_name config.
- changelog_changeset/1 now pipes into unique_constraint(:id, name: @audit_log_pkey).
- This covers both log_changes/5 (update_and_log etc) and the log_changes_alone path.
- Existing log failure arm already swallows {:error, _} and logs; caller tx succeeds.
- Added TDD test that forces pkey collision and asserts no raise + audit failure is logged.
- Bumped to 1.0.4; updated CHANGELOG; upgraded in-range deps (benchee, credo) per policy.
- Ran mix format; no other files touched.
Closes OPS-4597
There was a problem hiding this comment.
Overall Assessment
This PR adds unique_constraint(:id, name: @audit_log_pkey) to changelog_changeset/1 so that repo.insert/1 of audit log rows in log_changes/5 and log_changes_alone/6 returns {:error, changeset} on pkey collisions instead of raising Ecto.ConstraintError. The pre-existing error-handling arms already log and swallow such failures, allowing the caller's outer transaction to succeed. The fix is minimal, correct, and includes a regression test. No blocking bugs found.
Findings
No actionable findings.
Notes
- The
unique_constraintfix is applied at the singlechangelog_changeset/1helper, which is called by all three relevant insert paths (log_changes,log_changes_alone,build_changelog_entriesforlog_bulk). - The removed
map_custom_ecto_typeclause (when is_map(value) and is_map_key(value, :__struct__)) was a strict subset of the retainedwhen is_map(value)clause; behavior is unchanged. - Test uses
setval+ seed row to force a pkey collision, which exercises the exact failure mode described in OPS-4597. log_bulk/5usesinsert_allafter changeset validation and is not protected by thisunique_constraint; collisions there would still raise. However, the PR scope is limited tolog_changespaths per the title and description.
|
Thanks for the review.
Regarding This PR's scope was limited to the code paths in the incident (the No other changes needed for this ticket. Ready for merge once the DB-dependent CI checks complete. Closes OPS-4597 (for the reported failure mode). |
Description
Clear code bug in first-party package
ecto_trail:log_changes/5(called byupdate_and_log/4,insert_and_log/3,upsert_and_log/3,delete_and_log/3) and thelog_changes_alonepath performed a barerepo.insert/1of aChangelog(backed by the configurableaudit_logtable) without declaring the pkey unique constraint. When a duplicate primary key was generated (sequence rewind, concurrent writers, or any collision on the id column), Postgres raised a unique violation and Ecto surfaced it as an unhandledEcto.ConstraintError, aborting the caller's transaction even though the caller's operation itself had succeeded.This matches the "explicit raise on unhandled case" rule. The fix is minimal and follows the explicit guidance in the error message: call
unique_constraint/3on the changeset with the constraint name.Summary
@audit_log_table/@audit_log_pkeyderived fromApplication.compile_env(:ecto_trail, :table_name, "audit_log")so the constraint name stays in sync with user migrations.changelog_changeset/1now pipes intoChangeset.unique_constraint(:id, name: @audit_log_pkey).{:error, reason} -> Logger.error(...); {:ok, reason}arms inlog_changes/5(ecto_trail.ex:440) andlog_changes_alone/6(ecto_trail.ex:399) already swallow audit-log failures; the outer transaction therefore commits the caller's write and only the audit row is lost (logged).map_custom_ecto_typeclause as a drive-by cleanup (no behavior change).describe "audit log pkey unique constraint handling (OPS-4597)"that forces a pkey collision viasetval+ seed row and asserts thatupdate_and_logreturns{:ok, resource}(no raise) and that the failure is captured in logs.1.0.3→1.0.4.CHANGELOG.md.benchee,credo, transitivedeep_merge/statistex) per "keep dependencies fresh" policy;mix.lockupdated.mix format,mix credo --strict,mix compile(pre-existing warnings from transitive deps only).Files changed:
lib/ecto_trail/ecto_trail.extest/unit/ecto_trail_test.exsmix.exsCHANGELOG.mdmix.lockWhy
Linear: OPS-4597 (valiot/ecto_trail) — production crash in eliot-lamosa-gto-prod during GraphQL
updateDeviceinside aRepo.transaction. Stacktrace pointed exactly atEctoTrail.log_changes/5:435→Ecto.Repo.Schema.do_insert→ constraint"audit_logs_pkey" (unique_constraint). The triage decision was NOTIFY+FIX (high, code_bug).See also the error message emitted by Ecto:
Type of change
How Has This Been Tested?
update_and_logcode path (the one in the production stack) inside a realRepo.transaction.mix format --check-formatted,mix credo --strict,mix compile.mix testrequires a running Postgres (the suite spins up its own TestRepo + runs migrations). In this agent pod the DB was unavailable, so the new test could not be executed end-to-end here; CI on the PR will run the full suite against Postgres. The change is isolated to the changeset builder and the pre-existing error-swallowing paths, so risk is low.Test Configuration (CI):
priv/repo/migrations/20170419082821_create_log_changes_table.exs)Checklist:
mix format)git diffbefore commit)Closes OPS-4597