You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
chore(db): evaluate reconstructing purchase_history rows lost to the VARCHAR(20) account_id truncation (#1603) #146
PR LeanerCloud/cloud-commitments-cli#1677 (closing LeanerCloud/cloud-commitments-cli#1603) widens purchase_history.account_id from VARCHAR(20) to VARCHAR(255), stopping further audit-row loss on Azure and long-project-ID GCP purchases. It deliberately does not attempt to recover the rows already lost, because those rows were never inserted — there is nothing in purchase_history to repair.
However, the lost rows are not entirely unreconstructable, and this issue tracks evaluating that.
Current behaviour
When SavePurchaseHistory failed with SQLSTATE 22001, recordHistoryAuditGap (internal/purchase/execution.go) stamped a marker on the execution rather than losing the event entirely:
history_write_failed: commitment <commitmentID> purchased but its history record failed to save
So for each lost row, purchase_executions still holds:
error — the marker, carrying the provider-side commitment ID
recommendations (JSONB) — the RecommendationRecord set, including per-rec PurchaseID, Purchased, service/region/resource-type/term/payment and cost fields
That is most of the purchase_history column set. A backfill that joins the audit-gap marker to the matching recommendation entry is plausible.
Steps to verify the gap
SELECT execution_id, error FROM purchase_executions
WHERE error LIKE'%history_write_failed%';
Each match is a real, billed commitment with no purchase_history row. Note the double-sided wildcard: recordHistoryAuditGap appends the marker via appendErrNote, so it is not necessarily at the start of the field when the execution already carried an error (a retry successor carrying a predecessor's error, or a second gap in the same execution).
Expected behaviour
Either:
A one-shot reconciliation path (migration or operator job) that reconstructs purchase_history rows from the execution's recommendations JSON for every history_write_failed marker, writing only fields it can source and leaving the rest explicitly NULL rather than fabricating them; or
An explicit decision that reconstruction is not wanted, documented in known_issues/, so operators know the History view is permanently incomplete for those commitments.
Option 1 must not fabricate values. Fields not derivable from the execution row (e.g. revocation_window_closes_at) should stay NULL, and reconstructed rows should be distinguishable from natively-written ones.
Proposed fix
Scope out before implementing:
Confirm the recommendations JSON reliably carries PurchaseID matching the commitment ID in the marker (internal/purchase/execution.go, aggregatePurchaseOutcomes).
Decide how to represent account_id for the reconstructed row — the execution's cloud_account_id resolves to cloud_accounts.external_id, which is the value that failed to insert in the first place.
Medium. No further loss occurs once LeanerCloud/cloud-commitments-cli#1677 merges; this is recovery of historical data. Impact scales with how long an affected Azure/GCP deployment has been purchasing.
Summary
PR LeanerCloud/cloud-commitments-cli#1677 (closing LeanerCloud/cloud-commitments-cli#1603) widens
purchase_history.account_idfromVARCHAR(20)toVARCHAR(255), stopping further audit-row loss on Azure and long-project-ID GCP purchases. It deliberately does not attempt to recover the rows already lost, because those rows were never inserted — there is nothing inpurchase_historyto repair.However, the lost rows are not entirely unreconstructable, and this issue tracks evaluating that.
Current behaviour
When
SavePurchaseHistoryfailed withSQLSTATE 22001,recordHistoryAuditGap(internal/purchase/execution.go) stamped a marker on the execution rather than losing the event entirely:So for each lost row,
purchase_executionsstill holds:error— the marker, carrying the provider-side commitment IDrecommendations(JSONB) — theRecommendationRecordset, including per-recPurchaseID,Purchased, service/region/resource-type/term/payment and cost fieldscloud_account_id(where set),plan_id,source,executed_at,executed_by_user_idThat is most of the
purchase_historycolumn set. A backfill that joins the audit-gap marker to the matching recommendation entry is plausible.Steps to verify the gap
Each match is a real, billed commitment with no
purchase_historyrow. Note the double-sided wildcard:recordHistoryAuditGapappends the marker viaappendErrNote, so it is not necessarily at the start of the field when the execution already carried an error (a retry successor carrying a predecessor's error, or a second gap in the same execution).Expected behaviour
Either:
purchase_historyrows from the execution'srecommendationsJSON for everyhistory_write_failedmarker, writing only fields it can source and leaving the rest explicitly NULL rather than fabricating them; orknown_issues/, so operators know the History view is permanently incomplete for those commitments.Option 1 must not fabricate values. Fields not derivable from the execution row (e.g.
revocation_window_closes_at) should stay NULL, and reconstructed rows should be distinguishable from natively-written ones.Proposed fix
Scope out before implementing:
recommendationsJSON reliably carriesPurchaseIDmatching the commitment ID in the marker (internal/purchase/execution.go,aggregatePurchaseOutcomes).account_idfor the reconstructed row — the execution'scloud_account_idresolves tocloud_accounts.external_id, which is the value that failed to insert in the first place.purchase_history.cloud_account_idfrom(provider, external_id)), which touches the same column pair and cannot repair rows that were never inserted.References
Severity
Medium. No further loss occurs once LeanerCloud/cloud-commitments-cli#1677 merges; this is recovery of historical data. Impact scales with how long an affected Azure/GCP deployment has been purchasing.