fix(subsUnion): stop treating a failed worker call as a valid empty union
All checks were successful
build-image / build (push) Successful in 12m16s

storeUnion checked `typeof union === 'object'` as its "did this work"
guard, but typeof null is also 'object' — so a worker call that failed
(caught upstream, leaving union as its initial null) looked identical
to a legitimate "zero valid subscriptions" result, and got silently
stored as the string "null" instead of being treated as a failure.
This is exactly what corrupted subs-public-union/subs-private-union on
staging just now, while chasing the missing-turf-deps bug.

storeUnion is now only called once calcUnionAsync has actually
resolved; process() and incrementalAdd() catch failures themselves and
skip storing anything, leaving the previous (still valid) union alone.
This commit is contained in:
vjrj 2026-07-30 08:29:21 +02:00
parent 4879d3482d
commit 50ca9cc6cf

View file

@ -41,48 +41,50 @@ Meteor.startup(async () => {
return valid; return valid;
}; };
// Only called once calcUnionAsync has actually resolved — union === null here
// means "zero valid subscriptions", a real result, not a failed computation.
// (A prior version guarded this with `typeof union === 'object'`, which is
// true for null too, so a failed worker call — caught upstream, leaving
// union as null — looked identical to a legitimate empty union and got
// silently stored as "null". Callers must catch calcUnionAsync failures
// themselves and skip calling this, instead of relying on this check.)
const storeUnion = async (isPublic, union, count) => { const storeUnion = async (isPublic, union, count) => {
const publicl = isPublic ? 'public' : 'private'; const publicl = isPublic ? 'public' : 'private';
const Publicl = publicl.replace(/\b\w/g, l => l.toUpperCase()); const Publicl = publicl.replace(/\b\w/g, l => l.toUpperCase());
const bounds = union === null ? null : L.geoJSON(union).getBounds();
if (typeof union === 'object') { const unionSet = {
const bounds = union === null ? null : L.geoJSON(union).getBounds(); $set: {
const unionSet = { name: `subs-${publicl}-union`,
$set: { value: JSON.stringify(union),
name: `subs-${publicl}-union`, isPublic,
value: JSON.stringify(union), description: `${Publicl} subscriptions union`,
isPublic, type: 'string'
description: `${Publicl} subscriptions union`, }
type: 'string' };
} const boundsSet = {
}; $set: {
const boundsSet = { name: `subs-${publicl}-union-bounds`,
$set: { value: JSON.stringify(bounds),
name: `subs-${publicl}-union-bounds`, isPublic,
value: JSON.stringify(bounds), description: `${Publicl} subscriptions union bounds`,
isPublic, type: 'string'
description: `${Publicl} subscriptions union bounds`, }
type: 'string' };
} const sizeSet = {
}; $set: {
const sizeSet = { name: 'subs-union-count',
$set: { value: count,
name: 'subs-union-count', isPublic: false,
value: count, description: 'Subscriptions count',
isPublic: false, type: 'number'
description: 'Subscriptions count', }
type: 'number' };
} // FIXME, take care of object size:
}; // https://stackoverflow.com/questions/10827812/what-is-the-length-maximum-for-a-string-data-type-in-mongodb-used-with-ruby
// FIXME, take care of object size: await SiteSettings.upsertAsync({ name: `subs-${publicl}-union` }, unionSet, { multi: false });
// https://stackoverflow.com/questions/10827812/what-is-the-length-maximum-for-a-string-data-type-in-mongodb-used-with-ruby await SiteSettings.upsertAsync({ name: `subs-${publicl}-union-bounds` }, boundsSet, { multi: false });
await SiteSettings.upsertAsync({ name: `subs-${publicl}-union` }, unionSet, { multi: false }); await SiteSettings.upsertAsync({ name: 'subs-union-count' }, sizeSet, { multi: false });
await SiteSettings.upsertAsync({ name: `subs-${publicl}-union-bounds` }, boundsSet, { multi: false }); if (debug) console.log(`${Publicl} subscription union calculated`);
await SiteSettings.upsertAsync({ name: 'subs-union-count' }, sizeSet, { multi: false });
if (debug) console.log(`${Publicl} subscription union calculated`);
} else {
console.log('Subscription union failed!');
}
}; };
const process = async (isPublic) => { const process = async (isPublic) => {
@ -90,13 +92,12 @@ Meteor.startup(async () => {
const decorate = isPublic ? addNoisy : noNoisy; const decorate = isPublic ? addNoisy : noNoisy;
const decorated = subscribers.filter(isValidSub).map(decorate); const decorated = subscribers.filter(isValidSub).map(decorate);
let union = null;
try { try {
union = await calcUnionAsync(decorated); const union = await calcUnionAsync(decorated);
await storeUnion(isPublic, union, subscribers.length);
} catch (e) { } catch (e) {
console.error('subsUnion worker failed', e); console.error('subsUnion worker failed, union left unchanged', e);
} }
await storeUnion(isPublic, union, subscribers.length);
}; };
const recreate = async () => { await process(true); await process(false); }; const recreate = async () => { await process(true); await process(false); };
@ -138,17 +139,16 @@ Meteor.startup(async () => {
const current = await SiteSettings.findOneAsync({ name: `subs-${publicl}-union` }); const current = await SiteSettings.findOneAsync({ name: `subs-${publicl}-union` });
const baseUnion = current && current.value ? JSON.parse(current.value) : null; const baseUnion = current && current.value ? JSON.parse(current.value) : null;
let union = null;
try { try {
// eslint-disable-next-line no-await-in-loop // eslint-disable-next-line no-await-in-loop
union = await calcUnionAsync([decorated], baseUnion); const union = await calcUnionAsync([decorated], baseUnion);
// eslint-disable-next-line no-await-in-loop
await storeUnion(isPublic, union, countSubs);
} catch (e) { } catch (e) {
console.error('subsUnion incremental worker failed, falling back to a full recreate', e); console.error('subsUnion incremental worker failed, falling back to a full recreate', e);
recomputePending = true; recomputePending = true;
return; return;
} }
// eslint-disable-next-line no-await-in-loop
await storeUnion(isPublic, union, countSubs);
} }
if (debug) console.log('Subs union updated incrementally'); if (debug) console.log('Subs union updated incrementally');
}; };