Skip to content

fix(viewer): quieten two Sentry/journal noise sources and fail fast on a lost display - #3271

Merged
vpetersson merged 2 commits into
Screenly:masterfrom
vpetersson-bot:split/viewer-log-and-guard
Aug 7, 2026
Merged

fix(viewer): quieten two Sentry/journal noise sources and fail fast on a lost display#3271
vpetersson merged 2 commits into
Screenly:masterfrom
vpetersson-bot:split/viewer-log-and-guard

Conversation

@vpetersson-bot

Copy link
Copy Markdown
Contributor

Issues Fixed

Fixes #3265, #3266, #3268 — found in the 2026-08-07 Sentry triage sweep and validated on six physical testbeds.

Description

Three independent viewer fixes. Grouped because they are all in asset_loop/load_browser and all about the same underlying problem — the viewer drowning its own diagnostics — but they are otherwise unrelated and each stands alone.

#3265 — the recovery logged its own success at ERROR

The Wayland headless-boot watchdog exits so the container restarts and re-enumerates the display. It logged that at ERROR, and the Sentry logging integration promotes ERROR records to events, so every successful self-heal filed a Sentry issue — that is ANTHIAS-4M, the only issue ever seen on the current release. Demoted to warning. The give-up-after-cap branch stays at ERROR, because that is the point where recovery has failed and a human is needed.

#3266 — fail fast when the display device is gone

The container's /dev is a start-time snapshot and wait_for_framebuffer only runs at container start, so a linuxfb board whose display disappears afterwards can never spawn a webview again. Retrying cannot re-enumerate /dev; it only delays the container restart that actually recovers, by ~6.5 min on the 30-attempt startup path.

Conservative by construction: requires both one of Qt's own no-screen messages and the framebuffer node genuinely absent, and returns False for wayland/eglfs. Also honours linuxfb's fb=/dev/fbN rather than hardcoding /dev/fb0.

#3268 — stop the viewer evicting its own crash diagnostics

In production the docker journald driver feeds a volatile journal capped at ~10% of /run — about 15 MB on an 800 MB board. The idle viewer was emitting ~1440 lines/hour into it, and the not-displayable arm (a 0.5s wait) up to ~7200 lines/hour, so a single unreachable asset could evict the crash context around any failure within hours. Now: empty-playlist logged on transition, per-tick URL line at debug, and the not-displayable arm throttled to once per offending asset.

Hardware validation

Board Stack Result
Pi 2 armhf / Qt5 / linuxfb #3266 before/after + false-positive hunt; #3268 rate
Pi 3-64 arm64 / Qt6 / eglfs guard inert, retry budget preserved; #3268 rate
Pi 4 arm64 / Qt6 / eglfs guard inert with a real induced failure; #3268 rate
Pi 5 arm64 / Qt6 / Wayland #3265 induced for real, with a Sentry A/B
x86 x86_64 / Wayland guard inert; #3268 rate
Pi 3 A+ armv7l, 361 MB guard logic correct; unreachable there (viewer never starts)

#3266 — Pi 2, before: 3 attempts / 6.02s / generic message. After: 1 attempt / 2.94s / distinct message, no Retrying lines at all.

The false-positive risk does not exist, and this was the main risk in the change. It was attacked three ways on hardware: 5 healthy restarts with /dev/fb0 present (0 firings); an adversarial /dev/fb0 replaced by an unopenable node so that Qt emitted two of the three guard signatures while the device still existed — the guard correctly declined and spent its full budget; and an 8/8 on-board truth table. The device check is the load-bearing condition, so a signature match alone cannot fire it.

#3265 — induced for real on the Pi 5 by running a second live cage with no outputs and driving the real watchdog (nothing mocked): recovery at WARNING(30), give-up still ERROR(40), restart counter and cap unchanged. A capturing-transport A/B confirmed the mechanism: ERROR → 1 Sentry event, WARNING → 0.

#3268 — idle, 0 enabled assets: 1437–1824 lines/hour → 0, on four boards. Pi 2 measured 17.9 minutes of complete silence. The latch resets in both directions, verified across multiple enable/disable cycles.

Scope notes, so this is not oversold

Testing

190 passed in tests/test_viewer.py, mypy clean across 169 files, ruff check + format clean.

New tests cover the gaps the boards flagged as uncovered: the load_browser short-circuit and its false-positive counterpart (full budget spent when the device is present), the fb=/dev/fbN lookup, both #3268 latches including that they reset rather than going silent, and the #3265 ERROR→WARNING demotion.

Checklist

  • I have performed a self-review of my own code.
  • New and existing unit tests pass locally and on CI with my changes.
  • I have done an end-to-end test for Raspberry Pi devices.
  • I have tested my changes for x86 devices.
  • I added a documentation for the changes I have made (when necessary).

…n a lost display

Three independent viewer fixes, all found in the 2026-08-07 Sentry sweep
and validated on the physical testbeds.

Screenly#3265 — the Wayland headless-boot recovery logged its own SUCCESS at
ERROR, and the Sentry logging integration promotes ERROR records to
events, so every self-heal filed an issue (ANTHIAS-4M, the only issue
seen on the current release). Demoted to warning; the give-up-after-cap
branch stays at ERROR, since that is where a human is actually needed.
Induced for real on the Pi 5: recovery WARNING(30), give-up ERROR(40),
and a capturing-transport A/B confirmed ERROR -> 1 event, WARNING -> 0.

