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.pyruns as a oneshot systemd service- a systemd timer triggers it once per day
flockprevents 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.servicewinecellar-kiosk.service
Scheduled batch execution¶
wine-inventory-stock.timerwine-inventory-stock.servicestock_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:
- start
- execute the full stock workflow
- write results and logs
- exit cleanly
Because of that, it should not be configured as a permanent Restart=always daemon.
The correct model is:
Type=oneshotservicesystemd timerschedule
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=oneshotruns one batch and exitsWorkingDirectoryis the worker root- the worker venv is called explicitly
flockblocks 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=truecatches 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