Taking an app's icon out of its build fixed every app arriving from then on and nothing already in the catalog, unless somebody found the button for it. That is the wrong way round: the placeholders people are looking at are on the builds that are already there, and a fix nobody presses is a fix that has not happened. So the server now does it on its own. A pass runs every half minute while there are builds it has not looked inside and once an hour when there are none, which means a deployment works through a catalog full of grey squares in minutes and then costs one counted query an hour. The button stays, for wanting it now rather than within the hour, and both go through the same pass. Every build examined is recorded as examined, whether or not artwork came out of it. Without that the pass keeps re-opening the same archives that have nothing in them and never reaches the ones behind them, which is a backfill that runs forever and finishes nothing. A build recorded with no icon has been looked inside and had none; re-examining those is a switch on the request, for the day the extractor learns something new.
153 lines
5.0 KiB
TypeScript
153 lines
5.0 KiB
TypeScript
import { generateAllJson } from "../src/lib/json/generator";
|
|
import { cleanupStoredIpas } from "../src/lib/storage/retention";
|
|
import { cleanupOldLogs } from "../src/lib/cleanup/logs";
|
|
import { cleanupCaches } from "../src/lib/cleanup/cache";
|
|
import { getSettings } from "../src/lib/config";
|
|
import {
|
|
backfillMissingIcons,
|
|
describeIconBackfill,
|
|
} from "../src/lib/pipeline/icon-backfill";
|
|
import { scanConfiguredGithubReleaseSources } from "../src/lib/github/source-scanner";
|
|
import { logger } from "../src/lib/logger";
|
|
import { syncStorageMetrics } from "../src/lib/metrics/sync-storage";
|
|
import { TelegramService } from "../src/lib/telegram/service";
|
|
|
|
/**
|
|
* How long to stop polling the queue after the storage cap blocks a download.
|
|
* Without this the loop would keep `didWork` false, sleep only a second, and
|
|
* hammer the database with cap checks forever.
|
|
*/
|
|
const STORAGE_BLOCKED_BACKOFF_MS = 60_000;
|
|
|
|
/**
|
|
* How the icon backfill paces itself.
|
|
*
|
|
* Fast while there is a backlog, so a catalog full of placeholders is caught up
|
|
* in minutes rather than over days of cleanup intervals; hourly once every
|
|
* build has been looked inside, where the whole cost is one indexed count. The
|
|
* pass stamps every build it examines, so "caught up" is a state it can reach
|
|
* rather than the same archives being re-opened forever.
|
|
*/
|
|
const ICON_BACKFILL_BUSY_MS = 30_000;
|
|
const ICON_BACKFILL_IDLE_MS = 60 * 60_000;
|
|
|
|
let running = true;
|
|
|
|
function sleep(ms: number): Promise<void> {
|
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
}
|
|
|
|
async function main() {
|
|
const telegram = new TelegramService();
|
|
let nextScanAt = 0;
|
|
let nextJsonAt = 0;
|
|
let nextCleanupAt = 0;
|
|
let nextQueueAt = 0;
|
|
let nextIconAt = 0;
|
|
|
|
await logger.info("system", "FTStore Admin worker starting...");
|
|
await telegram.start();
|
|
|
|
while (running) {
|
|
try {
|
|
await telegram.heartbeat();
|
|
|
|
const settings = await getSettings();
|
|
const now = Date.now();
|
|
let didWork = false;
|
|
|
|
if (await telegram.processNextCommand()) {
|
|
didWork = true;
|
|
}
|
|
|
|
if (settings.system_enabled) {
|
|
if (now >= nextScanAt) {
|
|
await scanConfiguredGithubReleaseSources(settings.github_release_scan_limit);
|
|
try {
|
|
await telegram.scanConfiguredChannels(settings.scan_message_limit);
|
|
} catch (error) {
|
|
await logger.warn("system", "Scheduled scan skipped because Telegram is unavailable", {
|
|
error: error instanceof Error ? error.message : String(error),
|
|
});
|
|
}
|
|
nextScanAt = Date.now() + settings.scan_interval_minutes * 60 * 1000;
|
|
didWork = true;
|
|
}
|
|
|
|
if (now >= nextJsonAt) {
|
|
await syncStorageMetrics();
|
|
await generateAllJson();
|
|
nextJsonAt = Date.now() + settings.json_regen_interval_minutes * 60 * 1000;
|
|
didWork = true;
|
|
}
|
|
|
|
if (now >= nextCleanupAt) {
|
|
await cleanupStoredIpas();
|
|
await cleanupOldLogs();
|
|
await cleanupCaches();
|
|
nextCleanupAt = Date.now() + settings.cleanup_interval_hours * 60 * 60 * 1000;
|
|
didWork = true;
|
|
}
|
|
|
|
// Artwork for the builds the App Store could not answer for, taken out
|
|
// of the IPA already on disk. Deliberately not tied to the cleanup
|
|
// interval: this is what a person is looking at, and hours is the wrong
|
|
// answer to a store full of grey squares.
|
|
if (now >= nextIconAt) {
|
|
const icons = await backfillMissingIcons();
|
|
nextIconAt =
|
|
Date.now() +
|
|
(icons.scanned > 0 ? ICON_BACKFILL_BUSY_MS : ICON_BACKFILL_IDLE_MS);
|
|
|
|
if (icons.scanned > 0) {
|
|
didWork = true;
|
|
await logger.info("process", describeIconBackfill(icons));
|
|
}
|
|
}
|
|
|
|
if (now >= nextQueueAt) {
|
|
const tick = await telegram.processNextQueuedIpa();
|
|
if (tick === "processed") {
|
|
didWork = true;
|
|
} else if (tick === "storage-blocked") {
|
|
// Entries stay pending; back off so the cap check doesn't run at
|
|
// 1 Hz until an operator raises the limit or cleanup frees space.
|
|
nextQueueAt = Date.now() + STORAGE_BLOCKED_BACKOFF_MS;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!didWork) {
|
|
await sleep(1000);
|
|
}
|
|
} catch (error) {
|
|
await logger.error("system", "Worker loop error", {
|
|
error: error instanceof Error ? error.message : String(error),
|
|
});
|
|
await sleep(5000);
|
|
}
|
|
}
|
|
|
|
await telegram.stop();
|
|
}
|
|
|
|
async function shutdown(signal: string) {
|
|
await logger.info("system", `Received ${signal}, shutting down gracefully...`);
|
|
running = false;
|
|
}
|
|
|
|
process.on("SIGTERM", () => {
|
|
void shutdown("SIGTERM");
|
|
});
|
|
process.on("SIGINT", () => {
|
|
void shutdown("SIGINT");
|
|
});
|
|
|
|
main().catch(async (error) => {
|
|
console.error("Worker fatal error:", error);
|
|
await logger.error("system", "Worker crashed", {
|
|
error: error instanceof Error ? error.message : String(error),
|
|
});
|
|
process.exit(1);
|
|
});
|