This is an automated email from the ASF dual-hosted git repository.

sbp pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/tooling-trusted-release.git


The following commit(s) were added to refs/heads/main by this push:
     new 9edc6f2  Use immediate transactions to ensure that writes are serial
9edc6f2 is described below

commit 9edc6f2b95e9954c4b1938b20e1561cf40f76aee
Author: Sean B. Palmer <[email protected]>
AuthorDate: Wed May 21 20:39:47 2025 +0100

    Use immediate transactions to ensure that writes are serial
---
 atr/db/__init__.py |  3 +++
 atr/revision.py    | 38 ++++++++++++++++++++++++++++----------
 2 files changed, 31 insertions(+), 10 deletions(-)

diff --git a/atr/db/__init__.py b/atr/db/__init__.py
index 02094ec..daa1aeb 100644
--- a/atr/db/__init__.py
+++ b/atr/db/__init__.py
@@ -130,6 +130,9 @@ class Session(sqlalchemy.ext.asyncio.AsyncSession):
 
     # TODO: Need to type all of these arguments correctly
 
+    async def begin_immediate(self) -> None:
+        await self.execute(sql.text("BEGIN IMMEDIATE"))
+
     def check_result(
         self,
         id: Opt[int] = NOT_SET,
diff --git a/atr/revision.py b/atr/revision.py
index a0df39b..ebb1151 100644
--- a/atr/revision.py
+++ b/atr/revision.py
@@ -79,8 +79,12 @@ async def create_and_manage(
         await aioshutil.rmtree(temp_dir)  # type: ignore[call-arg]
         return
 
-    # Create a revision row, but hold the write lock
-    async with db.session() as data, data.begin():
+    # Create a revision row, holding the write lock
+    async with db.session() as data:
+        # This is the only place where models.Revision is constructed
+        # That makes models.populate_revision_sequence_and_name safe against 
races
+        # Because that event is called when data.add is called below
+        # And we have a write lock at that point through the use of 
data.begin_immediate
         new_revision = models.Revision(
             release_name=release_name,
             release=release,
@@ -89,27 +93,41 @@ async def create_and_manage(
             phase=release.phase,
             description=description,
         )
+
+        # Acquire the write lock and add the row
+        # We need this write lock for moving the directory below atomically
+        # But it also helps to make models.populate_revision_sequence_and_name 
safe against races
+        await data.begin_immediate()
         data.add(new_revision)
+
         # Flush but do not commit the row to get its name and number
+        # The row will still be invisible to other sessions after flushing
         await data.flush()
-        # The row is still invisible to other sessions
+        # Give the caller details about the new revision
         creating.new = new_revision
-        # The caller will now have the details about the new revision
 
         # Rename the directory to the new revision number
         await data.refresh(release)
         new_revision_dir = util.release_directory(release)
+
         # Ensure that the parent directory exists
         await aiofiles.os.makedirs(new_revision_dir.parent, exist_ok=True)
+
         # Rename the temporary interim directory to the new revision number
         await aiofiles.os.rename(temp_dir, new_revision_dir)
-        # creating.interim_path = None
 
-        # Run checks if in DRAFT phase
-        if release.phase == models.ReleasePhase.RELEASE_CANDIDATE_DRAFT:
-            # Must use caller_data here because we acquired the write lock
-            await tasks.draft_checks(project_name, version_name, 
new_revision.number, caller_data=data)
-        # Commit by leaving the data.begin() context manager
+        # Commit to end the transaction started by data.begin_immediate
+        # We must commit the revision before starting the checks
+        await data.commit()
+
+        async with data.begin():
+            # Run checks if in DRAFT phase
+            # We could also run this outside the data Session
+            # But then it would create its own new Session
+            # It does, however, need a transaction to be created using 
data.begin()
+            if release.phase == models.ReleasePhase.RELEASE_CANDIDATE_DRAFT:
+                # Must use caller_data here because we acquired the write lock
+                await tasks.draft_checks(project_name, version_name, 
new_revision.number, caller_data=data)
 
 
 async def latest_info(project_name: str, version_name: str) -> tuple[str, str, 
datetime.datetime] | None:


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to