Skip to content

Commit

Permalink
expose preventEviction as unsafePreventEviction for Durable Objects (#…
Browse files Browse the repository at this point in the history
…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
  • Loading branch information
RamIdeas authored and mrbbot committed Nov 1, 2023
1 parent 6bdf26c commit f02927b
Show file tree
Hide file tree
Showing 6 changed files with 108 additions and 9 deletions.
19 changes: 14 additions & 5 deletions packages/miniflare/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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 });
}
}
}
Expand Down
3 changes: 2 additions & 1 deletion packages/miniflare/src/plugins/core/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -424,14 +424,15 @@ 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
// path when persisting to the file-system. `-` is invalid in
// JavaScript class names, but safe on filesystems (incl. Windows).
uniqueKey:
unsafeUniqueKey ?? `${options.name ?? ""}-${className}`,
preventEviction: unsafePreventEviction,
};
}
),
Expand Down
14 changes: 12 additions & 2 deletions packages/miniflare/src/plugins/do/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
}),
])
)
Expand All @@ -35,15 +37,23 @@ export function normaliseDurableObject(
designator: NonNullable<
z.infer<typeof DurableObjectsOptionsSchema>["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 =
isObject && designator.scriptName !== undefined
? 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";
Expand Down
8 changes: 7 additions & 1 deletion packages/miniflare/src/plugins/shared/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,13 @@ export type Persistence = z.infer<typeof PersistenceSchema>;
// Maps **service** names to the Durable Object class names exported by them
export type DurableObjectClassNames = Map<
string,
Map</* className */ string, /* unsafeUniqueKey */ string | undefined>
Map<
/* className */ string,
{
unsafeUniqueKey?: string;
unsafePreventEviction?: boolean;
}
>
>;

// Maps queue names to the Worker that wishes to consume it. Note each queue
Expand Down
1 change: 1 addition & 0 deletions packages/miniflare/src/shared/error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<MiniflareCoreErrorCode> {}
72 changes: 72 additions & 0 deletions packages/miniflare/test/plugins/do/index.spec.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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);
});

0 comments on commit f02927b

Please sign in to comment.