fix(diagnostics): deep-review round 2 — a send that claims, not one that blocks
The send no longer holds the session's row lock while it talks to the tracker: it claims the session, gives the lock back, and checkpoints each step. A Stop is never held behind a send, while a draft, a screenshot or a second send are refused at once instead of queueing. The claim ages out, so a request that died mid-send does not strand the session. What the composer says is what happened: an attachment that went is no longer named as one that did not (the missing body edit is its own line), and the issue's screenshot count comes from the server when the list cannot be read, so a failed read no longer blocks saving the draft. The zip is fetched as a request, so its pace is a message and not a saved error page. Screenshots of a finished report can be removed again; only one still being sent is held. A recording's log elevation can no longer outlive it. The step prompt keeps the keyboard (the page's keys stay quiet) and traps Tab itself. An image pasted into a dialog opened over the composer stays that dialog's.
This commit is contained in:
+27
-19
@@ -19,8 +19,10 @@ composer's own preview shows our copies, never these links.
|
||||
Delivery keeps its progress in the session's draft (`delivery`: the issue number, the files still to
|
||||
upload, the links of those uploaded, whether the body shows the screenshots yet), so a failure
|
||||
half-way keeps the issue that exists and a retry does only what is missing — never a second issue.
|
||||
Every step flushes; the caller holds the session's lock throughout (`debugsession.lock`) and commits,
|
||||
also when a step fails, so what did happen is kept. `debugsession.unfinished` reads that progress.
|
||||
Every step is checkpointed (the caller's `db.commit`), so what did happen is kept whatever fails
|
||||
next — and no transaction is held open across these calls. The caller claims the session for the
|
||||
send first (`debugsession.claim_send`), which is what keeps a second send, an autosave or a
|
||||
screenshot out of the way. `debugsession.not_uploaded` / `images_pending` read that progress.
|
||||
"""
|
||||
|
||||
from collections.abc import Callable, Iterator
|
||||
@@ -110,8 +112,12 @@ def _call(client: httpx.Client, method: str, path: str, **kwargs) -> httpx.Respo
|
||||
|
||||
|
||||
def _json(r: httpx.Response, *keys: str) -> dict[str, Any]:
|
||||
"""The answer as the API's JSON, holding `keys` — anything else is refused, not a crash: a
|
||||
crash after the issue was created would lose the record that it was."""
|
||||
"""The answer as the API's JSON, holding `keys` — anything else is refused, not a crash, so a
|
||||
step after the issue was created cannot take down the request that holds its record.
|
||||
|
||||
The CREATE call is the one place this cannot keep that record: its answer IS the record. An
|
||||
answer it cannot read there leaves the session unsent, and a retry opens another issue — the
|
||||
price of not being able to tell a refused create from an unreadable one."""
|
||||
try:
|
||||
data = r.json()
|
||||
# silent-ok: not JSON is one of the answers refused (and logged) just below.
|
||||
@@ -149,17 +155,19 @@ def preview(db: Session, text: str) -> str:
|
||||
return r.text
|
||||
|
||||
|
||||
def _save(session: DebugSession, state: dict) -> None:
|
||||
# A new dict, so the JSONB column sees the change.
|
||||
session.draft = {**(session.draft or {}), "delivery": state}
|
||||
|
||||
|
||||
def deliver(
|
||||
db: Session, session: DebugSession, *, before_create: Callable[[], None] | None = None
|
||||
db: Session,
|
||||
session: DebugSession,
|
||||
*,
|
||||
checkpoint: Callable[[], None],
|
||||
before_create: Callable[[], None] | None = None,
|
||||
) -> str:
|
||||
"""Send the session's issue, or finish sending it; returns its URL. Flushes after every step.
|
||||
`before_create` runs right before a NEW issue is opened — after every check that could refuse
|
||||
it — and may raise to stop it (the caller's pace on new issues)."""
|
||||
(
|
||||
"""Send the session's issue, or finish sending it; returns its URL. `checkpoint` persists what
|
||||
has happened so far and is called after every step (the caller's commit — this module does not
|
||||
decide when a transaction ends). `before_create` runs right before the tracker is asked to open
|
||||
a NEW issue — after the checks that can refuse it without asking — and may raise to stop it.""",
|
||||
)
|
||||
state = debugsession.delivery(session)
|
||||
files = bundle.files(db, session)
|
||||
attachments = [f for f in files if f.name != "issue.md"]
|
||||
@@ -184,8 +192,8 @@ def deliver(
|
||||
"links": {},
|
||||
}
|
||||
session.issue_url = created["html_url"]
|
||||
_save(session, state)
|
||||
db.flush()
|
||||
debugsession.set_delivery(session, state)
|
||||
checkpoint()
|
||||
log.info(
|
||||
"issue created",
|
||||
extra=logs.event("issues.gitea.created", number=created["number"]),
|
||||
@@ -209,13 +217,13 @@ def deliver(
|
||||
"pending": [n for n in state["pending"] if n != item.name],
|
||||
"links": {**state["links"], item.name: asset["browser_download_url"]},
|
||||
}
|
||||
_save(session, state)
|
||||
db.flush()
|
||||
debugsession.set_delivery(session, state)
|
||||
checkpoint()
|
||||
shots = [n for n in state["links"] if n.startswith("screenshot-")]
|
||||
if shots and not state.get("images_shown"):
|
||||
images = "\n".join(f"" for n in sorted(shots))
|
||||
body = f"{bundle.body_of(session)}\n\n---\n\n{images}"
|
||||
_call(client, "PATCH", f"/repos/{repo}/issues/{number}", json={"body": body})
|
||||
_save(session, {**state, "images_shown": True})
|
||||
db.flush()
|
||||
debugsession.set_delivery(session, {**state, "images_shown": True})
|
||||
checkpoint()
|
||||
return session.issue_url or ""
|
||||
|
||||
Reference in New Issue
Block a user