asf-tooling commented on issue #299:
URL: 
https://github.com/apache/tooling-trusted-releases/issues/299#issuecomment-4404074221

   <!-- gofannon-issue-triage-bot v2 -->
   
   **Automated triage** — analyzed at `main@837830e8`
   
   **Type:** `discussion`  •  **Classification:** `no_action`  •  
**Confidence:** `high`
   **Application domain(s):** `data_storage`
   
   ### Summary
   This is an ongoing architectural design discussion about preventing use of 
detached SQLAlchemy/SQLModel session objects after their session closes. The 
team has identified three distinct failure modes (detached-writes, 
detached-reads, and greenlet-lazy-load) but has not converged on a single 
implementation approach. @sbp listed five potential solutions ranging from 
one-session-per-request to continuation passing style (CPS), and most recently 
(Apr 23 2026) noted the need to verify that live session data isn't returned 
from continuation functions. @dave2wave characterized the near-term approach as 
'awareness while developing.' The CPS approach is being tested first in #628.
   
   ### Where this lives in the code today
   
   #### `atr/db/interaction.py` — `releases_by_phase` (lines 480-501)
   _currently does this_
   Returns ORM objects that become detached once the session context manager 
exits — the core pattern this issue targets.
   
   ```python
   async def releases_by_phase(project: sql.Project, phase: sql.ReleasePhase) 
-> list[sql.Release]:
       """Get the releases for the project by phase."""
   
       query = (
           sqlmodel.select(sql.Release)
           .where(
               sql.Release.project_key == project.key,
               sql.Release.phase == phase,
           )
           
.order_by(sql.validate_instrumented_attribute(sql.Release.created).desc())
       )
   
       results = []
       async with db.session() as data:
           for result in (await data.execute(query)).all():
               release = result[0]
               results.append(release)
   
       for release in results:
           # Don't need to eager load and lose it when the session closes
           release.project = project
       return results
   ```
   
   #### `atr/db/interaction.py` — `all_releases` (lines 107-118)
   _currently does this_
   Another instance of the pattern: opens a session, fetches ORM objects, 
closes the session, then manually assigns a relationship. The returned objects 
are detached.
   
   ```python
   async def all_releases(project: sql.Project) -> list[sql.Release]:
       """Get all releases for the project, sorted by version."""
       query = sqlmodel.select(sql.Release).where(sql.Release.project_key == 
project.key)
   
       results = []
       async with db.session() as data:
           for result in (await data.execute(query)).all():
               release = result[0]
               results.append(release)
   
       for release in results:
           release.project = project
   ```
   
   #### `atr/storage/__init__.py` — `write` (lines 455-463)
   _needs modification_
   The session is scoped to this context manager, but Write (and its writer 
subsystems) may return ORM objects that outlive the session. @sbp's latest 
comment addresses this: 'check that live session data is not returned from any 
of the continuation functions.'
   
   ```python
   @contextlib.asynccontextmanager
   async def write(asf_uid: principal.UID = principal.ArgumentNone) -> 
AsyncGenerator[Write]:
       if asf_uid is principal.ArgumentNone:
           authorisation = await principal.Authorisation()
       else:
           authorisation = await principal.Authorisation(asf_uid)
       async with db.session() as data:
           # TODO: Replace data with a DatabaseWriter instance
           yield Write(authorisation, data)
   ```
   
   #### `atr/storage/__init__.py` — `read` (lines 455-463)
   _needs modification_
   Same session-scoping pattern as write(); ORM objects retrieved via Read can 
escape the session scope.
   
   ```python
   @contextlib.asynccontextmanager
   async def read(asf_uid: principal.UID = principal.ArgumentNone) -> 
AsyncGenerator[Read]:
       if asf_uid is principal.ArgumentNone:
           authorisation = await principal.Authorisation()
       else:
           authorisation = await principal.Authorisation(asf_uid)
       async with db.session() as data:
           # TODO: Replace data with a DatabaseReader instance
           yield Read(authorisation, data)
   ```
   
   #### `atr/db/interaction.py` — `release_ready_for_vote` (lines 436-451)
   _currently does this_
   The specific function @andrewmusselman's analysis cited — it loads 
with_project_release_policy but not with_release_policy, causing 
MissingGreenlet on lazy access (failure mode C).
   
   ```python
   async def release_ready_for_vote(
       session: web.Committer,
       project_key: safe.ProjectKey,
       version_key: safe.VersionKey,
       revision: safe.RevisionNumber,
       data: db.Session,
       allowed_vote_modes: frozenset[sql.VoteMode],
   ) -> tuple[sql.Release, sql.Committee] | str:
       release = await session.release(
           project_key,
           version_key,
           data=data,
           with_project=True,
           with_committee=True,
           with_project_release_policy=True,
       )
   ```
   
   #### `atr/storage/writers/cache.py` — `GeneralPublic` (lines 30-40)
   _currently does this_
   Writer class that stores a db.Session reference — if the session closes 
before the writer is used, operations on stale objects may fail silently or 
with detached errors.
   
   ```python
   class GeneralPublic:
       def __init__(
           self,
           write: storage.Write,
           write_as: storage.WriteAsGeneralPublic,
           data: db.Session,
       ) -> None:
           self.__write = write
           self.__write_as = write_as
           self.__data = data
           self.__asf_uid = write.authorisation.asf_uid
   ```
   
   ### Proposed approach
   The team has not converged on a single implementation approach. @sbp 
identified continuation passing style (CPS) as the most complete solution — it 
prevents detached objects from leaking out of session scopes by construction — 
but acknowledged it's cumbersome. This is being tested first in issue #628 
(quarantined mode). @dave2wave characterized the near-term approach as 
developer awareness. @sbp's most recent comment (Apr 23) suggests the next 
concrete step is checking that live session data is not returned from 
continuation functions. Until #628 yields results and the team decides on an 
approach, no unilateral patch should be proposed.
   
   ### Open questions
   - What was the outcome of the CPS experiment in #628? Has it been 
implemented or is it still in progress?
   - Does @dave2wave's question about per-connection pragmas refer to SQLite 
PRAGMA statements that could help with session isolation?
   - Would a runtime check (e.g., asserting objects are not detached before 
passing them across boundaries) be acceptable as an interim measure?
   - Should there be a linting rule or custom mypy/pyright plugin to flag ORM 
objects escaping session scopes?
   - Are there specific writer methods that have been identified as currently 
returning detached ORM objects that callers rely on?
   
   _The agent reviewed this issue and is not proposing patches in this run. 
Review the existing-code citations and open questions above before deciding 
next steps._
   
   ### Files examined
   - `atr/db/__init__.py`
   - `atr/storage/writers/cache.py`
   - `atr/db/interaction.py`
   - `atr/storage/writers/mail.py`
   - `atr/storage/__init__.py`
   
   ---
   *Draft from a triage agent. A human reviewer should validate before merging 
any change. The agent did not run tests or verify diffs apply.*


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


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

Reply via email to