feat(jobs/ui): reorder job names + fix(blobs_consistency)
This commit is contained in:
@@ -65,9 +65,32 @@
|
|||||||
|
|
||||||
// ─── Loading + polling ─────────────────────────────────────────────
|
// ─── Loading + polling ─────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stable render order for the jobs table. The backend snapshot
|
||||||
|
* iterates a HashMap so its order is non-deterministic —
|
||||||
|
* refreshing shuffles rows and hurts orientation.
|
||||||
|
*
|
||||||
|
* Two-tier sort: consistency tenants (including the
|
||||||
|
* `consistency_batch` coordinator) group first, other tenants
|
||||||
|
* follow. Alphabetical within each group. Keeps the consistency
|
||||||
|
* story visually together so an operator investigating
|
||||||
|
* corruption doesn't have to scan the full list to find the
|
||||||
|
* related tenants.
|
||||||
|
*/
|
||||||
|
function sortKey(name: string): [number, string] {
|
||||||
|
const isConsistency = name.endsWith('_consistency') || name === 'consistency_batch';
|
||||||
|
return [isConsistency ? 0 : 1, name];
|
||||||
|
}
|
||||||
|
|
||||||
async function loadJobs() {
|
async function loadJobs() {
|
||||||
try {
|
try {
|
||||||
jobs = await listJobs();
|
const fetched = await listJobs();
|
||||||
|
jobs = fetched.slice().sort((a, b) => {
|
||||||
|
const [ga, na] = sortKey(a.name);
|
||||||
|
const [gb, nb] = sortKey(b.name);
|
||||||
|
if (ga !== gb) return ga - gb;
|
||||||
|
return na.localeCompare(nb);
|
||||||
|
});
|
||||||
loadError = null;
|
loadError = null;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
loadError = errorMessage(e);
|
loadError = errorMessage(e);
|
||||||
|
|||||||
@@ -213,6 +213,24 @@ impl RecoverableJobHandler for BlobsConsistencyCheck {
|
|||||||
// GIN index on `chunk_hashes` (migration
|
// GIN index on `chunk_hashes` (migration
|
||||||
// 20260628000000_delta_upload_gin_index) makes the
|
// 20260628000000_delta_upload_gin_index) makes the
|
||||||
// `= ANY(chunk_hashes)` probe cheap.
|
// `= ANY(chunk_hashes)` probe cheap.
|
||||||
|
// `storage.blobs.ref_count` semantics — what the invariant
|
||||||
|
// dedup_service maintains actually is:
|
||||||
|
//
|
||||||
|
// ref_count = (number of chunk_manifests whose
|
||||||
|
// chunk_hashes[] contains this hash)
|
||||||
|
// + (number of files.blob_hash pointing at
|
||||||
|
// this hash on the LEGACY whole-file path
|
||||||
|
// — i.e. files with NO manifest for their
|
||||||
|
// blob_hash)
|
||||||
|
//
|
||||||
|
// Naively `COUNT(files) + COUNT(manifests referring)`
|
||||||
|
// double-counts single-chunk CDC files: for a file whose
|
||||||
|
// whole-file hash == its single chunk's hash (any file
|
||||||
|
// small enough to fit in one CDC chunk — under ~256 KB
|
||||||
|
// average), the file appears BOTH in `files.blob_hash`
|
||||||
|
// AND in the manifest's `chunk_hashes[]`. The `NOT
|
||||||
|
// EXISTS` clause below excludes CDC-path files from the
|
||||||
|
// legacy count so the two terms don't overlap.
|
||||||
let rows: Vec<BlobRow> = match sqlx::query_as(
|
let rows: Vec<BlobRow> = match sqlx::query_as(
|
||||||
r#"
|
r#"
|
||||||
SELECT
|
SELECT
|
||||||
@@ -222,7 +240,11 @@ impl RecoverableJobHandler for BlobsConsistencyCheck {
|
|||||||
b.created_at AS created_at,
|
b.created_at AS created_at,
|
||||||
(
|
(
|
||||||
(SELECT COUNT(*) FROM storage.files f
|
(SELECT COUNT(*) FROM storage.files f
|
||||||
WHERE f.blob_hash = b.hash)
|
WHERE f.blob_hash = b.hash
|
||||||
|
AND NOT EXISTS (
|
||||||
|
SELECT 1 FROM storage.chunk_manifests m
|
||||||
|
WHERE m.file_hash = f.blob_hash
|
||||||
|
))
|
||||||
+ (SELECT COUNT(*) FROM storage.chunk_manifests m
|
+ (SELECT COUNT(*) FROM storage.chunk_manifests m
|
||||||
WHERE b.hash = ANY(m.chunk_hashes))
|
WHERE b.hash = ANY(m.chunk_hashes))
|
||||||
)::bigint AS actual_ref_count
|
)::bigint AS actual_ref_count
|
||||||
|
|||||||
Reference in New Issue
Block a user