Screenly#3266 — short-circuit the launch retry loop when the display device has
vanished. The container's /dev is a start-time snapshot and
wait_for_framebuffer only runs at container start, so retrying cannot
re-enumerate it; it only delays the restart that actually recovers.
Reproduced deterministically on the Pi 2: 3 attempts / 6.02s before,
1 attempt / 2.94s after. Conservative by construction — needs a Qt
no-screen signature AND the framebuffer node genuinely absent, and never
fires on wayland/eglfs. The false-positive risk was attacked directly:
with /dev/fb0 replaced by an unopenable node Qt emitted two of the three
signatures and the guard correctly declined, spending its full budget.

Screenly#3268 — log the empty-playlist notice on transition rather than every
5s poll, drop the per-tick URL line to debug, and throttle the
not-displayable arm to once per offending asset. Measured idle: 1437-1824
lines/hour -> 0 across boards. That arm was the worse offender at
7155-7200 lines/hour on a 0.5s wait, and it also never cleared the
empty-playlist latch, which made the notice go permanently silent after
"empty -> unavailable -> empty".

Honest scope note: Screenly#3266 does NOT reduce Sentry event volume (2 events
before and after) — it improves grouping via the distinct message and
cuts the black-screen delay. It also cannot fire on the handshake-timeout
flavour, whose message carries no Qt output to match.
@vpetersson-bot
vpetersson-bot requested a review from a team as a code owner August 7, 2026 08:20
@vpetersson-bot vpetersson-bot self-assigned this Aug 7, 2026
@codecov

codecov Bot commented Aug 7, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
⚠️ Please upload report for BASE (master@82c5def). Learn more about missing BASE report.

Additional details and impacted files
@@            Coverage Diff            @@
##             master    #3271   +/-   ##
=========================================
  Coverage          ?   90.71%           
=========================================
  Files             ?       76           
  Lines             ?     8440           
  Branches          ?      896           
=========================================
  Hits              ?     7656           
  Misses            ?      563           
  Partials          ?      221           

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR targets the Anthias viewer’s startup/asset loop diagnostics by reducing high-volume log noise (to preserve journald crash context), demoting a benign Wayland recovery log level to avoid Sentry event spam, and adding a linuxfb-specific “fail fast” path when the display device is no longer usable inside the container.

Changes:

  • Demote the Wayland headless-boot recovery log from ERROR to WARNING while preserving the give-up branch at ERROR.
  • Add a linuxfb-only guard to abandon the load_browser retry budget immediately when Qt reports “no screen” and the framebuffer device is absent.
  • Throttle high-frequency viewer loop logs via transition-based latches (empty playlist + per-offender unavailable asset), and move per-tick “Current url” logging to debug.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

File Description
src/anthias_viewer/__init__.py Implements the logging changes, linuxfb fail-fast guard, and new latch-based throttling in asset_loop / load_browser.
tests/test_viewer.py Adds unit/integration-style coverage for the new guard behavior, linuxfb fb-device parsing, and the new log-latch semantics.
Suppressed comments (1)

src/anthias_viewer/init.py:1191

  • This fail-fast WebviewLaunchError message hardcodes "/dev/fb0 absent", but the guard explicitly supports linuxfb's fb=/dev/fbN option via _linuxfb_device(). If fb is set (or becomes set via existing rotation-option preservation), the message can be misleading even when the guard is correct.
                raise WebviewLaunchError(
                    'AnthiasViewer cannot start: the display device is gone '
                    '(/dev/fb0 absent inside the container, and Qt reports '
                    'no usable screen). The container needs to restart to '
                    're-enumerate /dev; retrying in-process cannot help. '
                    f'Last error: {exc}'

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/anthias_viewer/__init__.py
Copilot review of Screenly#3266. Qt's QPA syntax is
`<plugin>[:opt1=val1,opt2=val2,...]` — a single colon, then
COMMA-separated options, as _set_qpa_rotation already documents and
parses. _linuxfb_device() split the options on ':' instead, so
`linuxfb:fb=/dev/fb1,rotation=90` yielded '/dev/fb1,rotation=90' as the
device path; os.path.exists then said False and the guard short-circuited
the retry budget on a board whose framebuffer was perfectly fine.

That is a false positive in exactly the guard the hardware run was
checking for false positives — it just needed a board with more than one
QPA option set, which none of the testbeds has.

- parse the same way _set_qpa_rotation does
- name the resolved device in the error message instead of hardcoding
  /dev/fb0, which would have misreported on an fb1 board
- test the comma-separated forms, including options either side of fb=
@sonarqubecloud

sonarqubecloud Bot commented Aug 7, 2026

Copy link
Copy Markdown

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated no new comments.

Suppressed comments (2)

tests/test_viewer.py:3796

  • This test’s name/docstring claims it verifies the empty-playlist latch “resets on both edges”, but the body never transitions out of the empty state, so it only tests the log-once behavior while continuously empty. Either add a transition in the test, or rename the test/docstring to match what it actually asserts.
def test_empty_playlist_logs_once_then_resets_on_both_edges(
    caplog: Any,
) -> None:
    """The idle notice must fire on the transition, not on every 5s poll —
    2 lines/tick was ~1440 lines/hour into a ~15 MB volatile journal,

tests/test_viewer.py:3868

  • caplog is listed as a fixture argument here but never used in the test body, which makes the intent unclear and adds unnecessary fixture setup. Remove it (or add assertions that actually use captured logs).
def test_load_browser_short_circuits_a_vanished_display(caplog: Any) -> None:
    """Integration for the guard: the retry loop must abandon its budget

@vpetersson
vpetersson merged commit 6cb152b into Screenly:master Aug 7, 2026
11 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Wayland headless-boot recovery logs a successful self-heal at ERROR, filing a Sentry issue per recovery

3 participants