Stop calling an empty parity slot an emulated disk

mdNumDisabled counts a slot that was never populated, so HOST2 reported
"Emulating 1 disk" for a parity2 slot with no disk in it. Emulation is a
data-disk state, and an assigned identity is what separates a slot that lost
its disk from one that never had one.
This commit is contained in:
Gmer4Lfe
2026-08-21 08:07:38 -04:00
parent 9c15df8e22
commit aa16fbaa8b
2 changed files with 58 additions and 4 deletions
+47
View File
@@ -517,6 +517,48 @@ function vv_parity_status(): array {
$numMissing = (int)($var['mdNumMissing'] ?? 0); $numMissing = (int)($var['mdNumMissing'] ?? 0);
$exitCode = (int)($var['sbSyncExit'] ?? 0); $exitCode = (int)($var['sbSyncExit'] ?? 0);
$errors = (int)($var['sbSyncErrs'] ?? 0); $errors = (int)($var['sbSyncErrs'] ?? 0);
// How many disks are actually being emulated, which is not mdNumDisabled.
//
// mdNumDisabled counts every slot the array marks disabled, including a parity slot that was
// never populated. HOST2 reports mdNumDisabled=1 with mdNumMissing=0, and the slot is parity2
// at DISK_NP_DSBL — an empty second-parity slot. Unraid's own Main page shows nothing for it,
// while this card said "Emulating 1 disk" and turned the banner amber.
//
// Parity is never emulated: reconstructing a parity disk from parity is not a thing. Only data
// slots are, so the count is taken from disks.ini and restricted to them. A data disk that is
// disabled *is* emulated whether or not it is physically present — a pulled failed drive reads
// DISK_NP_DSBL and is genuinely being served from parity — so presence is not the test here,
// role is.
//
// A disabled parity slot is still worth knowing about when a real parity disk fails, so it is
// counted separately rather than discarded.
//
// Both counts require the slot to have an assigned identity. A slot that was never populated
// reads id="" device="" size="0" — parity2 on HOST2 is exactly that — while a disk that failed
// and was pulled keeps its id, because the array remembers the assignment it is emulating.
// Presence is therefore the wrong test and identity is the right one: it separates "no disk was
// ever here" from "the disk that belongs here is gone", which look identical in the status
// field alone and mean opposite things.
$numEmulated = 0;
$numParityDisabled = 0;
$slot = ''; $slotId = ''; $slotStatus = '';
$tally = function () use (&$slot, &$slotId, &$slotStatus, &$numEmulated, &$numParityDisabled) {
if ($slot === '' || $slotId === '') return;
if (strpos($slotStatus, 'DSBL') === false) return;
if (preg_match('/^disk\d+$/', $slot)) $numEmulated++;
elseif (preg_match('/^parity\d*$/', $slot)) $numParityDisabled++;
};
foreach (@file('/var/local/emhttp/disks.ini') ?: [] as $line) {
if (preg_match('/^\["([^"]+)"\]/', $line, $m)) {
$tally(); // close the previous slot
$slot = $m[1]; $slotId = ''; $slotStatus = '';
continue;
}
if (preg_match('/^id="([^"]*)"/', $line, $m)) $slotId = trim($m[1]);
if (preg_match('/^status="([^"]*)"/', $line, $m)) $slotStatus = $m[1];
}
$tally(); // and the last one, which has no header after it
// Emulated (DISK_DSBL) disks are protected by parity and don't make parity invalid. // Emulated (DISK_DSBL) disks are protected by parity and don't make parity invalid.
// True invalidity: sync errors on the last check, or unprotectable missing slots. // True invalidity: sync errors on the last check, or unprotectable missing slots.
$isValid = $errors === 0 && $numMissing === 0; $isValid = $errors === 0 && $numMissing === 0;
@@ -566,7 +608,12 @@ function vv_parity_status(): array {
$exitMap = ['0' => 'Completed', '-4' => 'Aborted', '-5' => 'Cancelled']; $exitMap = ['0' => 'Completed', '-4' => 'Aborted', '-5' => 'Cancelled'];
return [ return [
'valid' => $isValid, 'valid' => $isValid,
// mdNumDisabled as the array reports it, kept because it is Unraid's own number and
// anything comparing against the WebGUI wants it. Not what the card labels, though —
// num_emulated is the one that means "data is being served from parity".
'num_disabled' => $numDisabled, 'num_disabled' => $numDisabled,
'num_emulated' => $numEmulated,
'num_parity_disabled' => $numParityDisabled,
'num_missing' => $numMissing, 'num_missing' => $numMissing,
'in_progress' => $inProgress, 'in_progress' => $inProgress,
'resync_action' => $resyncAction, 'resync_action' => $resyncAction,
+11 -4
View File
@@ -1290,21 +1290,28 @@ function vvPollMonitor(live) {
return new Date(ts * 1000).toLocaleString([], {weekday:'short',day:'numeric',month:'short',year:'numeric',hour:'2-digit',minute:'2-digit'}); return new Date(ts * 1000).toLocaleString([], {weekday:'short',day:'numeric',month:'short',year:'numeric',hour:'2-digit',minute:'2-digit'});
} }
const numDisabled = par.num_disabled ?? 0; // num_emulated, not num_disabled: the array counts an empty parity slot as disabled, and
// labelling that "Emulating 1 disk" described a failure that was not happening. See
// vv_parity_status() for why parity slots are counted separately.
const numEmulated = par.num_emulated ?? 0;
const numParityDsbl = par.num_parity_disabled ?? 0;
const numMissing = par.num_missing ?? 0; const numMissing = par.num_missing ?? 0;
const degraded = numDisabled > 0 || numMissing > 0; const degraded = numEmulated > 0 || numMissing > 0;
const exitColor = par.exit_label === 'Completed' ? '#4caf50' : par.exit_label === 'Aborted' ? '#ff9800' : '#f44336'; const exitColor = par.exit_label === 'Completed' ? '#4caf50' : par.exit_label === 'Aborted' ? '#ff9800' : '#f44336';
const errColor = (par.errors ?? 0) > 0 ? '#f44336' : '#444'; const errColor = (par.errors ?? 0) > 0 ? '#f44336' : '#444';
const speedStr = par.last_speed_mb ? ` · ${par.last_speed_mb} MB/s` : ''; const speedStr = par.last_speed_mb ? ` · ${par.last_speed_mb} MB/s` : '';
const nextDate = vvFmtDate(par.next_ts); const nextDate = vvFmtDate(par.next_ts);
const dueIn = vvDueIn(par.next_ts); const dueIn = vvDueIn(par.next_ts);
const parBannerCls = !valid ? 'vv-banner-err' : degraded || (par.errors ?? 0) > 0 ? 'vv-banner-warn' : 'vv-banner-ok'; const parBannerCls = !valid ? 'vv-banner-err' : degraded || (par.errors ?? 0) > 0 ? 'vv-banner-warn' : 'vv-banner-ok';
const emulLabel = numDisabled > 0 ? `<span style="font-size:11px;">Emulating ${numDisabled} disk${numDisabled !== 1 ? 's' : ''}</span>` : ''; const emulLabel = numEmulated > 0 ? `<span style="font-size:11px;">Emulating ${numEmulated} disk${numEmulated !== 1 ? 's' : ''}</span>` : '';
const missingLabel = numMissing > 0 ? `<span style="font-size:11px;color:#f44336;">${numMissing} slot${numMissing !== 1 ? 's' : ''} missing</span>` : ''; const missingLabel = numMissing > 0 ? `<span style="font-size:11px;color:#f44336;">${numMissing} slot${numMissing !== 1 ? 's' : ''} missing</span>` : '';
// A failed parity disk is a real fault and still has to surface — it just is not emulation,
// and it does not mean data is being reconstructed.
const parDsblLabel = numParityDsbl > 0 ? `<span style="font-size:11px;color:#ffb74d;">${numParityDsbl} parity disk${numParityDsbl !== 1 ? 's' : ''} disabled</span>` : '';
let html = `<div class="vv-banner ${parBannerCls}"> let html = `<div class="vv-banner ${parBannerCls}">
<span>${valid ? '✓ Valid' : '✗ INVALID'}</span> <span>${valid ? '✓ Valid' : '✗ INVALID'}</span>
${emulLabel}${missingLabel} ${emulLabel}${missingLabel}${parDsblLabel}
${(par.errors ?? 0) > 0 ? `<span style="font-size:11px;">${par.errors} error${par.errors !== 1 ? 's' : ''}</span>` : ''} ${(par.errors ?? 0) > 0 ? `<span style="font-size:11px;">${par.errors} error${par.errors !== 1 ? 's' : ''}</span>` : ''}
</div>`; </div>`;