Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changes/34116-cert-source-deadlocks
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fixed MySQL deadlocks when multiple hosts are updating their certificates in host vitals at the same time.
52 changes: 43 additions & 9 deletions server/datastore/mysql/host_certificates.go
Original file line number Diff line number Diff line change
Expand Up @@ -340,22 +340,56 @@ func replaceHostCertsSourcesDB(ctx context.Context, tx sqlx.ExtContext, toReplac
return nil
}

// Sort by host_certificate_id to ensure consistent lock ordering and prevent deadlocks
slices.SortFunc(toReplaceSources, func(a, b *fleet.HostCertificateRecord) int {
if a.ID != b.ID {
if a.ID < b.ID {
return -1
}
return 1
}
// Secondary sort by source/username for determinism
if a.Source != b.Source {
return strings.Compare(string(a.Source), string(b.Source))
}
return strings.Compare(a.Username, b.Username)
})

// Build unique certificate IDs for deletion (already sorted from above)
certIDs := make([]uint, 0, len(toReplaceSources))
for _, source := range toReplaceSources {
certIDs = append(certIDs, source.ID)
var lastID uint
for i, source := range toReplaceSources {
// Deduplicate: only add if this ID is different from the last one
if i == 0 || source.ID != lastID {
certIDs = append(certIDs, source.ID)
lastID = source.ID
}
}

// delete existing sources
stmtDelete := `DELETE FROM host_certificate_sources WHERE host_certificate_id IN (?)`
stmtDelete, args, err := sqlx.In(stmtDelete, certIDs)
// Check if any sources exist before deleting to avoid unnecessary gap locks
stmtCheck := `SELECT EXISTS(SELECT 1 FROM host_certificate_sources WHERE host_certificate_id IN (?))`
stmtCheck, args, err := sqlx.In(stmtCheck, certIDs)
if err != nil {
return ctxerr.Wrap(ctx, err, "building delete host cert sources query")
return ctxerr.Wrap(ctx, err, "building check host cert sources query")
}
var exists bool
if err := sqlx.GetContext(ctx, tx, &exists, stmtCheck, args...); err != nil {
return ctxerr.Wrap(ctx, err, "checking if host cert sources exist")
}
if _, err := tx.ExecContext(ctx, stmtDelete, args...); err != nil {
return ctxerr.Wrap(ctx, err, "deleting host cert sources")

// Only delete if sources exist
if exists {
stmtDelete := `DELETE FROM host_certificate_sources WHERE host_certificate_id IN (?)`
stmtDelete, args, err := sqlx.In(stmtDelete, certIDs)
if err != nil {
return ctxerr.Wrap(ctx, err, "building delete host cert sources query")
}
if _, err := tx.ExecContext(ctx, stmtDelete, args...); err != nil {
return ctxerr.Wrap(ctx, err, "deleting host cert sources")
}
}

// create incoming sources
// Insert new sources
stmtInsert := `
INSERT INTO host_certificate_sources (
host_certificate_id,
Expand Down
Loading