mirror of
https://github.com/Lidarr/Lidarr
synced 2026-05-08 12:33:04 +02:00
Cap scan/import parallelism via LIDARR_MEDIA_IO_PARALLELISM
Default max degree 2 to avoid IO storms on NFS/remote storage. Applies to folder scan, tag reads, and candidate scoring PLINQ. Document env var and Docker usage in MULTITHREAD_README.md. Made-with: Cursor
This commit is contained in:
parent
20e8ab9754
commit
a98d331b20
5 changed files with 101 additions and 3 deletions
57
MULTITHREAD_README.md
Normal file
57
MULTITHREAD_README.md
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
# Multithreaded library scan / import (this fork)
|
||||
|
||||
This branch adds a faster, **parallel** disk scan and import path. Upstream Lidarr does much of this work sequentially; this fork parallelizes folder scanning, tag reads, and release-candidate scoring.
|
||||
|
||||
A **Dockerfile.multithread** in this repository builds a self-contained binary and overlays it on `ghcr.io/linuxserver/lidarr:nightly` (see CI or build from repo root per that file’s comments). A wrapper layout that keeps this tree in a `lidarr-src/` subdirectory can use the parent `Dockerfile` instead.
|
||||
|
||||
## `LIDARR_MEDIA_IO_PARALLELISM`
|
||||
|
||||
Parallel import work is **not** limited by Lidarr’s download bandwidth or rate settings (those apply to indexers/clients only). On **slow or remote storage** (especially NFS), too much concurrency can saturate IOPS and make the host feel stuck. This variable caps how many workers run at once for the fork’s parallel paths.
|
||||
|
||||
| | |
|
||||
| --- | --- |
|
||||
| **Name** | `LIDARR_MEDIA_IO_PARALLELISM` |
|
||||
| **Default** | `2` (used when the variable is unset, empty, or not a valid integer) |
|
||||
| **Allowed** | Integers **1**–**64**; values below **1** fall back to the default |
|
||||
| **Scope** | Process environment (read once at first use) |
|
||||
|
||||
It applies to:
|
||||
|
||||
- parallel **folder scans** when collecting audio files;
|
||||
- parallel **tag / metadata reads** when building import decisions;
|
||||
- parallel **candidate release scoring** during identification.
|
||||
|
||||
**Docker:** fully supported. Set the variable on the container like any other env; the .NET process reads the container environment.
|
||||
|
||||
### Docker Compose
|
||||
|
||||
```yaml
|
||||
services:
|
||||
lidarr:
|
||||
image: your-registry/lidarr-nightly-multithread:latest
|
||||
environment:
|
||||
- PUID=1000
|
||||
- PGID=1000
|
||||
- TZ=Etc/UTC
|
||||
# Gentle on NFS / network mounts; omit for default (2)
|
||||
- LIDARR_MEDIA_IO_PARALLELISM=1
|
||||
```
|
||||
|
||||
### `docker run`
|
||||
|
||||
```bash
|
||||
docker run -e LIDARR_MEDIA_IO_PARALLELISM=4 … your-image
|
||||
```
|
||||
|
||||
### When to change it
|
||||
|
||||
- **NFS, SMB, or sluggish disks:** try `1` or leave default `2`.
|
||||
- **Library and app on fast local storage (e.g. same NAS app dataset, local SSD):** try `4`–`8` or higher (up to 64) and watch CPU, I/O, and responsiveness.
|
||||
|
||||
### Implementation reference
|
||||
|
||||
Logic and constant name: `src/NzbDrone.Common/MediaImportParallelism.cs`.
|
||||
|
||||
## Relationship to upstream
|
||||
|
||||
Behavior outside scan/import parallelism matches your chosen base (e.g. nightly image + overlaid build). For upstream docs and support channels, see [Lidarr](https://github.com/Lidarr/Lidarr) and the [Servarr wiki](https://wiki.servarr.com/lidarr).
|
||||
40
src/NzbDrone.Common/MediaImportParallelism.cs
Normal file
40
src/NzbDrone.Common/MediaImportParallelism.cs
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
using System;
|
||||
|
||||
namespace NzbDrone.Common
|
||||
{
|
||||
/// <summary>
|
||||
/// Limits parallel disk work during library scan/import (tag reads, folder scans, candidate scoring).
|
||||
/// Unrelated to download bandwidth limits in Lidarr settings.
|
||||
/// </summary>
|
||||
public static class MediaImportParallelism
|
||||
{
|
||||
public const string EnvironmentVariableName = "LIDARR_MEDIA_IO_PARALLELISM";
|
||||
|
||||
private const int DefaultMaxDegree = 2;
|
||||
private const int MinDegree = 1;
|
||||
private const int MaxDegreeCap = 64;
|
||||
|
||||
private static readonly Lazy<int> MaxDegreeLazy = new Lazy<int>(ReadMaxDegree);
|
||||
|
||||
/// <summary>
|
||||
/// Maximum concurrent workers for scan/import parallelism. Default 2; override with LIDARR_MEDIA_IO_PARALLELISM (1–64).
|
||||
/// </summary>
|
||||
public static int MaxDegreeOfParallelism => MaxDegreeLazy.Value;
|
||||
|
||||
private static int ReadMaxDegree()
|
||||
{
|
||||
var raw = Environment.GetEnvironmentVariable(EnvironmentVariableName);
|
||||
if (string.IsNullOrWhiteSpace(raw) || !int.TryParse(raw.Trim(), out var parsed))
|
||||
{
|
||||
return DefaultMaxDegree;
|
||||
}
|
||||
|
||||
if (parsed < MinDegree)
|
||||
{
|
||||
return DefaultMaxDegree;
|
||||
}
|
||||
|
||||
return Math.Min(parsed, MaxDegreeCap);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -126,7 +126,7 @@ public void Scan(List<string> folders = null, FilterFilesType filter = FilterFil
|
|||
|
||||
var musicFilesStopwatch = Stopwatch.StartNew();
|
||||
|
||||
Parallel.ForEach(foldersToScan, folder =>
|
||||
Parallel.ForEach(foldersToScan, new ParallelOptions { MaxDegreeOfParallelism = MediaImportParallelism.MaxDegreeOfParallelism }, folder =>
|
||||
{
|
||||
_logger.ProgressInfo("Scanning {0}", folder);
|
||||
|
||||
|
|
|
|||
|
|
@ -305,7 +305,7 @@ private void GetBestRelease(LocalAlbumRelease localAlbumRelease, List<CandidateA
|
|||
_logger.Debug("Matching {0} track files against {1} candidates", localAlbumRelease.TrackCount, candidateReleases.Count);
|
||||
_logger.Trace("Processing files:\n{0}", string.Join("\n", localAlbumRelease.LocalTracks.Select(x => x.Path)));
|
||||
|
||||
var maxParallelism = Math.Max(1, Environment.ProcessorCount);
|
||||
var maxParallelism = MediaImportParallelism.MaxDegreeOfParallelism;
|
||||
var scoredCandidates = candidateReleases
|
||||
.Select((candidateRelease, index) => new { candidateRelease, index })
|
||||
.AsParallel()
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using NLog;
|
||||
using NzbDrone.Common;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Common.Instrumentation.Extensions;
|
||||
using NzbDrone.Core.DecisionEngine;
|
||||
|
|
@ -106,7 +107,7 @@ public Tuple<List<LocalTrack>, List<ImportDecision<LocalTrack>>> GetLocalTracks(
|
|||
var processedTracks = new ConcurrentBag<(int Index, LocalTrack Track)>();
|
||||
var processedDecisions = new ConcurrentBag<(int Index, ImportDecision<LocalTrack> Decision)>();
|
||||
var progress = 0;
|
||||
var maxParallelism = Math.Max(1, Environment.ProcessorCount);
|
||||
var maxParallelism = MediaImportParallelism.MaxDegreeOfParallelism;
|
||||
var filesWithIndex = files.Select((file, index) => new { file, index }).ToList();
|
||||
|
||||
Parallel.ForEach(filesWithIndex, new ParallelOptions { MaxDegreeOfParallelism = maxParallelism }, item =>
|
||||
|
|
|
|||
Loading…
Reference in a new issue