Skip to content

Commit 21a5a69

Browse files
Run MWL backup/reset on os-defined schedule
1 parent b3a5cf0 commit 21a5a69

10 files changed

Lines changed: 62 additions & 229 deletions

File tree

.env.development

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,7 @@ MWL_AET=SCREENING_MWL
1111
MWL_PORT=4243
1212
MWL_DB_PATH=/var/lib/pacs/worklist.db
1313

14-
# MWL Reset Scheduler Configuration
1514
BACKUP_PATH=/var/lib/pacs/backups
16-
MWL_RESET_SCHEDULE=0 2 * * *
1715

1816
# PACS Server Configuration
1917
PACS_AET=SCREENING_PACS

compose.yml

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -93,21 +93,6 @@ services:
9393
condition: service_healthy
9494
restart: unless-stopped
9595

96-
reset:
97-
build:
98-
context: .
99-
dockerfile: Dockerfile
100-
container_name: mwl-reset
101-
command: ["uv", "run", "python", "-m", "reset_main"]
102-
volumes:
103-
- pacs-db:/var/lib/pacs
104-
environment:
105-
- MWL_DB_PATH=/var/lib/pacs/worklist.db
106-
- BACKUP_PATH=/var/lib/pacs/backups
107-
- MWL_RESET_SCHEDULE=0 2 * * *
108-
- LOG_LEVEL=INFO
109-
restart: unless-stopped
110-
11196
volumes:
11297
pacs-storage:
11398
driver: local

docs/adr/ADR-004_MWL_Daily_Backup_And_Reset.md

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,22 +12,22 @@ The worklist is inherently ephemeral: appointments are scheduled per clinic, and
1212

1313
The gateway MWL is not intended to be a canonical or long-term data store. Retaining worklist items indefinitely would cause the database to grow to unwieldy proportions and would give a false impression of data durability.
1414

15+
The gateway runs as a Python process on a Windows VM managed by Azure Arc. Scheduling therefore belongs in the infrastructure layer (Windows Task Scheduler, configurable via Arc).
16+
1517
## Decision
1618

17-
Reset the MWL database on a configurable schedule (default: daily at 02:00 UTC) by:
19+
Reset the MWL database on a schedule managed by Windows Task Scheduler by:
1820

1921
1. Backing up the database before clearing it, using SQLite's native `conn.backup()` API
2022
2. Deleting all rows from `worklist_items`
2123

22-
This runs as a dedicated `reset` container (`reset_main.py` / `MWLResetScheduler`).
23-
24-
The schedule is configured via a cron expression (`MWL_RESET_SCHEDULE`), which allows any cadence (daily, weekly, etc) to be expressed in a single environment variable without code changes.
24+
`reset_main.py` is performs these two steps and exits. Windows Task Scheduler (registered via `scripts/bat/schtasks.bat`) invokes it on whatever schedule is configured in infrastructure.
2525

2626
**Alternatives considered:**
2727

28-
- **Reset on startup only** — would not handle long-running deployments where the container is not restarted between clinics
28+
- **In-process Python scheduler** (`croniter` sleep loop) — puts scheduling logic in application code; duplicates a facility the OS already provides; requires a long-running process that serves no other purpose
29+
- **Reset on startup only** — would not handle long-running deployments where the process is not restarted between clinics
2930
- **No reset** — leads to unbounded growth and stale data visible to the modality
30-
- **Interval-based env vars** (`RESET_INTERVAL=daily`, `RESET_TIME=02:00`, `RESET_DAY=monday`) — more verbose config, requires code changes to support new intervals; cron expression is more expressive in a single value
3131

3232
## Consequences
3333

