fix: make Search watch admission recoverable (Closes #1295)
CI / lint (pull_request) Successful in 4m17s
CI / build-release (pull_request) Successful in 10s
CI / browser-journey (pull_request) Failing after 7m36s
CI / release-candidate (pull_request) Has been skipped

This commit is contained in:
timmy committed 2026-08-23 06:05:48 +00:00
1 parent 4fe07bcd52
commit 6c12731215
7 files changed
+213 -15

No files matched your search

+21
View File
@@ -113,6 +113,27 @@ class FollowingStore:
)
return snapshot
def preflight(self, login: str, raw_item: dict, watching: bool) -> dict:
"""Validate a requested change and capacity without mutating the registry."""
login = self._login(login)
item = self._normalize_item(raw_item)
if not isinstance(watching, bool):
raise ValueError("watching is invalid")
if not watching:
return item
identity = self._identity(item)
with self._connect() as connection:
row = connection.execute(
"SELECT revision, items FROM following_issues WHERE login = ?", (login,)
).fetchone()
current, _legacy = self._snapshot(row, login)
if (
len(current["items"]) >= self.limit
and not any(self._identity(candidate) == identity for candidate in current["items"])
):
raise ValueError(f"following is limited to {self.limit} issues")
return item
def set_watching(self, login: str, raw_item: dict, watching: bool) -> dict:
login = self._login(login)
item = self._normalize_item(raw_item)
+2
View File
@@ -608,6 +608,8 @@ async def work_preview(repository: str, kind: str, number: int) -> dict:
"title": issue.get("title", "") if isinstance(issue.get("title"), str) else "",
"body": issue.get("body", "") if isinstance(issue.get("body"), str) else "",
"state": state,
"updated_at": issue.get("updated_at", "")
if isinstance(issue.get("updated_at"), str) else "",
"author": author.get("login", "") if isinstance(author.get("login"), str) else "",
"labels": [
label["name"] for label in labels
+31 -14
View File
@@ -3738,26 +3738,43 @@ async def mutate_global_search_preview_subscription(
repository, preview = await _search_preview_subscription_target(owner, repo, number, kind)
login = await _confirmed_login()
store = _following_store()
await asyncio.to_thread(store.get, login)
following_item = {
"repository": repository,
"number": number,
"title": preview.get("title", ""),
"state": preview.get("state", ""),
"updated_at": preview.get("updated_at", ""),
"url": preview.get("url", ""),
}
await asyncio.to_thread(store.preflight, login, following_item, watching)
result = await asyncio.wait_for(
gitea_proxy.set_issue_subscription(repository, number, watching),
timeout=ISSUE_ACTION_TIMEOUT_SECONDS,
)
if result.get("watching") is not watching:
raise RuntimeError("Gitea did not confirm subscription state")
following = await asyncio.to_thread(
store.set_watching,
login,
{
"repository": repository,
"number": number,
"title": preview.get("title", ""),
"state": preview.get("state", ""),
"updated_at": preview.get("updated_at", ""),
"url": preview.get("url", ""),
},
watching,
)
try:
following = await asyncio.to_thread(
store.set_watching,
login,
following_item,
watching,
)
except Exception:
try:
compensated = await asyncio.wait_for(
gitea_proxy.set_issue_subscription(repository, number, not watching),
timeout=ISSUE_ACTION_TIMEOUT_SECONDS,
)
except Exception:
compensated = None
if not isinstance(compensated, dict) or compensated.get("watching") is not (not watching):
return JSONResponse({
**result,
"following_synced": False,
"error": "Watching in Gitea, but Following could not sync. Retry this action.",
})
raise
except HTTPException:
raise
except Exception: