[Concept,4/6] patman: Serialise schema migrations across processes

Message ID 20260506153006.529909-5-sjg@u-boot.org
State New
Headers
Series patman: Concurrent DB access and per-series review worktrees |

Commit Message

Simon Glass May 6, 2026, 3:29 p.m. UTC
  From: Simon Glass <sjg@chromium.org>

Now that PATMAN_DB_DIR lets multiple repos share one database, two
patman invocations can race on first launch after a schema bump: both
read the old schema_version, both run the same _migrate_to_vN() step,
and the second crashes on a duplicate-column ALTER TABLE. Even within
a single repo the same race exists; PATMAN_DB_DIR just makes it more
visible.

Wrap the migration loop in an advisory file lock on .patman.db.lock,
acquired with fcntl.flock(LOCK_EX). The first process performs the
migration; peers block until it finishes, re-read the (now current)
version inside the loop, and exit immediately. The lock fd is
independent of the SQLite connection, so the existing
close-backup-reopen flow inside the loop still works.

Signed-off-by: Simon Glass <sjg@chromium.org>
---

 tools/patman/database.py | 14 ++++++++++++++
 1 file changed, 14 insertions(+)
  

Patch

diff --git a/tools/patman/database.py b/tools/patman/database.py
index 2b05a6c4cfe..49b932320b2 100644
--- a/tools/patman/database.py
+++ b/tools/patman/database.py
@@ -12,6 +12,7 @@  and write some code in migrate_to() to call it.
 """
 
 from collections import namedtuple, OrderedDict
+import fcntl
 import os
 import sqlite3
 
@@ -275,7 +276,20 @@  class Database:  # pylint:disable=R0904
         Args:
             dest_version (int): Version to migrate to
         """
+        # Serialise migrations across processes via an advisory lock on a
+        # sentinel file beside the DB. Without this, two patman processes
+        # starting against the same out-of-date DB can both decide they
+        # need to run the next migration step, and the second one crashes
+        # on a duplicate-column ALTER TABLE
+        with open(f'{self.db_path}.lock', 'w', encoding='utf-8') as lock_fd:
+            fcntl.flock(lock_fd, fcntl.LOCK_EX)
+            self._migrate_locked(dest_version)
+
+    def _migrate_locked(self, dest_version):
+        """Run the migration loop; caller holds the migration lock"""
         while True:
+            # Re-read each iteration: a peer process may have advanced the
+            # version while we were waiting for the lock
             version = self.get_schema_version()
             if version == dest_version:
                 break