Skip to content

Commit

Permalink
Chore/CI: use fs cache to save bandwidth
Browse files Browse the repository at this point in the history
  • Loading branch information
SukkaW committed Dec 23, 2023
1 parent 7fbd4a5 commit 230ac3e
Show file tree
Hide file tree
Showing 21 changed files with 355 additions and 203 deletions.
6 changes: 6 additions & 0 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,12 @@ jobs:
uses: actions/checkout@v4
with:
persist-credentials: false
- name: Cache cache.db
uses: actions/cache@v3
with:
path: .cache
key: ${{ runner.os }}-v1

- uses: oven-sh/setup-bun@v1
with:
bun-version: latest
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
node_modules
.clinic
.wireit
.cache
public

# $ build output
Expand Down
4 changes: 2 additions & 2 deletions Build/build-anti-bogus-domain.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
// @ts-check
import path from 'path';
import { createRuleset } from './lib/create-file';
import { fetchRemoteTextAndReadByLine, readFileByLine } from './lib/fetch-text-by-line';
import { fetchRemoteTextByLine, readFileByLine } from './lib/fetch-text-by-line';
import { processLine } from './lib/process-line';
import { task } from './lib/trace-runner';
import { SHARED_DESCRIPTION } from './lib/constants';
import { isProbablyIpv4, isProbablyIpv6 } from './lib/is-fast-ip';

