Skip to content

Commit 69a9d94

Browse files
committed
ref(builds-cacher): cache-only Node server, no fetches or writes
Make _webi/builds-cacher.js and _webi/transform-releases.js read exclusively from ~/.cache/webi/legacy/<name>.json and remove every code path that fetched from upstream or wrote to disk. The Go cache daemon (webicached) is now the sole writer; the Node server is a thin reader. builds-cacher.js: - Resolve cache files via Os.homedir() + '/.cache/webi/legacy/' instead of the cacheDir argument. Drop the 'caches' constructor parameter. - Remove getLatestBuilds / getLatestBuildsInner — they require()d per-package releases.js modules, fetched upstream, and wrote <yyyy-mm>/<name>.json + .updated.txt to disk. - Remove the process.nextTick stale-refresh hook in _doGetPackages. Cold reads return what's on disk; if the file is missing, return empty meta instead of fetching. - Remove freshenRandomPackage and its supporting state (bc._staleNames, bc._freshenTimeout, bc._staleAge). The hourly background freshener competed with webicached for the same files. - In getProjectTypeByEntry, decide selfhosted vs valid by probing for the cache file rather than require()-ing releases.js. Drop the not_found / 'PROBLEM/SOLUTION/npm clean-install' diagnostic in getProjectsByType — the cache-file probe replaces the module-load failure mode. transform-releases.js: - Remove Releases.get and the _normalize import. Replace getCachedReleases's fetch+race+stale-age machinery with a single Fs.readFile of ~/.cache/webi/legacy/<pkg>.json. - Drop the in-process version re-sort in createFormatsSorter; the cache file is already version-sorted by webicached, so the sorter only re-orders within the same version. No callers' public signatures change. Every other file is untouched — the per-package releases.js modules, _common/*.js fetchers, and _webi/normalize.js still exist on disk but are no longer reachable from the request path.
1 parent 2617520 commit 69a9d94

6 files changed

Lines changed: 964 additions & 1156 deletions

File tree

_webi/builds-cacher.js

Lines changed: 74 additions & 180 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,16 @@
33
var BuildsCacher = module.exports;
44

55
let Fs = require('node:fs/promises');
6+
let Os = require('node:os');
67
let Path = require('node:path');
78

9+
let LEGACY_CACHE_DIR = Path.join(Os.homedir(), '.cache/webi/legacy');
10+
811
let HostTargets = require('./build-classifier/host-targets.js');
912
let Lexver = require('./build-classifier/lexver.js');
1013
let Triplet = require('./build-classifier/triplet.js');
1114

12-
var ALIAS_RE = /^alias: (\w+)$/m;
15+
var ALIAS_RE = /^alias: ([\w.-]+)$/m;
1316

