Aditya Oza
07/21/2026, 9:29 PMError 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
The error references "clearing sentinel host_software.last_opened_at values".
Our setup: we have two instances, one is the migration leader that runs fleet prepare db, the other just waits. There are no old Fleet instances running — we've verified zero running EC2 instances. So the only MySQL connections come from fleet prepare db itself.
Since Error 1205 means another transaction is holding a conflicting lock, and nothing else is connected to the DB, the contention seems to be internal to fleet prepare db.
Have you seen this issue before? Any recommendations on how I can get past it? I am open to the idea of deleting records in our sw inventory tables as it will likely get re-populated as hosts publish new info to Fleet server.
Appreciate any input!Unthread
07/22/2026, 11:35 AMAditya Oza
07/22/2026, 3:16 PMAditya Oza
07/22/2026, 3:19 PMUnthread
07/22/2026, 3:39 PMZay Hanlon
07/22/2026, 6:18 PMAditya Oza
07/22/2026, 7:01 PM2026/07/22 18:53:15 FAIL 20260608210432_CleanupSoftwareLastOpenedAtSentinels.go (clearing sentinel host_software.last_opened_at values: Error 1205 (HY000): Lock wait timeout exceeded; try restarting transaction), quitting migration
Do you have any suggestions? Can you think of any other process that might be holding up a lock to this table?Aditya Oza
07/22/2026, 7:04 PMFor community support, we only commit patches to the latest version of Fleet@Zay Hanlon All good here. We are running into the same issue with 4.87.x as well, and so my earlier request isn't relevant anymore.
Zay Hanlon
07/22/2026, 7:07 PMZay Hanlon
07/22/2026, 7:07 PMKathy Satterlee
07/22/2026, 7:16 PMKathy Satterlee
07/22/2026, 7:28 PMSELECT table_rows, data_length, index_length
FROM information_schema.tables
WHERE table_schema = DATABASE() AND table_name = 'host_software';Aditya Oza
07/22/2026, 10:04 PMmysql> SELECT table_rows, data_length, index_length
-> FROM information_schema.tables
-> WHERE table_schema = DATABASE() AND table_name = 'host_software';
+------------+-------------+--------------+
| TABLE_ROWS | DATA_LENGTH | INDEX_LENGTH |
+------------+-------------+--------------+
| 505025579 | 30064787456 | 0 |
+------------+-------------+--------------+
1 row in set (0.00 sec)Aditya Oza
07/23/2026, 2:40 PMLock wait timeout exceeded; try restarting transaction suggests that my process is waiting on something else to release the lock on that table. I'm not sure what might be holding up the lock since we have only one instance performing the DB migration.
To me it appears to be a lock contention problem and not a data volume problem.
@Kathy Satterlee could you share your thoughts?Kathy Satterlee
07/23/2026, 5:47 PMAditya Oza
07/23/2026, 9:43 PMUnthread
07/24/2026, 2:38 PM`-- =====================================================================`
`-- Mitigation for 20260608210432_CleanupSoftwareLastOpenedAtSentinels`
`-- Batches the UPDATE by host_id range to keep each transaction's lock`
`-- footprint small, instead of one full-table scan/update.`
`-- =====================================================================`
`DELIMITER $$`
`DROP PROCEDURE IF EXISTS cleanup_software_last_opened_at_sentinels$$`
`CREATE PROCEDURE cleanup_software_last_opened_at_sentinels(IN batch_size INT)`
`BEGIN`
`DECLARE min_host_id INT UNSIGNED;`
`DECLARE max_host_id INT UNSIGNED;`
`DECLARE range_start INT UNSIGNED;`
`DECLARE range_end INT UNSIGNED;`
`DECLARE rows_affected INT DEFAULT 0;`
`DECLARE total_cleared BIGINT DEFAULT 0;`
`IF batch_size IS NULL OR batch_size <= 0 THEN`
`SET batch_size = 5000;`
`END IF;`
`SELECT MIN(host_id), MAX(host_id) INTO min_host_id, max_host_id FROM host_software;`
`IF min_host_id IS NULL THEN`
`SELECT 'host_software is empty; nothing to do.' AS message;`
`ELSE`
`SET range_start = min_host_id;`
`WHILE range_start <= max_host_id DO`
`SET range_end = range_start + batch_size - 1;`
`UPDATE host_software`
`SET last_opened_at = NULL`
`WHERE last_opened_at = '1980-01-01 00:00:00'`
`AND host_id BETWEEN range_start AND range_end;`
`SET rows_affected = ROW_COUNT();`
`SET total_cleared = total_cleared + rows_affected;`
`IF rows_affected > 0 THEN`
`SELECT CONCAT('host_id ', range_start, '-', range_end,`
`': cleared ', rows_affected, ' row(s)') AS progress;`
`END IF;`
`SET range_start = range_end + 1;`
`-- Small pause between batches to avoid saturating I/O; drop if unnecessary.`
`DO SLEEP(0.05);`
`END WHILE;`
`SELECT CONCAT('Done. Total rows cleared: ', total_cleared) AS summary;`
`END IF;`
`END$$`
`DELIMITER ;`
`-- Adjust batch size (host_id span per iteration) to taste. 5000 is a`
`-- reasonable starting point; lower it if you still see contention.`
`CALL cleanup_software_last_opened_at_sentinels(5000);`
`DROP PROCEDURE cleanup_software_last_opened_at_sentinels;`
`-- =====================================================================`
`-- Verify no sentinel rows remain before marking the migration applied`
`-- =====================================================================`
`SELECT COUNT(*) AS remaining_sentinels`
`FROM host_software`
`WHERE last_opened_at = '1980-01-01 00:00:00';`
`-- Only proceed past this point if remaining_sentinels = 0.`
`-- =====================================================================`
`-- Mark the migration as applied in migration_status_tables`
`-- =====================================================================`
`INSERT INTO migration_status_tables (version_id, is_applied, tstamp)`
`SELECT 20260608210432, 1, NOW()`
`WHERE NOT EXISTS (`
`SELECT 1 FROM migration_status_tables WHERE version_id = 20260608210432`
`);`
`UPDATE migration_status_tables`
`SET is_applied = 1, tstamp = NOW()`
`WHERE version_id = 20260608210432 AND is_applied = 0;`
`-- Confirm`
`SELECT * FROM migration_status_tables WHERE version_id = 20260608210432;`Kathy Satterlee
07/24/2026, 2:40 PMsql
-- =====================================================================
-- Full reset of host_software and dependent aggregate/derived data.
-- Forces a complete software inventory re-collection on next check-in
-- for every host. Run only with servers stopped.
-- =====================================================================
-- 1. Per-host installed-path records are keyed by (host_id, software_id)
-- pairs that live in host_software. Once host_software is cleared,
-- these become orphaned rows with no corresponding inventory entry.
TRUNCATE TABLE host_software_installed_paths;
-- 2. The main host <-> software association table.
-- TRUNCATE (not DELETE) deliberately here — it's a DDL-style
-- operation, not a row-by-row scan/delete, so it avoids the exact
-- lock/scan cost problem from the sentinel migration entirely.
TRUNCATE TABLE host_software;
-- 3. Aggregate host counts per software_id (global + per-team) are
-- now all stale/zero. These have a CHECK (hosts_count > 0)
-- constraint, so the correct reset is to remove the rows entirely
-- rather than try to zero them out. Fleet's periodic host-count
-- calculation cron repopulates these once host_software fills
-- back in from re-collection.
TRUNCATE TABLE software_host_counts;
-- 4. Same idea, but per software TITLE rather than per exact version.
TRUNCATE TABLE software_titles_host_counts;
-- =====================================================================
-- Optional: force a fresh diff cycle rather than relying on osquery's
-- normal schedule to notice the gap on its own.
-- =====================================================================
-- software_updated_at is the diff-gate Fleet uses to decide whether a
-- host's software query results need reprocessing. Resetting it to
-- NULL is a nudge to make sure the next check-in re-ingests fully
-- rather than assuming "nothing changed" if it were to compare against
-- a stale timestamp. I'm flagging this as an assumption on ingestion
-- behavior, not something I've verified against the ingestion code —
-- worth confirming before running if you want to be precise about it.
-- UPDATE hosts SET software_updated_at = NULL;
-- =====================================================================
-- Verification
-- =====================================================================
SELECT
(SELECT COUNT(*) FROM host_software) AS host_software_rows,
(SELECT COUNT(*) FROM host_software_installed_paths) AS installed_paths_rows,
(SELECT COUNT(*) FROM software_host_counts) AS software_host_counts_rows,
(SELECT COUNT(*) FROM software_titles_host_counts) AS software_titles_host_counts_rows;
-- All four should read 0.Kathy Satterlee
07/24/2026, 2:40 PM-- =====================================================================
-- Mitigation for 20260608210432_CleanupSoftwareLastOpenedAtSentinels
-- Batches the UPDATE by host_id range to keep each transaction's lock
-- footprint small, instead of one full-table scan/update.
-- =====================================================================
DELIMITER $$
DROP PROCEDURE IF EXISTS cleanup_software_last_opened_at_sentinels$$
CREATE PROCEDURE cleanup_software_last_opened_at_sentinels(IN batch_size INT)
BEGIN
DECLARE min_host_id INT UNSIGNED;
DECLARE max_host_id INT UNSIGNED;
DECLARE range_start INT UNSIGNED;
DECLARE range_end INT UNSIGNED;
DECLARE rows_affected INT DEFAULT 0;
DECLARE total_cleared BIGINT DEFAULT 0;
IF batch_size IS NULL OR batch_size <= 0 THEN
SET batch_size = 5000;
END IF;
SELECT MIN(host_id), MAX(host_id) INTO min_host_id, max_host_id FROM host_software;
IF min_host_id IS NULL THEN
SELECT 'host_software is empty; nothing to do.' AS message;
ELSE
SET range_start = min_host_id;
WHILE range_start <= max_host_id DO
SET range_end = range_start + batch_size - 1;
UPDATE host_software
SET last_opened_at = NULL
WHERE last_opened_at = '1980-01-01 00:00:00'
AND host_id BETWEEN range_start AND range_end;
SET rows_affected = ROW_COUNT();
SET total_cleared = total_cleared + rows_affected;
IF rows_affected > 0 THEN
SELECT CONCAT('host_id ', range_start, '-', range_end,
': cleared ', rows_affected, ' row(s)') AS progress;
END IF;
SET range_start = range_end + 1;
-- Small pause between batches to avoid saturating I/O; drop if unnecessary.
DO SLEEP(0.05);
END WHILE;
SELECT CONCAT('Done. Total rows cleared: ', total_cleared) AS summary;
END IF;
END$$
DELIMITER ;
-- Adjust batch size (host_id span per iteration) to taste. 5000 is a
-- reasonable starting point; lower it if you still see contention.
CALL cleanup_software_last_opened_at_sentinels(5000);
DROP PROCEDURE cleanup_software_last_opened_at_sentinels;
-- =====================================================================
-- Verify no sentinel rows remain before marking the migration applied
-- =====================================================================
SELECT COUNT(*) AS remaining_sentinels
FROM host_software
WHERE last_opened_at = '1980-01-01 00:00:00';
-- Only proceed past this point if remaining_sentinels = 0.
-- =====================================================================
-- Mark the migration as applied in migration_status_tables
-- =====================================================================
INSERT INTO migration_status_tables (version_id, is_applied, tstamp)
SELECT 20260608210432, 1, NOW()
WHERE NOT EXISTS (
SELECT 1 FROM migration_status_tables WHERE version_id = 20260608210432
);
UPDATE migration_status_tables
SET is_applied = 1, tstamp = NOW()
WHERE version_id = 20260608210432 AND is_applied = 0;
-- Confirm
SELECT * FROM migration_status_tables WHERE version_id = 20260608210432;Aditya Oza
07/24/2026, 2:59 PMUnthread
07/24/2026, 4:28 PM