Skip to content

fix: e2e-test findings (host-agent venv, celery beat, asset GET 404) - #2881

Merged
vpetersson merged 7 commits into
masterfrom
fix/e2e-test-findings
May 12, 2026
Merged

fix: e2e-test findings (host-agent venv, celery beat, asset GET 404)#2881
vpetersson merged 7 commits into
masterfrom
fix/e2e-test-findings

Conversation

@vpetersson

Copy link
Copy Markdown
Contributor

Summary

Three independent fixes found by running an end-to-end test of master against an x86 device and a Pi 4B side-by-side.

1. Persistent host-agent venv (bin/install.sh)

The installer venv was made ephemeral (mktemp tmpdir, cleaned on EXIT) without updating anthias-host-agent.service's ExecStart, which still hardcodes /home/${USER}/installer_venv/bin/python. Result on every fresh install since: 203/EXEC restart loop, no IP cache, /api/v2/info blocks ~80s in get_node_ip() waiting for host_agent_ready that never lands. Now split into two venvs — ephemeral for ansible-core, persistent for the host-agent — and restart the unit explicitly on upgrade since the in-memory interpreter survives the venv removal.

2. Celery beat in-memory scheduler

celery -B with the default PersistentScheduler stores its schedule via shelve. On Python 3.13, shelve defaults to dbm.sqlite3, which intermittently raises dbm.sqlite3.error: locking protocol — observed in this build matrix on x86 only. When Beat stalls, reconcile_stuck_processing and the other periodic tasks stop, so is_processing=true rows never get re-dispatched. setup_periodic_tasks defines every periodic task statically, so a non-persistent scheduler is sufficient — switch to celery.beat.Scheduler in all three compose files.

3. 404 (not 500) for unknown asset_id

AssetView{V1,V1_1,V1_2,V2}.get/put/patch/update, DeleteAssetViewMixin, AssetContentViewMixin, and ViewerCurrentAssetViewV1 all called Asset.objects.get(asset_id=...) bare. The DoesNotExist had no DRF handler registered → 500 + traceback for what is structurally a missing resource. Route the lookup through django.shortcuts.get_object_or_404 (DRF's exception handler converts the Http404 to a clean 404 Response). New parametrised test guards every API version.

Test plan

  • ruff check + ruff format --check clean on all changed files
  • Host-side: recreated /home/mvip/installer_venv/ on the x86 test rig with the same uv command the new function emits, restarted anthias-host-agent.service — service active, host_agent_ready=true in Redis, /api/v2/info returns in 1s (down from 79s)
  • Host-side: celery -A anthias_server.celery_tasks.celery beat --scheduler celery.beat.Scheduler smoke-tested inside anthias-anthias-celery-1 — beat starts cleanly, schedule loads, no shelve/dbm files written
  • Stuck Sample Mpeg asset deleted manually from x86 (root cause The 'view playlist' web interface doesn't obey 'end date/time'  #2)
  • CI green on this PR (pytest covers the new 404 test across all four versions)
  • Post-merge: pull on the x86 rig + sanity-check the asset GET-after-delete returns 404

vpetersson and others added 3 commits May 12, 2026 19:57
…203/EXEC)

PR #2843 switched the installer venv to a mktemp tmpdir cleaned up
on EXIT, but anthias-host-agent.service's ExecStart still hardcodes
/home/${USER}/installer_venv/bin/python. Every fresh install since
that refactor leaves the unit in a status=203/EXEC restart loop with
no Python at the configured path, and /api/v2/info then blocks ~80s
on get_node_ip() waiting for the host_agent_ready key that will
never appear.

Split the two venvs:

* INSTALLER_VENV: still ephemeral mktemp, used by ansible-core during
  install/upgrade and torn down by the EXIT trap.
* HOST_AGENT_VENV: new persistent venv at /home/${USER}/installer_venv
  (path kept stable so devices installed before the refactor don't
  need a unit rewrite), recreated from the host dep group on every
  install + upgrade so deps track pyproject.toml.

provision_host_agent_venv runs after install_ansible() and before
run_ansible_playbook() so the venv exists before ansible's
state: started fires the unit. On upgrade the unit is already
loaded with the previous venv's in-memory interpreter, so the
state: started no-op never picks up the new deps — restart
explicitly when the unit is already active.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
…te3 locking)

celery -B with the default PersistentScheduler stores its schedule
via shelve. On Python 3.13, shelve defaults to dbm.sqlite3, which
raises dbm.sqlite3.error: locking protocol intermittently under
contention — observed on x86 but not pi4-64 in this build matrix,
which is consistent with a benign-looking race specific to the
amd64 docker layer's filesystem ordering. When Beat stalls,
reconcile_stuck_processing and the other periodic tasks set up by
setup_periodic_tasks stop firing, so stuck-in-is_processing assets
never get re-dispatched.

setup_periodic_tasks defines every periodic task statically (no
django-celery-beat / no dynamic schedule edits), so a non-persistent
scheduler is sufficient. Switch to celery.beat.Scheduler in all
three compose files (prod template + dev + test) and drop the
--schedule /tmp/celerybeat-schedule flag that's now unused. The
telemetry cooldown comment is updated to reference the new flag —
the actual 24h cooldown is still gated by the Redis TTL, which is
the persisted source of truth.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
….2/v2

