Fix media parallelism defaults and observability

- Unset/invalid/0: use Environment.ProcessorCount (restore pre-cap behavior)
- Drop Lazy cache; re-read env when parallel work runs
- Log effective MaxDegree once per process on first disk scan
- Update MULTITHREAD_README for NFS opt-in vs default

Made-with: Cursor
This commit is contained in:
Sean Parsons 2026-03-27 21:39:40 +00:00
parent a98d331b20
commit c0aec91d50
3 changed files with 28 additions and 14 deletions

View file

@ -11,9 +11,9 @@ Parallel import work is **not** limited by Lidarrs download bandwidth or rate
| | |
| --- | --- |
| **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) |
| **Default** | **Unset / empty / invalid / `0`:** same as **`Environment.ProcessorCount`** (original fork behavior). For **NFS or slow storage**, set **`1`** or **`2`** explicitly. |
| **Allowed** | **`1``64`:** use that cap. **`0`:** treat as unset (processor count). |
| **Scope** | Process environment (re-read on each scan/import parallel section) |
It applies to:
@ -23,6 +23,8 @@ It applies to:
**Docker:** fully supported. Set the variable on the container like any other env; the .NET process reads the container environment.
On the first disk scan, Lidarr logs a line like `Media import parallelism: MaxDegreeOfParallelism=…` so you can confirm the value it sees (useful if compose/env typos leave the variable unset).
### Docker Compose
```yaml
@ -33,7 +35,7 @@ services:
- PUID=1000
- PGID=1000
- TZ=Etc/UTC
# Gentle on NFS / network mounts; omit for default (2)
# Gentle on NFS / network mounts (omit var for processor-count default)
- LIDARR_MEDIA_IO_PARALLELISM=1
```

View file

@ -3,35 +3,34 @@
namespace NzbDrone.Common
{
/// <summary>
/// Limits parallel disk work during library scan/import (tag reads, folder scans, candidate scoring).
/// Caps parallel disk work during library scan/import (tag reads, folder scans, candidate scoring), optional via env.
/// 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 (164).
/// Maximum concurrent workers for scan/import parallelism.
/// If <c>LIDARR_MEDIA_IO_PARALLELISM</c> is unset, empty, invalid, 0, or negative: uses <see cref="Environment.ProcessorCount"/> (matches pre-cap fork behavior).
/// Otherwise uses the set value clamped to 164.
/// Re-reads the environment each call so container/env changes are visible without restart (same process still needs a new read on next scan).
/// </summary>
public static int MaxDegreeOfParallelism => MaxDegreeLazy.Value;
public static int MaxDegreeOfParallelism => ReadMaxDegree();
private static int ReadMaxDegree()
{
var raw = Environment.GetEnvironmentVariable(EnvironmentVariableName);
if (string.IsNullOrWhiteSpace(raw) || !int.TryParse(raw.Trim(), out var parsed))
{
return DefaultMaxDegree;
return Math.Max(1, Environment.ProcessorCount);
}
if (parsed < MinDegree)
if (parsed <= 0)
{
return DefaultMaxDegree;
return Math.Max(1, Environment.ProcessorCount);
}
return Math.Min(parsed, MaxDegreeCap);

View file

@ -5,6 +5,7 @@
using System.IO.Abstractions;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using NLog;
using NzbDrone.Common;
@ -48,6 +49,7 @@ public class DiskScanService :
private readonly IRootFolderService _rootFolderService;
private readonly IEventAggregator _eventAggregator;
private readonly Logger _logger;
private static int _mediaParallelismLogged;
public DiskScanService(IConfigService configService,
IDiskProvider diskProvider,
@ -126,6 +128,17 @@ public void Scan(List<string> folders = null, FilterFilesType filter = FilterFil
var musicFilesStopwatch = Stopwatch.StartNew();
if (Interlocked.CompareExchange(ref _mediaParallelismLogged, 1, 0) == 0)
{
var envRaw = Environment.GetEnvironmentVariable(MediaImportParallelism.EnvironmentVariableName);
_logger.Info(
"Media import parallelism: MaxDegreeOfParallelism={0} ({1}={2}). Set 164 to cap IO; unset or 0 uses processor count ({3}).",
MediaImportParallelism.MaxDegreeOfParallelism,
MediaImportParallelism.EnvironmentVariableName,
string.IsNullOrEmpty(envRaw) ? "(unset)" : envRaw,
Environment.ProcessorCount);
}
Parallel.ForEach(foldersToScan, new ParallelOptions { MaxDegreeOfParallelism = MediaImportParallelism.MaxDegreeOfParallelism }, folder =>
{
_logger.ProgressInfo("Scanning {0}", folder);