fix: defer background cleanup until pending display is confirmed

This commit is contained in:
渡口浪人
2026-10-18 20:32:37 +08:00
parent a2e8a204e8
commit b2c90e452b
5 changed files with 40 additions and 15 deletions
+2 -2
View File
@@ -1,7 +1,7 @@
# pigway-frame-flow v0.1.23
- 自动调度和自动重试在同一设备已有图片等待同步时排队,避免 /upload 自动激活新图片而替换尚未确认的目标。等待沿用现有同步超时;保留用户手动切换与优先级行为。
- 自动调度、自动重试和后台清理在同一设备已有图片等待同步时排队,避免 /upload 激活新图或删除操作更新云端计数,干扰尚未确认的目标。等待沿用现有同步超时;保留用户手动切换与优先级行为。
- 在操作回执中保留最多 12 条设备读回状态及受限错误字段,记录实际图片、同步、在线和心跳,供定位未确认原因;不保存原始异常、URL 或凭据。采样不延长同步超时,原始上传证据保留。
- 沿用安全基础镜像、完整构建和固定镜像 ID 的预构建验收流程;不重发不确定的设备写入。
验证:600 项测试(全量 599 项及新增超时边界用例)、Ruff、格式和 mypy 通过。历史设备物理刷新状态无法从缺失的原始采样追溯,不能将此版本描述为已修复所有网络或设备故障。
验证:601 项测试、Ruff、格式和 mypy 通过。历史设备物理刷新状态无法从缺失的原始采样追溯,不能将此版本描述为已修复所有网络或设备故障。
+4 -4
View File
@@ -10,7 +10,7 @@ c6fc984a1ca46cbd3e06de4591e41d21e4eb592416385d4000ade8fd570aad0c Dockerfile
250a2bcec248b074786d90e0329874628d70d5a0fe3b4fba47c8c57d4f84dcce NOTICE.md
26ad0d9063c0ee843f0f532604b8fd66e8b39fd72d129595ed078677c0d6d13c README.md
ee00cb267b38485f1e574d610f0a57b51d4ae575731a18e57d06a7c43dc0a8b0 README.zh-CN.md
668d9245385b20a1d7052b96c982a4d4654e40b0e2e978522d4bef513b85f668 RELEASE_NOTES.md
5ec5770a6e0202d981c5719de8cb09fefbd5d0a79b537d949b91ec67c8bce72b RELEASE_NOTES.md
6a0c85c8d52a2d4a3b779dce306a93c4efc4dfe3ab3b23e4a6a3bb7c55079910 SECURITY.md
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 build-cache/wheels/.gitkeep
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 config/.gitkeep
@@ -49,7 +49,7 @@ e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 pigway_frame_f
81effa34d6c452579cfa65781909f7c5a9e8ddad136e4c47ee0704939851879a pigway_frame_flow/display/cloud_library.py
5e7e272e671a6d01d9b677416c77967ca488b4e2aa559f4211cf0de4ba1e88bc pigway_frame_flow/display/recovery.py
e1e4ae3ba321ae7282c5a4e061c52f24d2a6a6e3d4637e69e4b1fbd05004f5f6 pigway_frame_flow/display/retention.py
faff3c264ef1ffe496d2989da0b7ee0add60bda1b7ff92cf47e4fc7690b95242 pigway_frame_flow/display/service.py
6b09243081d53fb6c8a780d77e064f0685267c61e05f514262c8514ee3a36fda pigway_frame_flow/display/service.py
0975b24488be19f24e768c4c76d9f3c0348417c1351c4e410897da00f7be6656 pigway_frame_flow/display/upload_reconciliation.py
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 pigway_frame_flow/drivers/__init__.py
9a8386ef368a6df7fd8cb23bcc442c28e784729a5ce4a78911ad86245d5c5794 pigway_frame_flow/drivers/base.py
@@ -207,8 +207,8 @@ f14bad45534041ff0b7dead21d2952ec9d6c60e9f718ab5393bdf5a47b2a6d3e tests/test_pho
ab773ca6868ec007786e7142d5425663a5c8da35b9aa90837ac8e46b51e51507 tests/test_production_bootstrap.py
fee152c5f6eaeda712d38cf88a79213f5e885ba323eb15dfa5a12cf4c88028c3 tests/test_production_deploy.py
f0601f5ed89e8a14a4584a9683611aa7510a964f50a642348b8ab6c62df72278 tests/test_publication_security.py
1f6ca49673eae895dacfb5dd74cecf72b2136c218078ab99e24ae766a1340ed8 tests/test_recovery.py
072e8ca2fa68ca73fb078b107ced96ea683652410b286017db883c97608d78f6 tests/test_retention.py
915992a2ef9aeb8ae5c688d5dd50c6acd37ba726eb5aff1a0df991ffe374d94e tests/test_recovery.py
e0a426eed6f1b6f59207b4de7d8286b6852550bfa28ad30e20f7e911ad9865ee tests/test_retention.py
ab2cd30163453fe571b55c30cb26dc047a104a536ec01d2c64aa2bc91996ba11 tests/test_scheduler.py
0cccc08adf35a06dbf704eb5c35966bc1c3de0ce39d3014efd7ca751253ce79f tests/test_server_release.py
a7ae1b4a64bfcbcf2b7ff795b30ba91db24ee2ad7544073dca2bf43d021cd266 tests/test_smoke_dependencies.py
+9 -7
View File
@@ -518,11 +518,11 @@ class DisplayService:
) and self.blocked(op["device_id"]):
continue
payload = json.loads(op["payload"])
if op["action"] == "display":
if op["client_id"] in (
"scheduler",
"scheduler-retry",
) and self.db.one(
if (
op["action"] in ("display", "delete", "restore")
and op["client_id"]
in ("scheduler", "scheduler-retry", "cloud-library", "retention")
and self.db.one(
"SELECT id FROM operations WHERE device_id=? AND action='display' "
"AND status='SYNCING' AND write_started=1 AND updated_at>? LIMIT 1",
(
@@ -532,10 +532,12 @@ class DisplayService:
- timedelta(seconds=self.settings.sync_timeout)
).isoformat(),
),
)
):
# Upload activates a new image upstream. Give the prior
# image its existing confirmation window before replacing it.
# Both upload and deletion update the cloud frame counter.
# Automatic writes must not invalidate a pending display ack.
continue
if op["action"] == "display":
priority = payload["options"]["priority"]
top = self.top(op["device_id"])
if top and priority < top["priority"]:
+21
View File
@@ -270,3 +270,24 @@ def test_automatic_wait_does_not_extend_past_sync_timeout(service):
)
service.process_one()
assert service.operation(second)["status"] == "SYNCING"
def test_automatic_cleanup_waits_for_display_confirmation(service, monkeypatch):
first = submit(service)
service.process_one()
monkeypatch.setattr(service.cloud_library, "eligible", lambda *args: True)
cleanup = service.submit(
"delete",
{"image_id": 1, "cloud_cleanup": True},
"living-room",
"cloud-library",
"fixture-cleanup",
)
service.process_one()
assert service.operation(cleanup)["status"] == "QUEUED"
assert 1 in [x.image_id for x in service.registry.get("living-room").images()]
service.sync()
service.process_one()
assert service.operation(first)["status"] == "COMPLETED"
assert service.operation(cleanup)["status"] == "COMPLETED"
assert 1 not in [x.image_id for x in service.registry.get("living-room").images()]
+2
View File
@@ -37,6 +37,8 @@ def due(service, image_id):
"UPDATE image_retention SET expires_at=? WHERE image_id=?",
((datetime.now(UTC) - timedelta(days=1)).isoformat(), image_id),
)
# Complete the upload before simulating expiry days later and switching away.
service.sync()
# The test driver switches to a different image, not a real device.
driver = service.registry.get("living-room")
driver.display(1)