Fix webhook starvation and isolate scheduled job failures for v0.1.2 [skip ci]

This commit is contained in:
渡口浪人
2026-10-16 09:48:51 +08:00
parent e1b001a431
commit 8a3c616b5f
8 changed files with 158 additions and 64 deletions
+2 -2
View File
@@ -124,7 +124,7 @@ def create_app(settings=None, start_worker=True):
app = FastAPI(
title="Pigway Frame Flow API",
docs_url=None,
version="0.1.1",
version="0.1.2",
lifespan=lifespan,
responses=error_responses,
)
@@ -1000,7 +1000,7 @@ def create_app(settings=None, start_worker=True):
)
def settings_view():
return SettingsView(
version="0.1.1",
version="0.1.2",
mock=settings.mock,
write_enabled=settings.write_enabled,
api_key_configured=bool(settings.api_key)
+5 -4
View File
@@ -39,12 +39,13 @@ class Webhooks:
db = self.service.db
for sub in db.all("SELECT * FROM webhook_subscriptions WHERE enabled=1"):
types = json.loads(sub["event_types"])
if not types:
continue
placeholders = ",".join("?" for _ in types)
for event in db.all(
"SELECT * FROM events e WHERE created_at>=? AND NOT EXISTS (SELECT 1 FROM webhook_deliveries d WHERE d.event_id=e.id AND d.subscription_id=?) ORDER BY created_at LIMIT 500",
(sub["created_at"], sub["id"]),
f"SELECT * FROM events e WHERE created_at>=? AND event_type IN ({placeholders}) AND NOT EXISTS (SELECT 1 FROM webhook_deliveries d WHERE d.event_id=e.id AND d.subscription_id=?) ORDER BY created_at LIMIT 500",
(sub["created_at"], *types, sub["id"]),
):
if event["event_type"] not in types:
continue
db.execute(
"INSERT OR IGNORE INTO webhook_deliveries VALUES(?,?,'QUEUED',0,?,NULL)",
(sub["id"], event["id"], now_iso()),
+2 -2
View File
@@ -336,7 +336,7 @@ class SettingsView(BaseModel):
class Health(BaseModel):
status: str
version: str = "0.1.1"
version: str = "0.1.2"
class Readiness(BaseModel):
@@ -366,7 +366,7 @@ class RestoreRequest(StrictModel):
class Version(BaseModel):
product: str = "Pigway Frame Flow"
version: str = "0.1.1"
version: str = "0.1.2"
api_version: str = "v1"
commit: str = "development"
build_time: str = "unknown"
+65 -54
View File
@@ -147,61 +147,72 @@ class Scheduler:
"SELECT * FROM jobs WHERE enabled=1 AND next_run<=?",
(current.isoformat(),),
):
job = JobInput.model_validate_json(row["payload"])
slot = row["next_run"]
if self.db.one(
"SELECT * FROM job_runs WHERE job_id=? AND scheduled_at=?",
(row["id"], slot),
):
continue
# Skip overdue tasks (>5 minutes): avoid an unexpected artwork
# overwrite after downtime. One-time tasks are retired too.
late = (current - datetime.fromisoformat(slot)).total_seconds() > 300
operation_id = None
if not late:
action = "restore" if job.source.type == "restore" else "display"
payload = (
{}
if isinstance(job.source, RestoreContent)
else DisplayRequest(
device_id=job.device_id,
content=job.source,
options=Options(
duration=job.display_duration,
restore_previous=job.restore_previous,
priority=job.priority,
cleanup_unstarred=job.cleanup_unstarred,
try:
job = JobInput.model_validate_json(row["payload"])
slot = row["next_run"]
if self.db.one(
"SELECT * FROM job_runs WHERE job_id=? AND scheduled_at=?",
(row["id"], slot),
):
continue
# Skip overdue tasks (>5 minutes): avoid an unexpected artwork
# overwrite after downtime. One-time tasks are retired too.
late = (
current - datetime.fromisoformat(slot)
).total_seconds() > 300
operation_id = None
if not late:
action = (
"restore" if job.source.type == "restore" else "display"
)
payload = (
{}
if isinstance(job.source, RestoreContent)
else DisplayRequest(
device_id=job.device_id,
content=job.source,
options=Options(
duration=job.display_duration,
restore_previous=job.restore_previous,
priority=job.priority,
cleanup_unstarred=job.cleanup_unstarred,
),
).model_dump(mode="json")
)
operation_id = self.service.submit(
action,
payload,
job.device_id,
"scheduler",
"schedule-" + uuid4().hex,
"job-" + row["id"] + "-" + slot,
)
following = next_run(job.schedule, current)
enabled = bool(following)
updated = job.model_copy(update={"enabled": enabled}).model_dump(
mode="json"
)
with self.db.transaction() as tx:
tx.execute(
"INSERT OR IGNORE INTO job_runs VALUES(?,?,?)",
(row["id"], slot, operation_id),
)
tx.execute(
"UPDATE jobs SET payload=?,enabled=?,next_run=?,last_run=?,last_result=? WHERE id=?",
(
encode(updated),
int(enabled),
following,
now_iso(),
"SKIPPED_OVERDUE" if late else operation_id,
row["id"],
),
).model_dump(mode="json")
)
operation_id = self.service.submit(
action,
payload,
job.device_id,
"scheduler",
"schedule-" + uuid4().hex,
"job-" + row["id"] + "-" + slot,
)
following = next_run(job.schedule, current)
enabled = bool(following)
updated = job.model_copy(update={"enabled": enabled}).model_dump(
mode="json"
)
with self.db.transaction() as tx:
tx.execute(
"INSERT OR IGNORE INTO job_runs VALUES(?,?,?)",
(row["id"], slot, operation_id),
)
tx.execute(
"UPDATE jobs SET payload=?,enabled=?,next_run=?,last_run=?,last_result=? WHERE id=?",
(
encode(updated),
int(enabled),
following,
now_iso(),
"SKIPPED_OVERDUE" if late else operation_id,
row["id"],
),
)
except Exception:
# Keep this slot pending for retry or the existing overdue policy.
# A single device must not prevent other jobs or queue consumption.
logging.getLogger("frame-flow").error(
"Scheduled job failed; details suppressed"
)
for row in self.db.all(
"SELECT * FROM playlists WHERE next_run<=?", (current.isoformat(),)
+1 -1
View File
@@ -1,3 +1,3 @@
<!doctype html>
<html lang="zh-CN"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><meta name="color-scheme" content="light dark"><title>Pigway Frame Flow</title><link rel="stylesheet" href="/ui/style.css"></head>
<body><aside class="sidebar"><a class="brand" href="/"><span class="brand-icon">F<span>↗</span></span><span>Frame Flow<small>Pigway · Smart Frame Automation</small></span></a><nav id="nav"></nav><div class="sidebar-bottom"><small>Version 0.1.1</small></div></aside><div class="workspace"><header><div><span class="eyebrow">PIGWAY FRAME FLOW</span><h1 id="page-title"></h1></div><div class="toolbar"><select id="device" aria-label="Device"></select><button id="lang" class="icon-button" aria-label="Change language">EN</button><button id="theme" class="icon-button" aria-label="Change theme">◐</button></div></header><main id="main"></main><footer id="footer"></footer></div><div id="toast" role="status" aria-live="polite"></div><dialog id="modal"><div id="modal-content"></div></dialog><script src="/ui/i18n.js"></script><script src="/ui/features.js"></script><script src="/ui/extensions.js"></script><script src="/ui/app.js"></script></body></html>
<body><aside class="sidebar"><a class="brand" href="/"><span class="brand-icon">F<span>↗</span></span><span>Frame Flow<small>Pigway · Smart Frame Automation</small></span></a><nav id="nav"></nav><div class="sidebar-bottom"><small>Version 0.1.2</small></div></aside><div class="workspace"><header><div><span class="eyebrow">PIGWAY FRAME FLOW</span><h1 id="page-title"></h1></div><div class="toolbar"><select id="device" aria-label="Device"></select><button id="lang" class="icon-button" aria-label="Change language">EN</button><button id="theme" class="icon-button" aria-label="Change theme">◐</button></div></header><main id="main"></main><footer id="footer"></footer></div><div id="toast" role="status" aria-live="polite"></div><dialog id="modal"><div id="modal-content"></div></dialog><script src="/ui/i18n.js"></script><script src="/ui/features.js"></script><script src="/ui/extensions.js"></script><script src="/ui/app.js"></script></body></html>