@@ -36,11 +36,11 @@ The schedule is configured via a cron expression (`MWL_RESET_SCHEDULE`), which a
3636
- Worklist is clean at the start of each clinic with no manual intervention
3737
- Database size remains bounded
3838
- Backup before clear means data is recoverable if needed
39-
- Schedule is fully configurable via env var without code changes
40-
- Follows the existing one-process-per-container pattern
39+
- Schedule is owned by infrastructure (Arc / Task Scheduler), not application code — no code change needed to adjust timing
40+
- `reset_main.py` is a simple, testable, one-shot script with no scheduler machinery
4141

4242
### Negative Consequences
4343

44-
- Adds a fifth service to the deployment
44+
- Schedule configuration lives outside the codebase; requires infrastructure access to change
4545
- Any worklist items written very close to the reset time could be cleared before the modality queries them. This is mitigated by choosing a reset time well outside clinic hours
4646
- Backups accumulate on disk and will need periodic pruning; this is not currently automated

docs/mwl/README.md

Lines changed: 33 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@ The MWL server is a lightweight, production-ready DICOM worklist solution that:
99
- Provides scheduled procedure information via [DICOM C-FIND](https://dicom.nema.org/medical/dicom/current/output/html/part04.html#chapter_C) protocol
1010
- Stores worklist items in SQLite database
1111
- Supports filtering by modality, date, and patient ID
12-
- Resets the worklist on a schedule, backing up the database before clearing it
13-
- Runs in a separate container alongside the [PACS Server](../pacs/README.md)
12+
- Resets the worklist daily via a scheduled `reset_main.py` script invoked by Windows Task Scheduler
13+
- Runs as a Python process on a Windows VM managed by Azure Arc
1414

1515
## Architecture
1616

@@ -26,12 +26,12 @@ The MWL server is a lightweight, production-ready DICOM worklist solution that:
2626
│ │ Handler │ │ Layer │ │ Database │ │ │
2727
│ └──────────────┘ └──────────────┘ └──────────┘ │ │
2828
│ │ ▲ │ │
29-
│ │ │ │ backup + clear (cron)
30-
│ └──────────────────────┘ │
29+
│ │ │ │ backup + clear
30+
│ └──────────────────────┘ │ (Task Scheduler)
3131
│ Query & Response │ │
3232
└─────────────────────────────────────────────────────────────┘ │
33-
▲ ┌──────┴─────────┐
34-
│ Reset Scheduler
33+
▲ ▲ ┌───────┴────────┐
34+
│ │ │ reset_main.py
3535
┌──────┴──────┐ ┌───────┴────────┐ └────────────────┘
3636
│ Modality │ │ Relay Listener │
3737
│ (SCU) │ │ (Populates DB) │
@@ -45,27 +45,25 @@ The MWL server is a lightweight, production-ready DICOM worklist solution that:
4545
3. **Filtering**: MWL server filters by modality, date, patient ID, status
4646
4. **Response**: Server returns matching worklist items to modality
4747
5. **Status Updates**: C-STORE receipt transitions items from `SCHEDULED` to `IN PROGRESS`; [MPPS](https://dicom.nema.org/medical/dicom/current/output/html/part04.html#chapter_F) transitions items to `COMPLETED` or `DISCONTINUED`
48-
6. **Reset**: Reset scheduler backs up and clears the database on a configurable schedule
48+
6. **Daily Reset**: Windows Task Scheduler invokes `reset_main.py`, which backs up and clears the database
4949

5050
## Running the MWL Server
5151

52-
The MWL server runs in a separate container:
52+
The gateway runs as a Python process on a Windows VM:
5353

5454
```bash
55-
# Start all services
56-
docker compose up -d
55+
# Start the MWL server
56+
uv run python -m mwl_main
5757

58-
# Start only the MWL server and reset scheduler
59-
docker compose up -d mwl reset
60-
61-
# View MWL server logs
62-
docker compose logs -f mwl
58+
# Run a one-shot backup and reset (normally invoked by Task Scheduler)
59+
uv run python -m reset_main
60+
```
6361

64-
# View reset scheduler logs
65-
docker compose logs -f reset
62+
For local development, Docker Compose is available:
6663

67-
# Stop all services
68-
docker compose down
64+
```bash
65+
docker compose up -d mwl
66+
docker compose logs -f mwl
6967
```
7068

7169
## Configuration
@@ -79,22 +77,25 @@ docker compose down
7977
| `MWL_DB_PATH` | `/var/lib/pacs/worklist.db` | SQLite database path |
8078
| `LOG_LEVEL` | `INFO` | Logging level |
8179

82-
### Reset scheduler
80+
### Reset script (`reset_main.py`)
8381

8482
| Variable | Default | Description |
8583
|----------|---------|-------------|
8684
| `MWL_DB_PATH` | `/var/lib/pacs/worklist.db` | SQLite database path |
8785
| `BACKUP_PATH` | `/var/lib/pacs/backups` | Directory for database backups |
88-
| `MWL_RESET_SCHEDULE` | `0 2 * * *` | Cron expression for reset schedule (UTC) |
8986
| `LOG_LEVEL` | `INFO` | Logging level |
9087

91-
The `MWL_RESET_SCHEDULE` value is a standard cron expression. Examples:
88+
The reset schedule is configured in Windows Task Scheduler (registered via `scripts/bat/schtasks.bat`), not in the application.
89+
90+
## Scheduling the daily reset (Windows)
91+
92+
Register the scheduled task (run once, on the gateway VM):
93+
94+
```bat
95+
scripts\bat\schtasks.bat
96+
```
9297

93-
| Expression | Schedule |
94-
|------------|----------|
95-
| `0 2 * * *` | Daily at 02:00 UTC (default) |
96-
| `0 2 * * 1` | Every Monday at 02:00 UTC |
97-
| `0 2 1 * *` | First day of each month at 02:00 UTC |
98+
This creates a Task Scheduler task that runs `reset_main.py` daily at midnight. The schedule can be adjusted in Task Scheduler or via Azure Arc without any code change.
9899

99100
## Example query
100101

@@ -131,14 +132,14 @@ assoc.release()
131132
Check worklist items:
132133

133134
```bash
134-
docker compose exec gateway sqlite3 /var/lib/pacs/worklist.db \
135+
sqlite3 /var/lib/pacs/worklist.db \
135136
"SELECT accession_number, patient_name, scheduled_date, status FROM worklist_items;"
136137
```
137138

138139
Add test worklist item:
139140

140141
```bash
141-
docker compose exec gateway sqlite3 /var/lib/pacs/worklist.db <<EOF
142+
sqlite3 /var/lib/pacs/worklist.db <<EOF
142143
INSERT INTO worklist_items (
143144
accession_number, patient_id, patient_name, patient_birth_date,
144145
scheduled_date, scheduled_time, modality, study_description
@@ -158,29 +159,12 @@ uv run pytest tests/integration/test_c_find_returns_worklist_items.py -v
158159
uv run pytest tests/integration/test_request_cfind_on_worklist.py -v
159160
```
160161

161-
## Multi-container architecture
162-
163-
The MWL-related services run in separate containers. See [ADR-003: Separate containers for PACS and MWL](../adr/ADR-003_Separate_containers_for_PACS_and_MWL.md) and [ADR-004: Daily backup and reset of the MWL database](../adr/ADR-004_MWL_Daily_Backup_And_Reset.md) for the architectural decisions.
164-
165-
**Docker Compose services:**
166-
167-
```yaml
168-
services:
169-
mwl:
170-
container_name: mwl-server
171-
command: ["uv", "run", "python", "-m", "mwl_main"]
172-
ports:
173-
- "4243:4243"
174-
175-
reset:
176-
container_name: mwl-reset
177-
command: ["uv", "run", "python", "-m", "reset_main"]
178-
```
179-
180-
**Worklist item status transitions:**
162+
## Worklist item status transitions
181163

182164
```
183165
SCHEDULED ──(first C-STORE)──▶ IN PROGRESS ──(MPPS N-SET)──▶ COMPLETED
184166
185167
└────────(MPPS N-SET)──▶ DISCONTINUED
186168
```
169+
170+
See [ADR-003: Separate containers for PACS and MWL](../adr/ADR-003_Separate_containers_for_PACS_and_MWL.md) and [ADR-004: Daily backup and reset of the MWL database](../adr/ADR-004_MWL_Daily_Backup_And_Reset.md) for architectural decisions.

pyproject.toml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ dependencies = [
1616
"requests>=2.32.0",
1717
"python-dotenv>=1.2.1",
1818
"azure-monitor-opentelemetry>=1.8.6",
19-
"croniter>=6.2.2",
2019
]
2120

2221
[dependency-groups]

scripts/bat/backup_database.bat

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
:: Batch file to run the backup_database.py script
1+
:: Backs up and resets the MWL worklist database. Intended to be run via Windows Task Scheduler.
22
@echo off
3-
set %rootdir=%1
4-
python "%rootdir%\scripts\python\backup_database.py"
3+
set rootdir=%~1
4+
"%rootdir%\.venv\Scripts\python.exe" -m reset_main

src/reset_main.py

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1-
"""Entry point for MWL daily backup and reset scheduler."""
1+
"""MWL backup and reset script. Intended to be invoked by Windows Task Scheduler (or equivalent)."""
22

33
import logging
44
import os
5+
import sys
56

67
from dotenv import load_dotenv
78

8-
from services.mwl.reset import MWLResetScheduler
99
from services.storage import MWLStorage
1010
from telemetry import configure_telemetry
1111

@@ -15,29 +15,35 @@
1515

1616
def main():
1717
"""
18-
Main entry point for MWL reset scheduler.
18+
Backs up and clears the MWL database. Exits with code 1 if the clear fails.
1919
2020
Environment variables:
21-
MWL_DB_PATH: Path to the MWL SQLite database (default: /var/lib/pacs/worklist.db)
22-
BACKUP_PATH: Directory for database backups (default: /var/lib/pacs/backups)
23-
MWL_RESET_SCHEDULE: Cron expression for reset schedule in UTC (default: 0 2 * * * — daily at 02:00)
21+
MWL_DB_PATH: Path to the MWL SQLite database (default: /var/lib/pacs/worklist.db)
22+
BACKUP_PATH: Directory for database backups (default: /var/lib/pacs/backups)
2423
"""
2524
logging.basicConfig(
2625
level=os.getenv("LOG_LEVEL", "INFO").upper(),
2726
format=os.getenv("LOG_FORMAT", "%(asctime)s - %(name)s - %(levelname)s - %(message)s"),
2827
)
28+
logger = logging.getLogger(__name__)
2929

3030
mwl_db_path = os.getenv("MWL_DB_PATH", "/var/lib/pacs/worklist.db")
3131
backup_path = os.getenv("BACKUP_PATH", "/var/lib/pacs/backups")
32-
reset_schedule = os.getenv("MWL_RESET_SCHEDULE", "0 2 * * *")
3332

3433
mwl_storage = MWLStorage(mwl_db_path)
35-
scheduler = MWLResetScheduler(mwl_storage, backup_path, reset_schedule)
3634

3735
try:
38-
scheduler.run()
39-
except KeyboardInterrupt:
40-
logging.info("Received shutdown signal")
36+
path = mwl_storage.backup(backup_path)
37+
logger.info(f"Backup complete: {path}")
38+
except Exception as e:
39+
logger.error(f"MWL backup failed: {e}", exc_info=True)
40+
41+
try:
42+
count = mwl_storage.clear()
43+
logger.info(f"MWL reset complete: {count} items deleted")
44+
except Exception as e:
45+
logger.error(f"MWL clear failed: {e}", exc_info=True)
46+
sys.exit(1)
4147

4248

4349
if __name__ == "__main__":

src/services/mwl/reset.py

Lines changed: 0 additions & 46 deletions
This file was deleted.

0 commit comments

Comments
 (0)