const getBogusNxDomainIPs = async () => {
const result: string[] = [];
for await (const line of await fetchRemoteTextAndReadByLine('https://raw.githubusercontent.com/felixonmars/dnsmasq-china-list/master/bogus-nxdomain.china.conf')) {
for await (const line of await fetchRemoteTextByLine('https://raw.githubusercontent.com/felixonmars/dnsmasq-china-list/master/bogus-nxdomain.china.conf')) {
if (line && line.startsWith('bogus-nxdomain=')) {
const ip = line.slice(15).trim();
if (isProbablyIpv4(ip)) {
Expand Down
20 changes: 4 additions & 16 deletions Build/build-cdn-conf.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,15 @@
import path from 'path';
import { createRuleset } from './lib/create-file';
import { fetchRemoteTextAndReadByLine, readFileByLine } from './lib/fetch-text-by-line';
import { readFileByLine } from './lib/fetch-text-by-line';
import { createTrie } from './lib/trie';
import { task } from './lib/trace-runner';
import { processLine } from './lib/process-line';
import { SHARED_DESCRIPTION } from './lib/constants';

const publicSuffixPath: string = path.resolve(import.meta.dir, '../node_modules/.cache/public_suffix_list_dat.txt');

import { getPublicSuffixListTextPromise } from './download-publicsuffixlist';
const getS3OSSDomains = async (): Promise<Set<string>> => {
const trie = createTrie();

const publicSuffixFile = Bun.file(publicSuffixPath);

if (await publicSuffixFile.exists()) {
for await (const line of readFileByLine(publicSuffixFile)) {
trie.add(line);
}
} else {
console.log('public_suffix_list.dat not found, fetch directly from remote.');
for await (const line of await fetchRemoteTextAndReadByLine('https://publicsuffix.org/list/public_suffix_list.dat')) {
trie.add(line);
}
for await (const line of (await getPublicSuffixListTextPromise()).split('\n')) {
trie.add(line);
}

/**
Expand Down
4 changes: 2 additions & 2 deletions Build/build-chn-cidr.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { fetchRemoteTextAndReadByLine } from './lib/fetch-text-by-line';
import { fetchRemoteTextByLine } from './lib/fetch-text-by-line';
import { resolve as pathResolve } from 'path';
import { compareAndWriteFile, withBannerArray } from './lib/create-file';
import { processLineFromReadline } from './lib/process-line';
Expand All @@ -21,7 +21,7 @@ const INCLUDE_CIDRS = [
export const getChnCidrPromise = createMemoizedPromise(async () => {
const cidr = await traceAsync(
picocolors.gray('download chnroutes2'),
async () => processLineFromReadline(await fetchRemoteTextAndReadByLine('https://raw.githubusercontent.com/misakaio/chnroutes2/master/chnroutes.txt')),
async () => processLineFromReadline(await fetchRemoteTextByLine('https://raw.githubusercontent.com/misakaio/chnroutes2/master/chnroutes.txt')),
picocolors.gray
);
return traceSync(
Expand Down
4 changes: 2 additions & 2 deletions Build/build-internal-reverse-chn-cidr.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { fetchRemoteTextAndReadByLine } from './lib/fetch-text-by-line';
import { fetchRemoteTextByLine } from './lib/fetch-text-by-line';
import { processLineFromReadline } from './lib/process-line';
import path from 'path';
import { task } from './lib/trace-runner';
Expand Down Expand Up @@ -26,7 +26,7 @@ const RESERVED_IPV4_CIDR = [
];

export const buildInternalReverseChnCIDR = task(import.meta.path, async () => {
const cidr = await processLineFromReadline(await fetchRemoteTextAndReadByLine('https://raw.githubusercontent.com/misakaio/chnroutes2/master/chnroutes.txt'));
const cidr = await processLineFromReadline(await fetchRemoteTextByLine('https://raw.githubusercontent.com/misakaio/chnroutes2/master/chnroutes.txt'));

const reversedCidr = merge(
exclude(
Expand Down
4 changes: 2 additions & 2 deletions Build/build-microsoft-cdn.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import path from 'path';
import { task, traceAsync } from './lib/trace-runner';
import { createRuleset } from './lib/create-file';
import { fetchRemoteTextAndReadByLine } from './lib/fetch-text-by-line';
import { fetchRemoteTextByLine } from './lib/fetch-text-by-line';
import { createTrie } from './lib/trie';
import { SHARED_DESCRIPTION } from './lib/constants';
import { createMemoizedPromise } from './lib/memo-promise';
Expand All @@ -22,7 +22,7 @@ const BLACKLIST = [
export const getMicrosoftCdnRulesetPromise = createMemoizedPromise(async () => {
const set = await traceAsync('fetch accelerated-domains.china.conf', async () => {
const trie = createTrie();
for await (const line of await fetchRemoteTextAndReadByLine('https://raw.githubusercontent.com/felixonmars/dnsmasq-china-list/master/accelerated-domains.china.conf')) {
for await (const line of await fetchRemoteTextByLine('https://raw.githubusercontent.com/felixonmars/dnsmasq-china-list/master/accelerated-domains.china.conf')) {
if (line.startsWith('server=/') && line.endsWith('/114.114.114.114')) {
const domain = line.slice(8, -16);
trie.add(domain);
Expand Down
6 changes: 3 additions & 3 deletions Build/build-reject-domainset.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,16 +32,16 @@ export const buildRejectDomainSet = task(import.meta.path, async () => {
const [gorhill] = await Promise.all([
getGorhillPublicSuffixPromise(),
// Parse from remote hosts & domain lists
...HOSTS.map(entry => processHosts(entry[0], entry[1]).then(hosts => {
...HOSTS.map(entry => processHosts(entry[0], entry[1], entry[2], entry[3]).then(hosts => {
hosts.forEach(host => {
domainSets.add(host);
});
})),
...DOMAIN_LISTS.map(entry => processDomainLists(entry[0], entry[1])),
...DOMAIN_LISTS.map(entry => processDomainLists(entry[0], entry[1], entry[2])),
...ADGUARD_FILTERS.map(input => {
const promise = typeof input === 'string'
? processFilterRules(input)
: processFilterRules(input[0], input[1]);
: processFilterRules(input[0], input[1], input[2]);

return promise.then(({ white, black, foundDebugDomain }) => {
if (foundDebugDomain) {
Expand Down
4 changes: 2 additions & 2 deletions Build/build-speedtest-domainset.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,8 @@ const querySpeedtestApi = async (keyword: string): Promise<Array<string | null>>
s.acquire()
]))[0];

const randomUserAgent = topUserAgents[Math.floor(Math.random() * topUserAgents.length)];

try {
const randomUserAgent = topUserAgents[Math.floor(Math.random() * topUserAgents.length)];
const key = `fetch speedtest endpoints: ${keyword}`;
console.time(key);

Expand All @@ -47,6 +46,7 @@ const querySpeedtestApi = async (keyword: string): Promise<Array<string | null>>
}

const json = await res.json() as Array<{ url: string }>;

s.release();

console.timeEnd(key);
Expand Down
13 changes: 1 addition & 12 deletions Build/download-previous-build.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import fs from 'fs';
import fsp from 'fs/promises';
import path from 'path';
import os from 'os';
import { Readable } from 'stream';
import { pipeline } from 'stream/promises';
import { readFileByLine } from './lib/fetch-text-by-line';
Expand Down Expand Up @@ -85,16 +84,6 @@ export const downloadPreviousBuild = task(import.meta.path, async () => {
);
});

export const downloadPublicSuffixList = task(import.meta.path, async () => {
const publicSuffixPath = path.resolve(import.meta.dir, '../node_modules/.cache/public_suffix_list_dat.txt');
const resp = await fetchWithRetry('https://publicsuffix.org/list/public_suffix_list.dat', defaultRequestInit);

return Bun.write(publicSuffixPath, resp as Response);
}, 'download-publicsuffixlist');

if (import.meta.main) {
Promise.all([
downloadPreviousBuild(),
downloadPublicSuffixList()
]);
downloadPreviousBuild();
}
10 changes: 10 additions & 0 deletions Build/download-publicsuffixlist.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { fsCache } from './lib/cache-filesystem';
import { defaultRequestInit, fetchWithRetry } from './lib/fetch-retry';
import { createMemoizedPromise } from './lib/memo-promise';
import { traceAsync } from './lib/trace-runner';

export const getPublicSuffixListTextPromise = createMemoizedPromise(() => traceAsync('obtain public_suffix_list', () => fsCache.apply(
'public_suffix_list.dat',
() => fetchWithRetry('https://publicsuffix.org/list/public_suffix_list.dat', defaultRequestInit).then(r => r.text()),
{ ttl: 24 * 60 * 60 * 1000 }
)));
15 changes: 3 additions & 12 deletions Build/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { downloadPreviousBuild, downloadPublicSuffixList } from './download-previous-build';
import { downloadPreviousBuild } from './download-previous-build';
import { buildCommon } from './build-common';
import { buildAntiBogusDomain } from './build-anti-bogus-domain';
import { buildAppleCdn } from './build-apple-cdn';
Expand Down Expand Up @@ -33,23 +33,15 @@ import type { TaskResult } from './lib/trace-runner';
// const buildInternalReverseChnCIDRWorker = new Worker(new URL('./workers/build-internal-reverse-chn-cidr-worker.ts', import.meta.url));

const downloadPreviousBuildPromise = downloadPreviousBuild();
const downloadPublicSuffixListPromise = downloadPublicSuffixList();
const buildCommonPromise = downloadPreviousBuildPromise.then(() => buildCommon());
const buildAntiBogusDomainPromise = downloadPreviousBuildPromise.then(() => buildAntiBogusDomain());
const buildAppleCdnPromise = downloadPreviousBuildPromise.then(() => buildAppleCdn());
const buildCdnConfPromise = Promise.all([
downloadPreviousBuildPromise,
downloadPublicSuffixListPromise
]).then(() => buildCdnConf());
const buildRejectDomainSetPromise = Promise.all([
downloadPreviousBuildPromise,
downloadPublicSuffixListPromise
]).then(() => buildRejectDomainSet());
const buildCdnConfPromise = downloadPreviousBuildPromise.then(() => buildCdnConf());
const buildRejectDomainSetPromise = downloadPreviousBuildPromise.then(() => buildRejectDomainSet());
const buildTelegramCIDRPromise = downloadPreviousBuildPromise.then(() => buildTelegramCIDR());
const buildChnCidrPromise = downloadPreviousBuildPromise.then(() => buildChnCidr());
const buildSpeedtestDomainSetPromise = downloadPreviousBuildPromise.then(() => buildSpeedtestDomainSet());
const buildInternalCDNDomainsPromise = Promise.all([
downloadPublicSuffixListPromise,
buildCommonPromise,
buildCdnConfPromise
]).then(() => buildInternalCDNDomains());
Expand Down Expand Up @@ -84,7 +76,6 @@ import type { TaskResult } from './lib/trace-runner';

const stats = await Promise.all([
downloadPreviousBuildPromise,
downloadPublicSuffixListPromise,
buildCommonPromise,
buildAntiBogusDomainPromise,
buildAppleCdnPromise,
Expand Down
131 changes: 131 additions & 0 deletions Build/lib/cache-filesystem.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
// eslint-disable-next-line import/no-unresolved -- bun built-in module
import { Database } from 'bun:sqlite';
import os from 'os';
import path from 'path';
import fs from 'fs';
import picocolors from 'picocolors';

const identity = (x: any) => x;

// eslint-disable-next-line sukka-ts/no-const-enum -- bun is smart, right?
const enum CacheStatus {
Hit = 'hit',
Stale = 'stale',
Miss = 'miss'
}

export interface CacheOptions {
cachePath?: string,
tbd?: number
}

interface CacheApplyNonStringOption<T> {
ttl?: number | null,
serializer: (value: T) => string,
deserializer: (cached: string) => T,
temporaryBypass?: boolean
}

interface CacheApplyStringOption {
ttl?: number | null,
temporaryBypass?: boolean
}

type CacheApplyOption<T> = T extends string ? CacheApplyStringOption : CacheApplyNonStringOption<T>;

export class Cache {
db: Database;
tbd = 60 * 1000; // time before deletion
cachePath: string;

constructor({ cachePath = path.join(os.tmpdir() || '/tmp', 'hdc'), tbd }: CacheOptions = {}) {
this.cachePath = cachePath;
fs.mkdirSync(this.cachePath, { recursive: true });
if (tbd != null) this.tbd = tbd;

const db = new Database(path.join(this.cachePath, 'cache.db'));
db.exec('PRAGMA journal_mode = WAL');

db.prepare('CREATE TABLE IF NOT EXISTS cache (key TEXT PRIMARY KEY, value TEXT, ttl REAL NOT NULL);').run();
db.prepare('CREATE INDEX IF NOT EXISTS cache_ttl ON cache (ttl);').run();

// perform purge on startup

// ttl + tbd < now => ttl < now - tbd
const now = Date.now() - this.tbd;
db.prepare('DELETE FROM cache WHERE ttl < ?').run(now);

this.db = db;
}

set(key: string, value: string, ttl = 60 * 1000): void {
const insert = this.db.prepare(
'INSERT INTO cache (key, value, ttl) VALUES ($key, $value, $valid) ON CONFLICT(key) DO UPDATE SET value = $value, ttl = $valid'
);

insert.run({
$key: key,
$value: value,
$valid: Date.now() + ttl
});
}

get(key: string, defaultValue?: string): string | undefined {
const rv = this.db.prepare<{ value: string }, string>(
'SELECT value FROM cache WHERE key = ?'
).get(key);

if (!rv) return defaultValue;
return rv.value;
}

has(key: string): CacheStatus {
const now = Date.now();
const rv = this.db.prepare<{ ttl: number }, string>('SELECT ttl FROM cache WHERE key = ?').get(key);

return !rv ? CacheStatus.Miss : (rv.ttl > now ? CacheStatus.Hit : CacheStatus.Stale);
}

del(key: string): void {
this.db.prepare('DELETE FROM cache WHERE key = ?').run(key);
}

async apply<T>(
key: string,
fn: () => Promise<T>,
opt: CacheApplyOption<T>
): Promise<T> {
const { ttl, temporaryBypass } = opt;

if (temporaryBypass) {
return fn();
}
if (ttl === null) {
this.del(key);
return fn();
}

const cached = this.get(key);
let value: T;
if (cached == null) {
console.log(picocolors.yellow('[cache] miss'), picocolors.gray(key));
value = await fn();

const serializer = 'serializer' in opt ? opt.serializer : identity;
this.set(key, serializer(value), ttl);
} else {
console.log(picocolors.green('[cache] hit'), picocolors.gray(key));

const deserializer = 'deserializer' in opt ? opt.deserializer : identity;
value = deserializer(cached);
}
return value;
}
}

export const fsCache = new Cache({ cachePath: path.resolve(import.meta.dir, '../../.cache') });

const separator = String.fromCharCode(0);

export const serializeSet = (set: Set<string>) => Array.from(set).join(separator);
export const deserializeSet = (str: string) => new Set(str.split(separator));
4 changes: 3 additions & 1 deletion Build/lib/fetch-text-by-line.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import type { BunFile } from 'bun';
import { fetchWithRetry, defaultRequestInit } from './fetch-retry';
import { fsCache } from './cache-filesystem';
import picocolors from 'picocolors';
// import { TextLineStream } from './text-line-transform-stream';
// import { PolyfillTextDecoderStream } from './text-decoder-stream';

Expand Down Expand Up @@ -78,6 +80,6 @@ export async function *createReadlineInterfaceFromResponse(resp: Response): Asyn
}
}

export function fetchRemoteTextAndReadByLine(url: string | URL) {
export function fetchRemoteTextByLine(url: string | URL) {
return fetchWithRetry(url, defaultRequestInit).then(res => createReadlineInterfaceFromResponse(res as Response));
}
Loading

0 comments on commit 230ac3e

Please sign in to comment.