From 6183645cc68d9857ccaa4deb8a34d4caaebc9be3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matheus=20Gaudencio=20do=20R=C3=AAgo?= Date: Mon, 30 Sep 2024 13:23:09 -0300 Subject: [PATCH] Improve env names and default --- README.md | 1 + runtime/caches/fileSystem.ts | 244 ++++++++++++++++++++++++++++++++++- runtime/caches/mod.ts | 4 +- 3 files changed, 242 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 50f861541..7652a3d65 100644 --- a/README.md +++ b/README.md @@ -174,6 +174,7 @@ Here is a table with the integrations that we have built and the statuses of the | `ENABLE_LOADER_CACHE` | Flag to enable or disable the loader cache | `true` | | `LOADER_CACHE_START_TRESHOLD` | Cache start threshold | `0` | | `WEB_CACHE_ENGINE` | Defines the cache engine(s) to use | `"FILE_SYSTEM,CACHE_API"` | +| `FILE_SYSTEM_CACHE_DIRECTORY` | Directory path for file system cache | `/tmp` | | `CACHE_MAX_SIZE` | Maximum size of the file system cache (in bytes) | `1073741824` (1 GB) | | `CACHE_TTL_AUTOPURGE` | Flag to automatically delete expired items from the file system cache (cpu intensive) | `false` | | `CACHE_TTL_RESOLUTION` | Time interval to check for expired items in the file system cache (in milliseconds) | `30000` (30 seconds) | diff --git a/runtime/caches/fileSystem.ts b/runtime/caches/fileSystem.ts index 94e1debf7..02c0c861e 100644 --- a/runtime/caches/fileSystem.ts +++ b/runtime/caches/fileSystem.ts @@ -1,11 +1,243 @@ -const FILE_SYSTEM_CACHE_DIRECTORY = "/tmp"; +import { existsSync } from "@std/fs"; +import { logger } from "../../observability/otel/config.ts"; -const hasWritePerm = async (fsDir: string): Promise => { +import { + assertCanBeCached, + assertNoOptions, + withCacheNamespace +} from "./utils.ts"; + +const FILE_SYSTEM_CACHE_DIRECTORY = + Deno.env.get("FILE_SYSTEM_CACHE_DIRECTORY") ?? '/tmp/deco_cache'; + +// Function to convert headers object to a Uint8Array +function headersToUint8Array(headers: [string, string][]) { + const headersStr = JSON.stringify(headers); + return new TextEncoder().encode(headersStr); +} + +// Function to combine the body and headers into a single buffer +function generateCombinedBuffer(body: Uint8Array, headers: Uint8Array) { + // This prepends the header length to the combined buffer. As it has 4 bytes in size, + // it can store up to 2^32 - 1 bytes of headers (4GB). This should be enough for all deco use cases. + const headerLength = new Uint8Array(new Uint32Array([headers.length]).buffer); + + // Concatenate length, headers, and body into one Uint8Array + const combinedBuffer = new Uint8Array( + headerLength.length + headers.length + body.length, + ); + combinedBuffer.set(headerLength, 0); + combinedBuffer.set(headers, headerLength.length); + combinedBuffer.set(body, headerLength.length + headers.length); + return combinedBuffer; +} + +// Function to extract the headers and body from a combined buffer +function extractCombinedBuffer(combinedBuffer: Uint8Array) { + // Extract the header length from the combined buffer + const headerLengthArray = combinedBuffer.slice(0, 4); + const headerLength = new Uint32Array(headerLengthArray.buffer)[0]; + + // Extract the headers and body from the combined buffer + const headers = combinedBuffer.slice(4, 4 + headerLength); + const body = combinedBuffer.slice(4 + headerLength); + return { headers, body }; +} + +function getIterableHeaders(headers: Uint8Array) { + const headersStr = new TextDecoder().decode(headers); + + // Directly parse the string as an array of [key, value] pairs + const headerPairs: [string, string][] = JSON.parse(headersStr); + + // Filter out any pairs with empty key or value + const filteredHeaders = headerPairs.filter(([key, value]) => + key !== "" && value !== "" + ); + return filteredHeaders; +} + +function createFileSystemCache(): CacheStorage { + let isCacheInitialized = false; + async function assertCacheDirectory() { + try { + if ( + FILE_SYSTEM_CACHE_DIRECTORY && !existsSync(FILE_SYSTEM_CACHE_DIRECTORY) + ) { + await Deno.mkdirSync(FILE_SYSTEM_CACHE_DIRECTORY, { recursive: true }); + } + isCacheInitialized = true; + } catch (err) { + console.error("Unable to initialize file system cache directory", err); + } + } + + async function putFile( + key: string, + responseArray: Uint8Array, + ) { + if (!isCacheInitialized) { + await assertCacheDirectory(); + } + const filePath = `${FILE_SYSTEM_CACHE_DIRECTORY}/${key}`; + + await Deno.writeFile(filePath, responseArray); + return; + } + + async function getFile(key: string) { + if (!isCacheInitialized) { + await assertCacheDirectory(); + } + try { + const filePath = `${FILE_SYSTEM_CACHE_DIRECTORY}/${key}`; + const fileContent = await Deno.readFile(filePath); + return fileContent; + } catch (err) { + // Error code different for file/dir not found + // The file won't be found in cases where it's not cached + if (err.code !== "ENOENT") { + logger.error(`error when reading from file system, ${err}`); + } + return null; + } + } + + async function deleteFile(key: string) { + if (!isCacheInitialized) { + await assertCacheDirectory(); + } + try { + const filePath = `${FILE_SYSTEM_CACHE_DIRECTORY}/${key}`; + await Deno.remove(filePath); + return true; + } catch (err) { + logger.error(`error when deleting from file system, ${err}`); + return false; + } + } + + const caches: CacheStorage = { + delete: (_cacheName: string): Promise => { + throw new Error("Not Implemented"); + }, + has: (_cacheName: string): Promise => { + throw new Error("Not Implemented"); + }, + keys: (): Promise => { + throw new Error("Not Implemented"); + }, + match: ( + _request: URL | RequestInfo, + _options?: MultiCacheQueryOptions | undefined, + ): Promise => { + throw new Error("Not Implemented"); + }, + open: (cacheName: string): Promise => { + const requestURLSHA1 = withCacheNamespace(cacheName); + return Promise.resolve({ + /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Cache/add) */ + add: (_request: RequestInfo | URL): Promise => { + throw new Error("Not Implemented"); + }, + /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Cache/addAll) */ + addAll: (_requests: RequestInfo[]): Promise => { + throw new Error("Not Implemented"); + }, + /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Cache/delete) */ + delete: async ( + request: RequestInfo | URL, + options?: CacheQueryOptions, + ): Promise => { + assertNoOptions(options); + + const deleteResponse = await deleteFile( + await requestURLSHA1(request), + ); + return deleteResponse; + }, + /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Cache/keys) */ + keys: ( + _request?: RequestInfo | URL, + _options?: CacheQueryOptions, + ): Promise> => { + throw new Error("Not Implemented"); + }, + /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Cache/match) */ + match: async ( + request: RequestInfo | URL, + options?: CacheQueryOptions, + ): Promise => { + assertNoOptions(options); + const cacheKey = await requestURLSHA1(request); + const data = await getFile(cacheKey); + + if (data === null) { + return undefined; + } + + const { headers, body } = extractCombinedBuffer(data); + const iterableHeaders = getIterableHeaders(headers); + const responseHeaders = new Headers(iterableHeaders); + return new Response( + body, + { headers: responseHeaders }, + ); + }, + /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Cache/matchAll) */ + matchAll: ( + _request?: RequestInfo | URL, + _options?: CacheQueryOptions, + ): Promise> => { + throw new Error("Not Implemented"); + }, + /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Cache/put) */ + put: async ( + request: RequestInfo | URL, + response: Response, + ): Promise => { + const req = new Request(request); + assertCanBeCached(req, response); + + if (!response.body) { + return; + } + + const cacheKey = await requestURLSHA1(request); + + const bodyBuffer = await response.arrayBuffer() + .then((buffer) => new Uint8Array(buffer)) + .then((buffer) => { + return buffer; + }); + const headersBuffer = headersToUint8Array([ + ...response.headers.entries(), + ]); + const buffer = generateCombinedBuffer(bodyBuffer, headersBuffer); + + await putFile( + cacheKey, + buffer + ).catch( + (err) => { + console.error("file system error", err); + }, + ); + }, + }); + }, + }; + + return caches; +} + +const hasWritePerm = async (): Promise => { return await Deno.permissions.query( - { name: "write", path: fsDir } as const, + { name: "write", path: FILE_SYSTEM_CACHE_DIRECTORY } as const, ).then((status) => status.state === "granted"); }; -export const isFileSystemAvailable = - FILE_SYSTEM_CACHE_DIRECTORY !== undefined && - await hasWritePerm(FILE_SYSTEM_CACHE_DIRECTORY); +export const isFileSystemAvailable = await hasWritePerm() && + FILE_SYSTEM_CACHE_DIRECTORY !== undefined; + +export const caches = createFileSystemCache(); \ No newline at end of file diff --git a/runtime/caches/mod.ts b/runtime/caches/mod.ts index ae89d9f3c..32c0ba00e 100644 --- a/runtime/caches/mod.ts +++ b/runtime/caches/mod.ts @@ -8,6 +8,8 @@ import { createTieredCache } from "./tiered.ts"; import { caches as lruCache } from "./lrucache.ts"; +import { caches as fileSystem } from "./fileSystem.ts"; + export const ENABLE_LOADER_CACHE: boolean = Deno.env.get("ENABLE_LOADER_CACHE") !== "false"; const DEFAULT_CACHE_ENGINE = "CACHE_API"; @@ -30,7 +32,7 @@ export const cacheImplByEngine: Record = { isAvailable: typeof globalThis.caches !== "undefined", }, FILE_SYSTEM: { - implementation: headersCache(lruCache(globalThis.caches)), + implementation: headersCache(lruCache(fileSystem)), isAvailable: isFileSystemAvailable, }, };