From 79f911de748245d513dfac8a0273854a5b0cde15 Mon Sep 17 00:00:00 2001 From: Rahul Sethi <5822355+RamIdeas@users.noreply.github.com> Date: Fri, 27 Oct 2023 15:55:30 +0100 Subject: [PATCH] expose preventEviction as unsafePreventEviction for Durable Objects (#726) * expose unsafePreventEviction option for durable object bindings * add tests for unsafePreventEviction * parallelise long test run * check unsafePreventEviction value is same across all bindings to each durable object * fix nit * increase (prevent) eviction test timeouts --- packages/miniflare/src/index.ts | 19 +++-- packages/miniflare/src/plugins/core/index.ts | 3 +- packages/miniflare/src/plugins/do/index.ts | 14 +++- .../miniflare/src/plugins/shared/index.ts | 8 ++- packages/miniflare/src/shared/error.ts | 1 + .../miniflare/test/plugins/do/index.spec.ts | 72 +++++++++++++++++++ 6 files changed, 108 insertions(+), 9 deletions(-) diff --git a/packages/miniflare/src/index.ts b/packages/miniflare/src/index.ts index 4b3840f3ba88..574bac1b865d 100644 --- a/packages/miniflare/src/index.ts +++ b/packages/miniflare/src/index.ts @@ -269,6 +269,7 @@ function getDurableObjectClassNames( // Fallback to current worker service if name not defined serviceName = workerServiceName, unsafeUniqueKey, + unsafePreventEviction, } = normaliseDurableObject(designator); // Get or create `Map` mapping class name to optional unsafe unique key let classNames = serviceClassNames.get(serviceName); @@ -278,19 +279,27 @@ function getDurableObjectClassNames( } if (classNames.has(className)) { // If we've already seen this class in this service, make sure the - // unsafe unique keys match - const existingUnsafeUniqueKey = classNames.get(className); - if (existingUnsafeUniqueKey !== unsafeUniqueKey) { + // unsafe unique keys and unsafe prevent eviction values match + const existingInfo = classNames.get(className); + if (existingInfo?.unsafeUniqueKey !== unsafeUniqueKey) { throw new MiniflareCoreError( "ERR_DIFFERENT_UNIQUE_KEYS", `Multiple unsafe unique keys defined for Durable Object "${className}" in "${serviceName}": ${JSON.stringify( unsafeUniqueKey - )} and ${JSON.stringify(existingUnsafeUniqueKey)}` + )} and ${JSON.stringify(existingInfo?.unsafeUniqueKey)}` + ); + } + if (existingInfo?.unsafePreventEviction !== unsafePreventEviction) { + throw new MiniflareCoreError( + "ERR_DIFFERENT_PREVENT_EVICTION", + `Multiple unsafe prevent eviction values defined for Durable Object "${className}" in "${serviceName}": ${JSON.stringify( + unsafePreventEviction + )} and ${JSON.stringify(existingInfo?.unsafePreventEviction)}` ); } } else { // Otherwise, just add it - classNames.set(className, unsafeUniqueKey); + classNames.set(className, { unsafeUniqueKey, unsafePreventEviction }); } } } diff --git a/packages/miniflare/src/plugins/core/index.ts b/packages/miniflare/src/plugins/core/index.ts index b710efe4b0d8..f7e861393885 100644 --- a/packages/miniflare/src/plugins/core/index.ts +++ b/packages/miniflare/src/plugins/core/index.ts @@ -424,7 +424,7 @@ export const CORE_PLUGIN: Plugin< compatibilityFlags: options.compatibilityFlags, bindings: workerBindings, durableObjectNamespaces: classNamesEntries.map( - ([className, unsafeUniqueKey]) => { + ([className, { unsafeUniqueKey, unsafePreventEviction }]) => { return { className, // This `uniqueKey` will (among other things) be used as part of the @@ -432,6 +432,7 @@ export const CORE_PLUGIN: Plugin< // JavaScript class names, but safe on filesystems (incl. Windows). uniqueKey: unsafeUniqueKey ?? `${options.name ?? ""}-${className}`, + preventEviction: unsafePreventEviction, }; } ), diff --git a/packages/miniflare/src/plugins/do/index.ts b/packages/miniflare/src/plugins/do/index.ts index 6d8865a95167..3e6cd9e28b3e 100644 --- a/packages/miniflare/src/plugins/do/index.ts +++ b/packages/miniflare/src/plugins/do/index.ts @@ -22,6 +22,8 @@ export const DurableObjectsOptionsSchema = z.object({ // another `workerd` process, to ensure the IDs created by the stub // object can be used by the real object too. unsafeUniqueKey: z.string().optional(), + // Prevents the Durable Object being evicted. + unsafePreventEviction: z.boolean().optional(), }), ]) ) @@ -35,7 +37,12 @@ export function normaliseDurableObject( designator: NonNullable< z.infer["durableObjects"] >[string] -): { className: string; serviceName?: string; unsafeUniqueKey?: string } { +): { + className: string; + serviceName?: string; + unsafeUniqueKey?: string; + unsafePreventEviction?: boolean; +} { const isObject = typeof designator === "object"; const className = isObject ? designator.className : designator; const serviceName = @@ -43,7 +50,10 @@ export function normaliseDurableObject( ? getUserServiceName(designator.scriptName) : undefined; const unsafeUniqueKey = isObject ? designator.unsafeUniqueKey : undefined; - return { className, serviceName, unsafeUniqueKey }; + const unsafePreventEviction = isObject + ? designator.unsafePreventEviction + : undefined; + return { className, serviceName, unsafeUniqueKey, unsafePreventEviction }; } export const DURABLE_OBJECTS_PLUGIN_NAME = "do"; diff --git a/packages/miniflare/src/plugins/shared/index.ts b/packages/miniflare/src/plugins/shared/index.ts index 9e67cfff6ff0..7de087f25366 100644 --- a/packages/miniflare/src/plugins/shared/index.ts +++ b/packages/miniflare/src/plugins/shared/index.ts @@ -16,7 +16,13 @@ export type Persistence = z.infer; // Maps **service** names to the Durable Object class names exported by them export type DurableObjectClassNames = Map< string, - Map + Map< + /* className */ string, + { + unsafeUniqueKey?: string; + unsafePreventEviction?: boolean; + } + > >; // Maps queue names to the Worker that wishes to consume it. Note each queue diff --git a/packages/miniflare/src/shared/error.ts b/packages/miniflare/src/shared/error.ts index 948f33e2f1bf..d86a9aed6231 100644 --- a/packages/miniflare/src/shared/error.ts +++ b/packages/miniflare/src/shared/error.ts @@ -29,5 +29,6 @@ export type MiniflareCoreErrorCode = | "ERR_VALIDATION" // Options failed to parse | "ERR_DUPLICATE_NAME" // Multiple workers defined with same name | "ERR_DIFFERENT_UNIQUE_KEYS" // Multiple Durable Object bindings declared for same class with different unsafe unique keys + | "ERR_DIFFERENT_PREVENT_EVICTION" // Multiple Durable Object bindings declared for same class with different unsafe prevent eviction values | "ERR_MULTIPLE_OUTBOUNDS"; // Both `outboundService` and `fetchMock` specified export class MiniflareCoreError extends MiniflareError {} diff --git a/packages/miniflare/test/plugins/do/index.spec.ts b/packages/miniflare/test/plugins/do/index.spec.ts index 587d6dbe345c..e8520385f25f 100644 --- a/packages/miniflare/test/plugins/do/index.spec.ts +++ b/packages/miniflare/test/plugins/do/index.spec.ts @@ -1,6 +1,7 @@ import assert from "assert"; import fs from "fs/promises"; import path from "path"; +import { setTimeout } from "timers/promises"; import test from "ava"; import { DeferredPromise, @@ -33,6 +34,24 @@ export default { }, };`; +const STATEFUL_SCRIPT = (responsePrefix = "") => ` + export class DurableObject { + constructor() { + this.uuid = crypto.randomUUID(); + } + fetch() { + return new Response(${JSON.stringify(responsePrefix)} + this.uuid); + } + } + export default { + fetch(req, env, ctx) { + const singleton = env.DURABLE_OBJECT.idFromName(""); + const durableObject = env.DURABLE_OBJECT.get(singleton); + return durableObject.fetch(req); + } + } +`; + test("persists Durable Object data in-memory between options reloads", async (t) => { const opts: MiniflareOptions = { modules: true, @@ -309,3 +328,56 @@ test("proxies Durable Object methods", async (t) => { const event = await eventPromise; t.is(event.data, "echo:hello"); }); + +test("Durable Object eviction", async (t) => { + // this test requires testing over a 10 second timeout + t.timeout(12_000); + + // first set unsafePreventEviction to undefined + const mf = new Miniflare({ + verbose: true, + modules: true, + script: STATEFUL_SCRIPT(), + durableObjects: { + DURABLE_OBJECT: "DurableObject", + }, + }); + t.teardown(() => mf.dispose()); + + // get uuid generated at durable object startup + let res = await mf.dispatchFetch("http://localhost"); + const original = await res.text(); + + // after 10+ seconds, durable object should be evicted, so new uuid generated + await setTimeout(10_000); + res = await mf.dispatchFetch("http://localhost"); + t.not(await res.text(), original); +}); + +test("prevent Durable Object eviction", async (t) => { + // this test requires testing over a 10 second timeout + t.timeout(12_000); + + // first set unsafePreventEviction to undefined + const mf = new Miniflare({ + verbose: true, + modules: true, + script: STATEFUL_SCRIPT(), + durableObjects: { + DURABLE_OBJECT: { + className: "DurableObject", + unsafePreventEviction: true, + }, + }, + }); + t.teardown(() => mf.dispose()); + + // get uuid generated at durable object startup + let res = await mf.dispatchFetch("http://localhost"); + const original = await res.text(); + + // after 10+ seconds, durable object should NOT be evicted, so same uuid + await setTimeout(10_000); + res = await mf.dispatchFetch("http://localhost"); + t.is(await res.text(), original); +});