let user set ionice in settings
Continuous Integration / backend-tests (push) Successful in 1m20s
Continuous Integration / frontend-check (push) Successful in 51s
Continuous Integration / e2e-tests (push) Successful in 6m55s

This commit is contained in:
2026-05-05 22:07:30 -04:00
parent 65860e0408
commit f5ddfed38b
4 changed files with 89 additions and 24 deletions
+48
View File
@@ -4,6 +4,54 @@ import sys
from loguru import logger 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: def get_path_uuid(path: str) -> str | None:
"""Attempts to retrieve a stable hardware/filesystem UUID for a given path.""" """Attempts to retrieve a stable hardware/filesystem UUID for a given path."""
if not os.path.exists(path): if not os.path.exists(path):
+5
View File
@@ -307,6 +307,10 @@ class ArchiverService:
) )
JobManager.add_job_log(job_id, f"Starting backup to {media_record.identifier}") 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) workload_batch = self.assemble_backup_batch(db_session, media_id)
if not workload_batch: if not workload_batch:
JobManager.add_job_log(job_id, "No files require backup") JobManager.add_job_log(job_id, "No files require backup")
@@ -744,6 +748,7 @@ class ArchiverService:
logger.exception(f"Archival failed: {e}") logger.exception(f"Archival failed: {e}")
JobManager.fail_job(job_id, str(e)) JobManager.fail_job(job_id, str(e))
finally: finally:
set_process_priority("normal")
# Clean up any residual staging files # Clean up any residual staging files
for chunk_file in os.listdir(self.staging_directory): for chunk_file in os.listdir(self.staging_directory):
if chunk_file.startswith("backup_") and chunk_file.endswith(".tar"): if chunk_file.startswith("backup_") and chunk_file.endswith(".tar"):
+6 -19
View File
@@ -169,23 +169,6 @@ class ScannerService:
return return
time.sleep(0.1) 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( def compute_sha256(
self, file_path: str, job_id: Optional[int] = None self, file_path: str, job_id: Optional[int] = None
) -> Optional[str]: ) -> Optional[str]:
@@ -231,7 +214,9 @@ class ScannerService:
JobManager.update_job(job_id, 0.0, "Starting system scan...") JobManager.update_job(job_id, 0.0, "Starting system scan...")
JobManager.add_job_log(job_id, "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: with self._metrics_lock:
self.files_processed = 0 self.files_processed = 0
self.files_new = 0 self.files_new = 0
@@ -434,7 +419,9 @@ class ScannerService:
with self._metrics_lock: with self._metrics_lock:
self.is_hashing = True self.is_hashing = True
self._set_process_priority("background") from app.core.utils import set_process_priority
set_process_priority("background")
try: try:
with SessionLocal() as db_session: with SessionLocal() as db_session:
+30 -5
View File
@@ -20,7 +20,8 @@
Upload, Upload,
Terminal, Terminal,
Globe, Globe,
Key Key,
ChevronDown
} from "lucide-svelte"; } from "lucide-svelte";
import { Button } from "$lib/components/ui/button"; import { Button } from "$lib/components/ui/button";
import PageHeader from "$lib/components/ui/PageHeader.svelte"; import PageHeader from "$lib/components/ui/PageHeader.svelte";
@@ -51,6 +52,7 @@
let scanSchedule = $state(""); let scanSchedule = $state("");
let archivalSchedule = $state(""); let archivalSchedule = $state("");
let notificationUrls = $state<string[]>([]); let notificationUrls = $state<string[]>([]);
let ioniceLevel = $state("idle");
// Secrets keystore // Secrets keystore
let secretsList = $state<string[]>([]); let secretsList = $state<string[]>([]);
@@ -66,7 +68,8 @@
globalExclusions, globalExclusions,
scanSchedule, scanSchedule,
archivalSchedule, archivalSchedule,
notificationUrls notificationUrls,
ioniceLevel
})); }));
beforeNavigate((navigation: any) => { beforeNavigate((navigation: any) => {
@@ -155,6 +158,7 @@
if (data.schedule_scan) scanSchedule = data.schedule_scan; if (data.schedule_scan) scanSchedule = data.schedule_scan;
if (data.schedule_archival) archivalSchedule = data.schedule_archival; if (data.schedule_archival) archivalSchedule = data.schedule_archival;
if (data.notification_urls) notificationUrls = JSON.parse(data.notification_urls); if (data.notification_urls) notificationUrls = JSON.parse(data.notification_urls);
if (data.ionice_level) ioniceLevel = data.ionice_level;
} }
// Load secrets // Load secrets
@@ -169,7 +173,8 @@
globalExclusions, globalExclusions,
scanSchedule, scanSchedule,
archivalSchedule, archivalSchedule,
notificationUrls notificationUrls,
ioniceLevel
}); });
} catch (error) { } catch (error) {
toast.error("Failed to load system configuration"); toast.error("Failed to load system configuration");
@@ -188,7 +193,8 @@
updateSettings({ body: { key: "global_exclusions", value: globalExclusions } }), updateSettings({ body: { key: "global_exclusions", value: globalExclusions } }),
updateSettings({ body: { key: "schedule_scan", value: scanSchedule } }), updateSettings({ body: { key: "schedule_scan", value: scanSchedule } }),
updateSettings({ body: { key: "schedule_archival", value: archivalSchedule } }), 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 // Snapshot saved state
@@ -199,7 +205,8 @@
globalExclusions, globalExclusions,
scanSchedule, scanSchedule,
archivalSchedule, archivalSchedule,
notificationUrls notificationUrls,
ioniceLevel
}); });
toast.success("System configuration committed"); toast.success("System configuration committed");
@@ -648,6 +655,24 @@
{:else if activeTab === 'system'} {:else if activeTab === 'system'}
<div class="animate-in slide-in-from-bottom-4 duration-500 space-y-6"> <div class="animate-in slide-in-from-bottom-4 duration-500 space-y-6">
<Card class="p-5 shadow-xl">
<SectionHeader title="I/O scheduling" icon={Cpu} class="mb-6 px-0" />
<div class="space-y-4">
<div class="space-y-2">
<label class="text-xs font-medium text-text-secondary ml-1" for="ionice-level">Background job I/O priority</label>
<div class="relative">
<select id="ionice-level" bind:value={ioniceLevel} class="w-full h-10 bg-bg-primary border border-border-color rounded-xl px-4 pr-10 text-sm font-medium text-text-primary outline-none focus:ring-2 focus:ring-blue-500/20 transition-all appearance-none cursor-pointer">
<option value="idle">Idle (only use I/O when system is free)</option>
<option value="best-effort">Best-effort (normal scheduling)</option>
<option value="realtime">Real-time (highest priority, requires root)</option>
</select>
<ChevronDown size={16} class="absolute right-3 top-1/2 -translate-y-1/2 text-text-secondary pointer-events-none" />
</div>
<p class="text-[10px] text-text-secondary leading-tight opacity-60">Applies to scan and backup jobs. Idle is recommended for production systems.</p>
</div>
</div>
</Card>
<Card class="p-5 shadow-xl"> <Card class="p-5 shadow-xl">
<SectionHeader title="Index management" icon={Database} class="mb-6 px-0" /> <SectionHeader title="Index management" icon={Database} class="mb-6 px-0" />
<div class="grid grid-cols-2 gap-4"> <div class="grid grid-cols-2 gap-4">