From f5ddfed38bc5cba9a4cff34db8774c3f1e05fa2c Mon Sep 17 00:00:00 2001 From: Adam Lamers Date: Tue, 5 May 2026 22:07:30 -0400 Subject: [PATCH] let user set ionice in settings --- backend/app/core/utils.py | 48 +++++++++++++++++++++++ backend/app/services/archiver.py | 5 +++ backend/app/services/scanner.py | 25 +++--------- frontend/src/routes/settings/+page.svelte | 35 ++++++++++++++--- 4 files changed, 89 insertions(+), 24 deletions(-) diff --git a/backend/app/core/utils.py b/backend/app/core/utils.py index dadce62..76c6cfe 100644 --- a/backend/app/core/utils.py +++ b/backend/app/core/utils.py @@ -4,6 +4,54 @@ import sys from loguru import logger +def _get_ionice_setting() -> str: + """Reads the user's preferred I/O scheduling class from settings.""" + try: + from app.db.database import SessionLocal + from app.db import models + + with SessionLocal() as db_session: + record = ( + db_session.query(models.SystemSetting) + .filter(models.SystemSetting.key == "ionice_level") + .first() + ) + if record and record.value in ("idle", "best-effort", "realtime"): + return record.value + except Exception: + pass + return "idle" # Default: be the most polite + + +def set_process_priority(level: str): + """Adjusts CPU and I/O priority of the current process. + + Args: + level: "background" for lowest priority (ionice idle + nice 19), + "normal" to reset (ionice best-effort + nice 0). + """ + try: + import psutil + + p = psutil.Process(os.getpid()) + if level == "background": + ionice_level = _get_ionice_setting() + if hasattr(p, "ionice"): + if ionice_level == "idle": + p.ionice(psutil.IOPRIO_CLASS_IDLE) # type: ignore[attr-defined] + elif ionice_level == "realtime": + p.ionice(psutil.IOPRIO_CLASS_RT) # type: ignore[attr-defined] + else: + p.ionice(psutil.IOPRIO_CLASS_BE) # type: ignore[attr-defined] + p.nice(19) + else: + if hasattr(p, "ionice"): + p.ionice(psutil.IOPRIO_CLASS_BE) # type: ignore[attr-defined] + p.nice(0) + except Exception as e: + logger.debug(f"Could not set process priority to '{level}': {e}") + + def get_path_uuid(path: str) -> str | None: """Attempts to retrieve a stable hardware/filesystem UUID for a given path.""" if not os.path.exists(path): diff --git a/backend/app/services/archiver.py b/backend/app/services/archiver.py index 8ad1741..7ee5ff9 100644 --- a/backend/app/services/archiver.py +++ b/backend/app/services/archiver.py @@ -307,6 +307,10 @@ class ArchiverService: ) JobManager.add_job_log(job_id, f"Starting backup to {media_record.identifier}") + from app.core.utils import set_process_priority + + set_process_priority("background") + workload_batch = self.assemble_backup_batch(db_session, media_id) if not workload_batch: JobManager.add_job_log(job_id, "No files require backup") @@ -744,6 +748,7 @@ class ArchiverService: logger.exception(f"Archival failed: {e}") JobManager.fail_job(job_id, str(e)) finally: + set_process_priority("normal") # Clean up any residual staging files for chunk_file in os.listdir(self.staging_directory): if chunk_file.startswith("backup_") and chunk_file.endswith(".tar"): diff --git a/backend/app/services/scanner.py b/backend/app/services/scanner.py index ad8171f..6375700 100644 --- a/backend/app/services/scanner.py +++ b/backend/app/services/scanner.py @@ -169,23 +169,6 @@ class ScannerService: return time.sleep(0.1) - def _set_process_priority(self, level: str): - """Adjusts the CPU and I/O priority of the current process.""" - try: - p = psutil.Process(os.getpid()) - if level == "background": - if hasattr(p, "ionice"): - p.ionice( - psutil.IOPRIO_CLASS_IDLE # ty: ignore[unresolved-attribute] - ) - p.nice(19) - else: - if hasattr(p, "ionice"): - p.ionice(psutil.IOPRIO_CLASS_BE) # ty: ignore[unresolved-attribute] - p.nice(0) - except Exception: - pass - def compute_sha256( self, file_path: str, job_id: Optional[int] = None ) -> Optional[str]: @@ -231,7 +214,9 @@ class ScannerService: JobManager.update_job(job_id, 0.0, "Starting system scan...") JobManager.add_job_log(job_id, "Starting system scan") - self._set_process_priority("normal") + from app.core.utils import set_process_priority + + set_process_priority("normal") with self._metrics_lock: self.files_processed = 0 self.files_new = 0 @@ -434,7 +419,9 @@ class ScannerService: with self._metrics_lock: self.is_hashing = True - self._set_process_priority("background") + from app.core.utils import set_process_priority + + set_process_priority("background") try: with SessionLocal() as db_session: diff --git a/frontend/src/routes/settings/+page.svelte b/frontend/src/routes/settings/+page.svelte index 622604e..8d004f9 100644 --- a/frontend/src/routes/settings/+page.svelte +++ b/frontend/src/routes/settings/+page.svelte @@ -20,7 +20,8 @@ Upload, Terminal, Globe, - Key + Key, + ChevronDown } from "lucide-svelte"; import { Button } from "$lib/components/ui/button"; import PageHeader from "$lib/components/ui/PageHeader.svelte"; @@ -51,6 +52,7 @@ let scanSchedule = $state(""); let archivalSchedule = $state(""); let notificationUrls = $state([]); + let ioniceLevel = $state("idle"); // Secrets keystore let secretsList = $state([]); @@ -66,7 +68,8 @@ globalExclusions, scanSchedule, archivalSchedule, - notificationUrls + notificationUrls, + ioniceLevel })); beforeNavigate((navigation: any) => { @@ -155,6 +158,7 @@ if (data.schedule_scan) scanSchedule = data.schedule_scan; if (data.schedule_archival) archivalSchedule = data.schedule_archival; if (data.notification_urls) notificationUrls = JSON.parse(data.notification_urls); + if (data.ionice_level) ioniceLevel = data.ionice_level; } // Load secrets @@ -169,7 +173,8 @@ globalExclusions, scanSchedule, archivalSchedule, - notificationUrls + notificationUrls, + ioniceLevel }); } catch (error) { toast.error("Failed to load system configuration"); @@ -188,7 +193,8 @@ updateSettings({ body: { key: "global_exclusions", value: globalExclusions } }), updateSettings({ body: { key: "schedule_scan", value: scanSchedule } }), updateSettings({ body: { key: "schedule_archival", value: archivalSchedule } }), - updateSettings({ body: { key: "notification_urls", value: JSON.stringify(notificationUrls) } }) + updateSettings({ body: { key: "notification_urls", value: JSON.stringify(notificationUrls) } }), + updateSettings({ body: { key: "ionice_level", value: ioniceLevel } }) ]); // Snapshot saved state @@ -199,7 +205,8 @@ globalExclusions, scanSchedule, archivalSchedule, - notificationUrls + notificationUrls, + ioniceLevel }); toast.success("System configuration committed"); @@ -648,6 +655,24 @@ {:else if activeTab === 'system'}
+ + +
+
+ +
+ + +
+

Applies to scan and backup jobs. Idle is recommended for production systems.

+
+
+
+