Skip to content

Commit

Permalink
Support durableObjectsPersist option
Browse files Browse the repository at this point in the history
Uses cloudflare/workerd#302 to enable Durable Object persistence.

- `durableObjectsPersist: undefined | false | "memory:"` stores
  "in-memory"
- `durableObjectsPersist: true` stores under `.mf` on the file-system
- `durableObjectsPersist: "(file://)?<path>"` stores under `<path>`
  on the file-system

Note Miniflare 2 persisted in-memory data between options reloads. In
Miniflare 3, `workerd` restarts on every reload, so if we used
`workerd`'s in-memory storage, we'd lose data every time options
changed. To maintain Miniflare 2's behaviour, "in-memory" storage
isn't actually in-memory. Instead, we write to a temporary directory
and clean this up on `dispose()`/exit. 🙈

Also fixes a bug if multiple services bound to the same Durable
Object class. This would result in duplicate entries in
`durableObjectClassNames`. This has been changed from a `Map` of
`string[]`s to a `Map` of `Set<string>`s to enforce the uniqueness
constraint.

Closes cloudflare/workers-sdk#2403
Closes cloudflare/workers-sdk#2458

Internal ticket: DEVX-219
  • Loading branch information
mrbbot committed Feb 27, 2023
1 parent 3b09263 commit f73ba3e
Show file tree
Hide file tree
Showing 12 changed files with 410 additions and 54 deletions.
104 changes: 87 additions & 17 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion packages/tre/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
"source-map-support": "0.5.21",
"stoppable": "^1.1.0",
"undici": "^5.13.0",
"workerd": "^1.20221111.5",
"workerd": "^1.20230221.0",
"ws": "^8.11.0",
"youch": "^3.2.2",
"zod": "^3.18.0"
Expand Down
35 changes: 30 additions & 5 deletions packages/tre/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import assert from "assert";
import crypto from "crypto";
import fs from "fs";
import http from "http";
import net from "net";
import os from "os";
import path from "path";
import { Duplex } from "stream";
import type {
IncomingRequestCfProperties,
Expand All @@ -22,6 +26,7 @@ import {
fetch,
} from "./http";
import {
DurableObjectClassNames,
GatewayConstructor,
GatewayFactory,
HEADER_CF_BLOB,
Expand Down Expand Up @@ -124,8 +129,8 @@ function validateOptions(
// in other services.
function getDurableObjectClassNames(
allWorkerOpts: PluginWorkerOptions[]
): Map<string, string[]> {
const serviceClassNames = new Map<string, string[]>();
): DurableObjectClassNames {
const serviceClassNames: DurableObjectClassNames = new Map();
for (const workerOpts of allWorkerOpts) {
const workerServiceName = getUserServiceName(workerOpts.core.name);
for (const designator of Object.values(
Expand All @@ -136,9 +141,9 @@ function getDurableObjectClassNames(
normaliseDurableObject(designator);
let classNames = serviceClassNames.get(serviceName);
if (classNames === undefined) {
serviceClassNames.set(serviceName, (classNames = []));
serviceClassNames.set(serviceName, (classNames = new Set()));
}
classNames.push(className);
classNames.add(className);
}
}
return serviceClassNames;
Expand Down Expand Up @@ -201,6 +206,12 @@ export class Miniflare {
#removeRuntimeExitHook?: () => void;
#runtimeEntryURL?: URL;

// Path to temporary directory for use as scratch space/"in-memory" Durable
// Object storage. Note this may not exist, it's up to the consumers to
// create this if needed. Deleted on `dispose()`.
readonly #tmpPath: string;
readonly #removeTmpPathExitHook: () => void;

// Mutual exclusion lock for runtime operations (i.e. initialisation and
// updating config). This essentially puts initialisation and future updates
// in a queue, ensuring they're performed in calling order.
Expand Down Expand Up @@ -272,6 +283,14 @@ export class Miniflare {
}
});

this.#tmpPath = path.join(
os.tmpdir(),
`miniflare-${crypto.randomBytes(16).toString("hex")}`
);
this.#removeTmpPathExitHook = exitHook(() => {
fs.rmSync(this.#tmpPath, { force: true, recursive: true });
});

this.#disposeController = new AbortController();
this.#runtimeMutex = new Mutex();
this.#initPromise = this.#runtimeMutex.runWith(() => this.#init());
Expand Down Expand Up @@ -646,6 +665,7 @@ export class Miniflare {
durableObjectClassNames,
additionalModules,
loopbackPort,
tmpPath: this.#tmpPath,
});
if (pluginServices !== undefined) {
for (const service of pluginServices) {
Expand Down Expand Up @@ -759,10 +779,15 @@ export class Miniflare {
await this.#initPromise;
await this.#lastUpdatePromise;
} finally {
// Cleanup as much as possible even if `#init()` threw
// Remove exit hooks, we're cleaning up what they would've cleaned up now
this.#removeTmpPathExitHook();
this.#removeRuntimeExitHook?.();

// Cleanup as much as possible even if `#init()` threw
await this.#runtime?.dispose();
await this.#stopLoopbackServer();
// `rm -rf ${#tmpPath}`, this won't throw if `#tmpPath` doesn't exist
await fs.promises.rm(this.#tmpPath, { force: true, recursive: true });
}
}
}
Expand Down
16 changes: 12 additions & 4 deletions packages/tre/src/plugins/core/index.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import { readFileSync } from "fs";
import fs from "fs/promises";
import { TextEncoder } from "util";
import { DURABLE_OBJECTS_STORAGE_SERVICE_NAME } from "@miniflare/tre";
import { bold } from "kleur/colors";
import { z } from "zod";
import {
Service,
Worker_Binding,
Worker_Module,
kVoid,
supportedCompatibilityDate,
} from "../../runtime";
import { Awaitable, JsonSchema, Log, MiniflareCoreError } from "../../shared";
Expand Down Expand Up @@ -383,7 +383,9 @@ export const CORE_PLUGIN: Plugin<
}

const name = getUserServiceName(options.name);
const classNames = durableObjectClassNames.get(name) ?? [];
const classNames = Array.from(
durableObjectClassNames.get(name) ?? new Set<string>()
);
const compatibilityDate = validateCompatibilityDate(
log,
options.compatibilityDate ?? FALLBACK_COMPATIBILITY_DATE
Expand All @@ -398,9 +400,15 @@ export const CORE_PLUGIN: Plugin<
bindings: workerBindings,
durableObjectNamespaces: classNames.map((className) => ({
className,
uniqueKey: 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: `${options.name ?? ""}-${className}`,
})),
durableObjectStorage: { inMemory: kVoid },
durableObjectStorage:
classNames.length === 0
? undefined
: { localDisk: DURABLE_OBJECTS_STORAGE_SERVICE_NAME },
cacheApiOutbound: { name: getCacheServiceName(workerIndex) },
},
});
Expand Down
Loading

0 comments on commit f73ba3e

Please sign in to comment.