AssetViewV{1,1_1,1_2,2}.get / put / patch / update and the shared
DeleteAssetViewMixin / AssetContentViewMixin / ViewerCurrentAssetViewV1
all called Asset.objects.get(asset_id=...) bare. The Asset.DoesNotExist
that fires for a deleted-or-typo'd id has no DRF exception handler
registered, so it bubbled up as a 500 with the database traceback —
caller sees a server error for what is structurally a missing
resource. AssetRecheckViewV2 already gets this right via
filter(...).exists() + explicit 404; standardise the rest by routing
the lookup through django.shortcuts.get_object_or_404 (DRF's exception
handler converts the resulting Http404 to a clean 404 Response).

The new test_unknown_asset_id_returns_404 parametrises across every
API version so a future view that reverts to Asset.objects.get bare
trips immediately.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
@vpetersson
vpetersson requested a review from a team as a code owner May 12, 2026 19:58
@vpetersson
vpetersson requested a review from Copilot May 12, 2026 19:58

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

Addresses three e2e-discovered regressions affecting host installs, Celery beat reliability, and asset API error semantics.

Changes:

  • Split installer/runtime Python environments by provisioning a persistent venv for anthias-host-agent and restarting the unit on upgrade (bin/install.sh).
  • Switch Celery beat to the in-memory scheduler to avoid Python 3.13 shelve/dbm locking issues (compose files) and update related telemetry docs.
  • Return proper 404s for unknown asset_id across all API versions by using get_object_or_404, with a new parametrized regression test.

Reviewed changes

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

Show a summary per file
File Description
bin/install.sh Adds persistent host-agent venv provisioning and upgrade-time service restart.
docker-compose.yml.tmpl Uses in-memory Celery beat scheduler instead of persisted schedule file.
docker-compose.dev.yml Mirrors Celery scheduler change for dev.
docker-compose.test.yml Mirrors Celery scheduler change for tests/CI.
src/anthias_server/lib/telemetry.py Updates comment to reflect in-memory beat scheduler.
src/anthias_server/api/views/v2.py Uses get_object_or_404 for asset detail/update lookups.
src/anthias_server/api/views/v1.py Uses get_object_or_404 for asset detail/update and current-asset lookup.
src/anthias_server/api/views/v1_1.py Uses get_object_or_404 for asset detail/update lookups.
src/anthias_server/api/views/v1_2.py Uses get_object_or_404 for asset detail/update lookups.
src/anthias_server/api/views/mixins.py Uses get_object_or_404 in delete + content mixins.
src/anthias_server/api/tests/test_assets.py Adds regression test asserting 404 for unknown asset IDs across API versions.

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

Comment thread src/anthias_server/api/views/v1.py Outdated
get_object_or_404 returns a single Asset, not a queryset; the
variable name was already misleading under the previous bare
Asset.objects.get(...) call. Address Copilot review.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
INSTALLER_VENV lands in /tmp (the mktemp -t default), while uv's
cache lives at ~/.cache/uv on $HOME. On the typical Pi/Debian
install /tmp is tmpfs and $HOME is the SD card, so uv's default
hardlink mode fails for every wheel and falls back to a noisy
"Failed to hardlink files; falling back to full copy" line. Set
UV_LINK_MODE=copy on the install_ansible invocation so the
fallback becomes the documented choice. provision_host_agent_venv
is unaffected — both its venv and the uv cache live on $HOME, so
hardlinks work there.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>

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 10 out of 11 changed files in this pull request and generated no new comments.

Surfaced during e2e testing: after a compose recreate, anthias-server's
up -d emitted "Found orphan containers ([anthias-anthias-viewer-run-…])
… you can run this command with the --remove-orphans flag to clean it
up." These linger from earlier `docker compose run` invocations that
created run-NNN sidecar containers — without --remove-orphans they
just keep running and clutter `docker ps`. Apply to both the prod
upgrade path (upgrade_containers.sh) and the dev bring-up
(start_development_server.sh).

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
@sonarqubecloud

Copy link
Copy Markdown

@vpetersson
vpetersson merged commit f547642 into master May 12, 2026
9 checks passed
vpetersson added a commit that referenced this pull request May 12, 2026
* Master's #2881 (e2e-test findings) added an `installer_venv` persistent
  venv provisioned by `bin/install.sh::provision_host_agent_venv` and
  kept the systemd unit ExecStart pointed at that stable path. Drops the
  duplicate ansible-side `.anthias-venv` task and reverts the
  unit-template path I'd introduced — master's approach is upstream
  and avoids rewriting the unit path on devices installed pre-refactor.
* Master's #2880 replaced the gum UI with whiptail. The auto-merge took
  whiptail's wiring; my arm64-aware INTRO_MESSAGE / set_device_type /
  Pi-tag-skip branches survived alongside it.
* Master's #2878 viewer locale changes auto-merged with my arm64
  start_viewer.sh conditional.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
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.

2 participants