fix metasort pipeline

This commit is contained in:
Pierre
2026-04-28 12:21:23 +02:00
parent e3d26549d0
commit f7315cbc3f
8 changed files with 419 additions and 402 deletions
+80 -11
View File
@@ -5,7 +5,8 @@ import {
RedisStats,
QueueItem,
HashTimingStats,
FailedFile
FailedFile,
CacheSlotPhase
} from '../types';
import {
formatMs,
@@ -14,6 +15,43 @@ import {
getFilename
} from '../utils/format';
/**
* Renders a single phase of the file-cache-slot view: capacity bar, count of
* waiting files, and a list of currently-held files showing which plugin
* each is currently waiting on. The plugin column is the long-pole — it tells
* operators which plugin is the per-file bottleneck.
*/
function CacheSlotPanel({ label, phase }: { label: string; phase: CacheSlotPhase }) {
const fillPct = Math.min(100, (phase.inUse / Math.max(1, phase.capacity)) * 100);
return (
<div style={{ marginTop: '0.75rem' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'baseline' }}>
<strong>{label}</strong>
<span style={{ color: '#9ca3af', fontSize: '0.85rem' }}>
{phase.inUse}/{phase.capacity} files
{phase.waiting > 0 ? ` • ${phase.waiting} waiting for slot` : ''}
</span>
</div>
<div style={{ height: '6px', background: '#0f3460', borderRadius: '3px', overflow: 'hidden', margin: '0.25rem 0 0.5rem' }}>
<div style={{ width: `${fillPct}%`, height: '100%', background: '#4ade80', transition: 'width 0.5s linear' }} />
</div>
{phase.holders.length > 0 && (
<div style={{ fontSize: '0.78rem', color: '#cbd5e1', maxHeight: '160px', overflow: 'auto' }}>
{phase.holders.map((h) => (
<div key={h.fileHash} style={{ display: 'flex', gap: '0.5rem', padding: '2px 0' }}>
<span style={{ color: '#facc15', minWidth: '8rem' }}>{h.currentPlugin ?? '—'}</span>
<span style={{ color: '#94a3b8', minWidth: '4rem' }}>×{h.tasksRemaining}</span>
<span style={{ flex: 1, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }} title={h.filePath ?? h.fileHash}>
{h.filePath ? getFilename(h.filePath) : h.fileHash}
</span>
</div>
))}
</div>
)}
</div>
);
}
function Monitor() {
const [status, setStatus] = useState<ProcessingStatus | null>(null);
const [metrics, setMetrics] = useState<Metrics | null>(null);
@@ -107,31 +145,50 @@ function Monitor() {
// Get queue statistics for display
// StreamingPipeline stages: validation → lightProcessing → hashProcessing
// UI mapping: Validation → Fast Queue → Background Queue
//
// Fast/Background "Running/Max" represents file-cache slot occupancy
// (cacheSlots.inUse / capacity), not raw task throughput. This matches the
// operator mental model: "how many files are currently tying up the WebDAV
// cache?" The task-callback count was misleading because dependency-wait
// and per-plugin HTTP queueing made it under-report by ~5×.
const getQueueStats = () => {
const computed = status?.computed;
const cacheSlots = status?.queueStatus?.cacheSlots;
const fastQueueConcurrency = status?.fastQueueConcurrency || 16;
const backgroundQueueConcurrency = status?.backgroundQueueConcurrency || 8;
return {
// Stage 1: Validation (quick extension checks)
// Concurrency: 2x fast queue workers
// Stage 1: Validation (quick extension checks). Still task-level
// — pre-process is a lightweight midhash256 scan, no cache impact.
preProcess: {
running: computed?.preProcessRunning || 0,
pending: computed?.preProcessPending || 0,
max: fastQueueConcurrency * 2,
paused: computed?.preProcessPaused || false
},
// Stage 2: Fast Queue (midhash256 + FFmpeg + plugins)
// Concurrency: fastQueueConcurrency workers
fast: {
// Stage 2: Fast Queue — files holding a light cache slot.
// running = files in cache, pending = files waiting for a slot.
// paused comes from the cacheSlots payload (true ContainerPluginScheduler
// PQueue state) — the legacy `computed.fastQueuePaused` field is sourced
// from a different StreamingPipeline queue and is unreliable here.
fast: cacheSlots ? {
running: cacheSlots.fast.inUse,
pending: cacheSlots.fast.waiting,
max: cacheSlots.fast.capacity,
paused: cacheSlots.fast.paused
} : {
running: computed?.fastQueueRunning || 0,
pending: computed?.fastQueuePending || 0,
max: fastQueueConcurrency,
paused: computed?.fastQueuePaused || false
},
// Stage 3: Background Queue (SHA-256 full file hash)
// Concurrency: backgroundQueueConcurrency workers
background: {
// Stage 3: Background Queue — files holding a bg cache slot.
background: cacheSlots ? {
running: cacheSlots.background.inUse,
pending: cacheSlots.background.waiting,
max: cacheSlots.background.capacity,
paused: cacheSlots.background.paused
} : {
running: computed?.backgroundQueueRunning || 0,
pending: computed?.backgroundQueuePending || 0,
max: backgroundQueueConcurrency,
@@ -165,9 +222,9 @@ function Monitor() {
{showPipelineHelp && (
<div className="pipeline-help">
<div className="help-item"><span className="help-dot discovered"></span><strong>Discovered:</strong> Files found on disk, waiting to be processed.</div>
<div className="help-item"><span className="help-dot fast"></span><strong>Fast Queue:</strong> Files being processed for quick metadata extraction (hash, ffmpeg, etc.).</div>
<div className="help-item"><span className="help-dot fast"></span><strong>Fast Queue:</strong> Files holding a light cache slot. The badge shows slots-in-use over the gate capacity (e.g. 16/16). Pending = files waiting for a slot to free.</div>
<div className="help-item"><span className="help-dot awaiting-background"></span><strong>Awaiting BG:</strong> Files done with fast processing, queued for slower tasks.</div>
<div className="help-item"><span className="help-dot background"></span><strong>Background:</strong> Files running slow tasks like full file hashing or TMDB lookups.</div>
<div className="help-item"><span className="help-dot background"></span><strong>Background:</strong> Files holding a background cache slot (full-hash, TMDB, etc.). Same slot-occupancy semantics as Fast Queue.</div>
<div className="help-item"><span className="help-dot done"></span><strong>Complete:</strong> Files fully processed and available in the virtual filesystem.</div>
<div className="help-item"><span className="help-dot failed"></span><strong>Failed:</strong> Files that encountered errors during processing.</div>
</div>
@@ -301,6 +358,18 @@ function Monitor() {
</div>
)}
{status?.queueStatus?.cacheSlots && (
<div className="card cache-slot-section" style={{ marginTop: '1rem' }}>
<h2>File cache slots</h2>
<p style={{ color: '#888', fontSize: '0.85rem', marginTop: '-0.5rem' }}>
Number of distinct files concurrently held by the WebDAV cache per phase. Caps how
many file fetches CORN/SMB sees at once, regardless of plugin fan-out.
</p>
<CacheSlotPanel label="Light (fast queue)" phase={status.queueStatus.cacheSlots.fast} />
<CacheSlotPanel label="Background" phase={status.queueStatus.cacheSlots.background} />
</div>
)}
<div className="grid grid-2">
{/* Metrics and Stats */}
<div className="card">
+23
View File
@@ -75,6 +75,29 @@ export interface TaskSchedulerQueueStatus {
preProcessQueue?: TaskQueueStatus;
fastQueue?: TaskQueueStatus;
backgroundQueue?: TaskQueueStatus;
cacheSlots?: CacheSlotsStatus;
}
export interface CacheSlotHolder {
fileHash: string;
filePath?: string;
currentPlugin?: string;
tasksRemaining: number;
}
export interface CacheSlotPhase {
capacity: number;
inUse: number;
waiting: number;
/** True when the underlying PQueue is paused — bg phase is paused while
* fast has work, so its slots can be "reserved" without active dispatch. */
paused: boolean;
holders: CacheSlotHolder[];
}
export interface CacheSlotsStatus {
fast: CacheSlotPhase;
background: CacheSlotPhase;
}
export interface ProcessingStatus {