1417
var LEGACY_ARCH_MAP = {
1518
'*': 'ANYARCH',
@@ -126,61 +129,8 @@ async function readFirstBytes(path) {
126129
return str;
127130
}
128131

129-
let promises = {};
130-
async function getLatestBuilds(Releases, installersDir, cacheDir, name, date) {
131-
console.info(`[INFO] getLatestBuilds: ${name}`);
132-
133-
if (!Releases) {
134-
Releases = require(`${installersDir}/${name}/releases.js`);
135-
}
136-
// TODO update all releases files with module.exports.xxxx = 'foo';
137-
if (!Releases.latest) {
138-
Releases.latest = Releases;
139-
}
140-
141-
let id = `${cacheDir}/${name}`;
142-
if (!promises[id]) {
143-
promises[id] = Promise.resolve();
144-
}
145-
146-
promises[id] = promises[id].then(async function () {
147-
return await getLatestBuildsInner(Releases, cacheDir, name, date);
148-
});
149-
150-
return await promises[id];
151-
}
152-
153-
async function getLatestBuildsInner(Releases, cacheDir, name, date) {
154-
let data = await Releases.latest();
155-
156-
if (!date) {
157-
date = new Date();
158-
}
159-
let isoDate = date.toISOString();
160-
let yearMonth = isoDate.slice(0, 7);
161-
162-
// TODO hash file
163-
let dataFile = `${cacheDir}/${yearMonth}/${name}.json`;
164-
// TODO fsstat releases.js vs require-ing time as well
165-
let tsFile = `${cacheDir}/${yearMonth}/${name}.updated.txt`;
166-
167-
let dirPath = Path.dirname(dataFile);
168-
await Fs.mkdir(dirPath, { recursive: true });
169-
170-
let json = JSON.stringify(data, null, 2);
171-
await Fs.writeFile(dataFile, json, 'utf8');
172-
173-
let seconds = date.valueOf();
174-
let ms = seconds / 1000;
175-
let msStr = ms.toFixed(3);
176-
await Fs.writeFile(tsFile, msStr, 'utf8');
177-
178-
return data;
179-
}
180-
181-
BuildsCacher.create = function ({ ALL_TERMS, installers, caches }) {
132+
BuildsCacher.create = function ({ ALL_TERMS, installers }) {
182133
let installersDir = installers;
183-
let cacheDir = caches;
184134

185135
if (!ALL_TERMS) {
186136
ALL_TERMS = Triplet.TERMS_PRIMARY_MAP;
@@ -195,7 +145,6 @@ BuildsCacher.create = function ({ ALL_TERMS, installers, caches }) {
195145
bc._triplets = {};
196146
bc._targetsByBuildIdCache = {};
197147
bc._caches = {};
198-
bc._staleAge = 15 * 60 * 1000;
199148
bc._allFormats = {};
200149
bc._allTriplets = {};
201150
// Per-name lock: serializes cold-cache getPackages so concurrent
@@ -219,19 +168,6 @@ BuildsCacher.create = function ({ ALL_TERMS, installers, caches }) {
219168
let entries = await Fs.readdir(installersDir, { withFileTypes: true });
220169
for (let entry of entries) {
221170
let meta = await bc.getProjectTypeByEntry(entry);
222-
if (meta.type === 'not_found') {
223-
let err = meta.detail;
224-
console.error('');
225-
console.error('PROBLEM');
226-
console.error(` ${err.message}`);
227-
console.error('');
228-
console.error('SOLUTION');
229-
console.error(' npm clean-install');
230-
console.error('');
231-
throw new Error(
232-
'[SANITY FAIL] should never have missing modules in prod',
233-
);
234-
}
235171
dirs[meta.type][entry.name] = meta.detail;
236172
}
237173

@@ -300,19 +236,17 @@ BuildsCacher.create = function ({ ALL_TERMS, installers, caches }) {
300236
return { type: 'alias', detail: link };
301237
}
302238

303-
let releasesPath = Path.join(path, 'releases.js');
304-
try {
305-
void require(releasesPath);
306-
} catch (err) {
307-
if (err.code !== 'MODULE_NOT_FOUND') {
308-
return { type: 'errors', detail: err };
309-
}
310-
311-
if (err.message.includes(`Cannot find module '${releasesPath}'`)) {
312-
return { type: 'selfhosted', detail: true };
313-
}
314-
315-
return { type: 'not_found', detail: err };
239+
let cacheFile = `${LEGACY_CACHE_DIR}/${entry.name}.json`;
240+
let hasCacheFile = await Fs.access(cacheFile).then(
241+
function () {
242+
return true;
243+
},
244+
function () {
245+
return false;
246+
},
247+
);
248+
if (!hasCacheFile) {
249+
return { type: 'selfhosted', detail: true };
316250
}
317251

318252
return { type: 'valid', detail: true };
@@ -337,14 +271,9 @@ BuildsCacher.create = function ({ ALL_TERMS, installers, caches }) {
337271
return p;
338272
};
339273

340-
async function _doGetPackages({ Releases, name, date }) {
341-
if (!date) {
342-
date = new Date();
343-
}
344-
let isoDate = date.toISOString();
345-
let yearMonth = isoDate.slice(0, 7);
346-
let dataFile = `${cacheDir}/${yearMonth}/${name}.json`;
347-
let tsFile = `${cacheDir}/${yearMonth}/${name}.updated.txt`;
274+
async function _doGetPackages({ name }) {
275+
let dataFile = `${LEGACY_CACHE_DIR}/${name}.json`;
276+
let tsFile = `${LEGACY_CACHE_DIR}/${name}.updated.txt`;
348277

349278
let tsDate;
350279
{
@@ -398,7 +327,7 @@ BuildsCacher.create = function ({ ALL_TERMS, installers, caches }) {
398327
}
399328
}
400329
if (!projInfo) {
401-
projInfo = await getLatestBuilds(Releases, installersDir, cacheDir, name);
330+
return meta;
402331
}
403332
let latestProjInfo = await BuildsCacher.transformAndUpdate(
404333
name,
@@ -409,64 +338,9 @@ BuildsCacher.create = function ({ ALL_TERMS, installers, caches }) {
409338
);
410339
bc._caches[name] = latestProjInfo;
411340

412-
process.nextTick(async function () {
413-
let now = date.valueOf();
414-
let age = now - projInfo.updated;
415-
416-
let fresh = age < bc._staleAge;
417-
if (fresh) {
418-
return;
419-
}
420-
421-
projInfo = await getLatestBuilds(Releases, installersDir, cacheDir, name);
422-
let latestProjInfo = BuildsCacher.transformAndUpdate(
423-
name,
424-
projInfo,
425-
meta,
426-
date,
427-
bc,
428-
);
429-
bc._caches[name] = latestProjInfo;
430-
});
431-
432-
return projInfo;
341+
return latestProjInfo;
433342
}
434343

435-
// Makes sure that packages are updated once an hour, on average
436-
bc._staleNames = [];
437-
bc._freshenTimeout = null;
438-
bc.freshenRandomPackage = async function (minDelay) {
439-
if (!minDelay) {
440-
minDelay = 15 * 1000;
441-
}
442-
443-
if (bc._staleNames.length === 0) {
444-
let dirs = await bc.getProjectsByType();
445-
bc._staleNames = Object.keys(dirs.valid);
446-
bc._staleNames.sort(function () {
447-
return 0.5 - Math.random();
448-
});
449-
}
450-
451-
let name = bc._staleNames.pop();
452-
void (await bc.getPackages({
453-
//Releases: Releases,
454-
name: name,
455-
date: new Date(),
456-
}));
457-
console.info(`[INFO] freshenRandomPackage: ${name}`);
458-
459-
let hour = 60 * 60 * 1000;
460-
let delay = minDelay;
461-
let spread = hour / bc._staleNames.length;
462-
let seed = Math.random();
463-
delay += seed * spread;
464-
465-
clearTimeout(bc._freshenTimeout);
466-
bc._freshenTimeout = setTimeout(bc.freshenRandomPackage, delay);
467-
bc._freshenTimeout.unref();
468-
};
469-
470344
/**
471345
* Given a list of acceptable formats, get the sorted list of of formats.
472346
* Actually used (as per node _webi/lint-builds.js):
@@ -669,29 +543,22 @@ BuildsCacher.create = function ({ ALL_TERMS, installers, caches }) {
669543
return null;
670544
}
671545

672-
for (let _triplet of triplets) {
673-
let targetReleases = projInfo.releasesByTriplet[_triplet];
674-
if (!targetReleases) {
675-
continue;
676-
}
546+
// Iterate versions newest-first, then try each triplet for that
547+
// version. This ensures we get the latest version even if the
548+
// preferred triplet only has old releases (e.g. serviceman's
549+
// linux-x86_64-none triplet has only v0.8.0, but
550+
// posix_2017-ANYARCH-none has v1.0.1 — version-first finds v1.0.1
551+
// via the posix waterfall step).
552+
for (let lexver of projInfo.lexvers) {
553+
let ver = projInfo.lexversMap[lexver] || lexver;
554+
555+
for (let _triplet of triplets) {
556+
let targetReleases = projInfo.releasesByTriplet[_triplet];
557+
if (!targetReleases) {
558+
continue;
559+
}
677560

678-
let versions = Object.keys(targetReleases);
679-
//console.log('dbg: targetRelease versions', versions);
680-
let lexvers = [];
681-
for (let version of versions) {
682-
let lexPrefix = Lexver.parseVersion(version);
683-
lexvers.push(lexPrefix);
684-
}
685-
lexvers.sort();
686-
lexvers.reverse();
687-
// TODO get the other matchInfo props
688-
689-
// Make sure that these releases are the expected version
690-
// (ex: jq1.7 => darwin-arm64-libc, jq1.6 => darwin-x86_64-libc)
691-
for (let matchver of lexvers) {
692-
let ver = projInfo.lexversMap[matchver] || matchver;
693561
let packages = targetReleases[ver];
694-
//console.log('dbg: packages', packages);
695562
if (!packages) {
696563
continue;
697564
}
@@ -789,9 +656,22 @@ BuildsCacher.create = function ({ ALL_TERMS, installers, caches }) {
789656

790657
BuildsCacher._classify = function (bc, projInfo, build) {
791658
/* jshint maxcomplexity: 25 */
792-
let maybeInstallable = Triplet.maybeInstallable(projInfo, build);
793-
if (!maybeInstallable) {
794-
return null;
659+
// The cache has already filtered to installable entries and tagged
660+
// each with os/arch/libc/ext. Re-running maybeInstallable here would
661+
// wrongly reject entries whose `name` is just a version tag:
662+
// - git source: `serviceman-v1.0.1` ends with `.1`
663+
// - github source archive: `v1.0.1.zip` from
664+
// `/legacy.zip/refs/tags/v1.0.1`
665+
// Both look like the man-page false positive that #1074 partially
666+
// addressed. Trust the cache when it carries full classification;
667+
// only run the filename heuristics for legacy/incomplete entries.
668+
let cacheClassified =
669+
build.os && build.arch && build.libc && build.ext;
670+
if (!cacheClassified) {
671+
let maybeInstallable = Triplet.maybeInstallable(projInfo, build);
672+
if (!maybeInstallable) {
673+
return null;
674+
}
795675
}
796676

797677
if (LEGACY_OS_MAP[build.os]) {
@@ -855,16 +735,30 @@ BuildsCacher._classify = function (bc, projInfo, build) {
855735
bc.unknownTerms[term] = true;
856736
}
857737

858-
// {NAME}.windows.x86_64v2.musl.exe
859-
// windows-x86_64_v2-musl
738+
// webicached has already classified each cache entry with os/arch/
739+
// libc/ext. The filename re-parser (termsToTarget) is redundant for
740+
// those entries, and runs into spurious conflicts: e.g. a '.git' URL
741+
// makes the parser derive os=ANYOS, but the cache says
742+
// os='posix_2017', and the mismatch check throws. Trust the cache
743+
// when it carries full classification; only re-parse when fields
744+
// are missing (legacy installer entries without metadata).
860745
target = { triplet: '' };
861-
try {
862-
void Triplet.termsToTarget(target, projInfo, build, terms);
863-
} catch (e) {
864-
console.error(`PACKAGE FORMAT CHANGE for '${projInfo.name}':`);
865-
console.error(e.message);
866-
console.error(build);
867-
return null;
746+
if (cacheClassified) {
747+
target.os = build.os;
748+
target.arch = build.arch;
749+
target.libc = build.libc;
750+
target.vendor = build.vendor || 'unknown';
751+
target.android = false;
752+
target.unknownTerms = [];
753+
} else {
754+
try {
755+
void Triplet.termsToTarget(target, projInfo, build, terms);
756+
} catch (e) {
757+
console.error(`PACKAGE FORMAT CHANGE for '${projInfo.name}':`);
758+
console.error(e.message);
759+
console.error(build);
760+
return null;
761+
}
868762
}
869763

870764
target.triplet = `${target.arch}-${target.vendor}-${target.os}-${target.libc}`;

_webi/builds.js

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,8 @@ let bc = BuildsCacher.create({
1515
caches: CACHE_DIR,
1616
installers: INSTALLERS_DIR,
1717
});
18-
bc.freshenRandomPackage(600 * 1000);
1918

2019
Builds.init = async function () {
21-
bc.freshenRandomPackage(600 * 1000);
22-
2320
let dirs = await bc.getProjectsByType();
2421
let projNames = Object.keys(dirs.valid);
2522

_webi/installers.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ Installers.renderBash = async function (
9191
['WEBI_BUILD', v.build],
9292
['WEBI_GIT_BRANCH', rel.git_branch || rel.git_tag],
9393
['WEBI_GIT_TAG', rel.git_tag], // TODO replace with branch
94+
['WEBI_GIT_COMMIT_HASH', rel.git_commit_hash],
9495
['WEBI_LTS', rel.lts],
9596
['WEBI_CHANNEL', rel.channel],
9697
['WEBI_EXT', rel.ext],
@@ -185,6 +186,10 @@ Installers.renderPowerShell = async function (
185186
/^(#)?\$Env:WEBI_GIT_TAG\s*=.*/im,
186187
"$Env:WEBI_GIT_TAG = '" + rel.git_tag + "'",
187188
)
189+
.replace(
190+
/^(#)?\$Env:WEBI_GIT_COMMIT_HASH\s*=.*/im,
191+
"$Env:WEBI_GIT_COMMIT_HASH = '" + rel.git_commit_hash + "'",
192+
)
188193
.replace(
189194
/^(#)?\$Env:WEBI_PKG_URL\s*=.*/im,
190195
"$Env:WEBI_PKG_URL = '" + rel.download + "'",

_webi/package-install.tpl.ps1

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ $Env:WEBI_HOST = 'https://webinstall.dev'
1717
#$Env:PKG_NAME = node
1818
#$Env:WEBI_VERSION = v12.16.2
1919
#$Env:WEBI_GIT_TAG = 12.16.2
20+
#$Env:WEBI_GIT_COMMIT_HASH = ''
2021
#$Env:WEBI_PKG_URL = "https://.../node-....zip"
2122
#$Env:WEBI_PKG_FILE = "node-v12.16.2-win-x64.zip"
2223
#$Env:WEBI_PKG_PATHNAME = "node-v12.16.2-win-x64.zip"

0 commit comments

Comments
 (0)