Skip to content

Services & Scheduling

Purpose

This document defines the production service model for the Wine Platform on Raspberry Pi.

It standardizes:

  • service naming
  • daily stock scheduling
  • systemd unit structure
  • monitoring and logging commands
  • separation between always-on services and batch runtimes

Canonical unit names

Use these names consistently across the whole documentation set.

Purpose Canonical name
FastAPI backend winecellar-api.service
Qt kiosk frontend winecellar-kiosk.service
Daily stock batch wine-inventory-stock.service
Daily stock scheduler wine-inventory-stock.timer

Planned but not part of the daily scheduler:

Purpose Suggested future name
Motion-triggered runtime wine-inventory-motion.service

Canonical stock runtime path

Confirmed production path:

/home/pi/wine_platform/workers/wine_inventory/src/pince_shelf/cli/stock_runtime.py

Do not use the old outdated path under pince_shelf/vision/stock_runtime.py.


Production recommendation

The agreed scheduling solution is:

  • stock_runtime.py runs as a oneshot systemd service
  • a systemd timer triggers it once per day
  • flock prevents overlapping executions
  • logs are read with journalctl

This is preferred over cron because it gives:

  • better observability
  • cleaner boot ordering
  • missed-run recovery with Persistent=true
  • easier manual testing through the exact production path

Service model

Always-on services

  • winecellar-api.service
  • winecellar-kiosk.service

Scheduled batch execution

  • wine-inventory-stock.timer
  • wine-inventory-stock.service
  • stock_runtime.py

Separate event-driven path

Motion-triggered execution should stay separate from the daily timer chain.


Architecture overview

flowchart TD
    API[winecellar-api.service] --> DB[FastAPI backend and DB access]
    KIOSK[winecellar-kiosk.service] --> UI[Qt kiosk UI]
    TIMER[wine-inventory-stock.timer] --> SERVICE[wine-inventory-stock.service]
    SERVICE --> SCRIPT[stock_runtime.py]
    SCRIPT --> PIPELINE[Snapshot, rectify, crop, infer, compare]

Why stock_runtime.py is not a daemon

stock_runtime.py is a batch orchestrator. It should:

  1. start
  2. execute the full stock workflow
  3. write results and logs
  4. exit cleanly

Because of that, it should not be configured as a permanent Restart=always daemon.

The correct model is:

  • Type=oneshot service
  • systemd timer schedule

Canonical service unit

Create:

sudo nano /etc/systemd/system/wine-inventory-stock.service

Content:

[Unit]
Description=Wine Inventory Daily Stock Runtime
After=network-online.target
Wants=network-online.target

[Service]
Type=oneshot
User=pi
Group=pi
WorkingDirectory=/home/pi/wine_platform/workers/wine_inventory
ExecStart=/usr/bin/flock -n /tmp/wine_stock_runtime.lock /home/pi/wine_platform/workers/wine_inventory/.venv/bin/python /home/pi/wine_platform/workers/wine_inventory/src/pince_shelf/cli/stock_runtime.py
StandardOutput=journal
StandardError=journal

[Install]
WantedBy=multi-user.target

Notes

  • Type=oneshot runs one batch and exits
  • WorkingDirectory is the worker root
  • the worker venv is called explicitly
  • flock blocks overlapping runs
  • stdout and stderr go to the systemd journal

Canonical timer unit

Create:

sudo nano /etc/systemd/system/wine-inventory-stock.timer

Content:

[Unit]
Description=Run Wine Inventory Stock Runtime Daily

[Timer]
OnCalendar=*-*-* 03:00:00
Persistent=true
Unit=wine-inventory-stock.service

[Install]
WantedBy=timers.target

Notes

  • runs every day at 03:00
  • Persistent=true catches up a missed run after reboot
  • the timer triggers the service, not the Python script directly

Execution flow

sequenceDiagram
    participant Timer as wine-inventory-stock.timer
    participant Service as wine-inventory-stock.service
    participant Lock as flock
    participant Script as stock_runtime.py
    participant Journal as journalctl

    Timer->>Service: trigger daily at 03:00
    Service->>Lock: request non-blocking lock
    alt lock acquired
        Lock-->>Service: ok
        Service->>Script: start runtime
        Script->>Journal: write stdout and stderr
        Script-->>Service: exit code
    else already running
        Lock-->>Service: denied
        Service->>Journal: skip second run
    end

Installation and activation

sudo systemctl daemon-reload
sudo systemctl enable --now wine-inventory-stock.timer

Verify:

systemctl list-timers --all | grep wine-inventory-stock
systemctl status wine-inventory-stock.timer --no-pager
systemctl status wine-inventory-stock.service --no-pager

Manual test procedure

Run one real production-style batch:

sudo systemctl start wine-inventory-stock.service

Inspect:

systemctl status wine-inventory-stock.service --no-pager
journalctl -u wine-inventory-stock.service -n 200 --no-pager
journalctl -u wine-inventory-stock.service -f

Monitoring

Backend

journalctl -u winecellar-api.service -f

Kiosk

journalctl -u winecellar-kiosk.service -f

Daily stock runtime

journalctl -u wine-inventory-stock.service -f

Timers

systemctl list-timers --all | grep wine-inventory

Fallback only: cron

Cron is acceptable only as an emergency fallback.

Example fallback command:

0 3 * * * /usr/bin/flock -n /tmp/wine_stock_runtime.lock /home/pi/wine_platform/workers/wine_inventory/.venv/bin/python /home/pi/wine_platform/workers/wine_inventory/src/pince_shelf/cli/stock_runtime.py

This is not the preferred documented production solution.


Final recommendation

Use this production model everywhere in the docs:

wine-inventory-stock.timer
    → wine-inventory-stock.service
        → /home/pi/wine_platform/workers/wine_inventory/src/pince_shelf/cli/stock_runtime.py

Backup Service (Versioned NAS Backup)

Purpose

Daily versioned backup of wine_platform to NAS.

Architecture

timer → service → backup script → NAS

Service

ExecStart=/home/pi/wine_platform/tools/backup_wine_platform.sh

Timer

OnCalendar=--* 02:00:00 Persistent=true

Behavior

  • creates /daily/YYYY-MM-DD
  • keeps last 5 backups
  • removes older ones

Safe folder

/mnt/nasData/wine_platform_backup/safe/

Rules:

  • manual only
  • never modified by automation