Skip to content

Commit

Permalink
refactor: garbage-collect unused workspaces (#2161)
Browse files Browse the repository at this point in the history
  • Loading branch information
fwouts authored Oct 26, 2023
1 parent cdf1eb8 commit abc1dcb
Show file tree
Hide file tree
Showing 15 changed files with 266 additions and 435 deletions.
76 changes: 26 additions & 50 deletions daemon/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,70 +4,46 @@ export type KillResponse = {
pid: number;
};

export interface UpdateClientStatusRequest {
clientId: string;
alive: boolean;
}

export type UpdateClientStatusResponse = Record<never, never>;

export interface GetWorkspaceRequest {
export type CrawlFileRequest = {
absoluteFilePath: string;
}
};

export type GetWorkspaceResponse =
| {
workspaceId: null;
}
export type CrawlFileResponse =
| { rootDir: null; previewables: never[] }
| {
workspaceId: string;
rootDir: string;
previewables: Array<{
start: number;
end: number;
id: string;
}>;
};

export interface DisposeWorkspaceRequest {
workspaceId: string;
}

export type DisposeWorkspaceResponse = Record<string, never>;

export interface CrawlFileRequest {
workspaceId: string;
absoluteFilePath: string;
}

export interface CrawlFileResponse {
previewables: Array<{
start: number;
end: number;
id: string;
}>;
}

export interface StartPreviewRequest {
workspaceId: string;
}
export type StartPreviewRequest = {
rootDir: string;
};

export interface StartPreviewResponse {
export type StartPreviewResponse = {
url: string;
}
};

export interface CheckPreviewStatusRequest {
workspaceId: string;
}
export type CheckPreviewStatusRequest = {
rootDir: string;
};

export interface CheckPreviewStatusResponse {
export type CheckPreviewStatusResponse = {
running: boolean;
}
};

export interface StopPreviewRequest {
workspaceId: string;
}
export type StopPreviewRequest = {
rootDir: string;
};

export type StopPreviewResponse = Record<string, never>;
export type StopPreviewResponse = Record<never, never>;

export interface UpdatePendingFileRequest {
export type UpdatePendingFileRequest = {
absoluteFilePath: string;
utf8Content: string | null;
}
};

export type UpdatePendingFileResponse = Record<string, never>;
export type UpdatePendingFileResponse = Record<never, never>;
16 changes: 0 additions & 16 deletions daemon/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,12 @@ import type {
CheckPreviewStatusResponse,
CrawlFileRequest,
CrawlFileResponse,
DisposeWorkspaceRequest,
DisposeWorkspaceResponse,
GetWorkspaceRequest,
GetWorkspaceResponse,
KillRequest,
KillResponse,
StartPreviewRequest,
StartPreviewResponse,
StopPreviewRequest,
StopPreviewResponse,
UpdateClientStatusRequest,
UpdateClientStatusResponse,
UpdatePendingFileRequest,
UpdatePendingFileResponse,
} from "./api.js";
Expand Down Expand Up @@ -78,9 +72,6 @@ export function createClient(baseUrl: string): Client {

const client: Client = {
kill: () => makeRPC<KillRequest, KillResponse>("/previewjs/kill")({}),
updateClientStatus: makeRPC("/previewjs/clients/status"),
getWorkspace: makeRPC("/workspaces/get"),
disposeWorkspace: makeRPC("/workspaces/dispose"),
crawlFile: makeRPC("/crawl-file"),
startPreview: makeRPC("/previews/start"),
checkPreviewStatus: makeRPC("/previews/status"),
Expand All @@ -104,13 +95,6 @@ export function destroyDaemon(lockFilePath: string) {

export interface Client {
kill(): Promise<KillResponse>;
updateClientStatus(
request: UpdateClientStatusRequest
): Promise<UpdateClientStatusResponse>;
getWorkspace(request: GetWorkspaceRequest): Promise<GetWorkspaceResponse>;
disposeWorkspace(
request: DisposeWorkspaceRequest
): Promise<DisposeWorkspaceResponse>;
crawlFile(request: CrawlFileRequest): Promise<CrawlFileResponse>;
startPreview(request: StartPreviewRequest): Promise<StartPreviewResponse>;
checkPreviewStatus(
Expand Down
176 changes: 53 additions & 123 deletions daemon/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import type { PreviewServer, Workspace } from "@previewjs/core";
import { load } from "@previewjs/loader/runner";
import crypto from "crypto";
import exitHook from "exit-hook";
import {
appendFileSync,
Expand All @@ -17,25 +16,17 @@ import type {
CheckPreviewStatusResponse,
CrawlFileRequest,
CrawlFileResponse,
DisposeWorkspaceRequest,
DisposeWorkspaceResponse,
GetWorkspaceRequest,
GetWorkspaceResponse,
KillRequest,
KillResponse,
StartPreviewRequest,
StartPreviewResponse,
StopPreviewRequest,
StopPreviewResponse,
UpdateClientStatusRequest,
UpdateClientStatusResponse,
UpdatePendingFileRequest,
UpdatePendingFileResponse,
} from "./api.js";
import { createClient } from "./client.js";

const AUTOMATIC_SHUTDOWN_DELAY_SECONDS = 30;

const lockFilePath = process.env.PREVIEWJS_LOCK_FILE;
if (lockFilePath) {
if (existsSync(lockFilePath)) {
Expand Down Expand Up @@ -128,8 +119,6 @@ export async function startDaemon({
});
const logger = previewjs.logger;

const clients = new Set<string>();
const workspaces: Record<string, Workspace> = {};
const previewServers: Record<string, PreviewServer> = {};
const endpoints: Record<string, (req: any) => Promise<any>> = {};
let wslRoot: string | null = null;
Expand Down Expand Up @@ -257,138 +246,79 @@ export async function startDaemon({
};
});

let shutdownTimer: NodeJS.Timeout | null = null;
endpoint<UpdateClientStatusRequest, UpdateClientStatusResponse>(
"/previewjs/clients/status",
async (req) => {
if (req.alive) {
clients.add(req.clientId);
} else {
clients.delete(req.clientId);
}
if (shutdownTimer) {
clearTimeout(shutdownTimer);
}
shutdownTimer = setTimeout(() => {
if (clients.size === 0) {
logger.info(
`No clients are alive after ${AUTOMATIC_SHUTDOWN_DELAY_SECONDS}s. Shutting down.`
);
process.exit(0);
}
}, AUTOMATIC_SHUTDOWN_DELAY_SECONDS * 1000);
return {};
}
);

endpoint<GetWorkspaceRequest, GetWorkspaceResponse>(
"/workspaces/get",
async (req) => {
const workspace = await previewjs.getWorkspace({
versionCode,
absoluteFilePath: transformAbsoluteFilePath(req.absoluteFilePath),
});
if (!workspace) {
return {
workspaceId: null,
};
}
const existingWorkspaceId = Object.entries(workspaces)
.filter(([, value]) => value === workspace)
?.map(([key]) => key)[0];
const workspaceId =
existingWorkspaceId || crypto.randomBytes(16).toString("hex");
workspaces[workspaceId] = workspace;
return {
workspaceId,
rootDir: workspace.rootDir,
};
}
);

endpoint<DisposeWorkspaceRequest, DisposeWorkspaceResponse>(
"/workspaces/dispose",
async (req) => {
const workspaceId = req.workspaceId;
const workspace = workspaces[workspaceId];
if (!workspace) {
throw new NotFoundError();
}
await workspace.dispose();
delete workspaces[workspaceId];
return {};
}
);
const inWorkspace = <T>(
absoluteFilePath: string,
run: (workspace: Workspace | null) => Promise<T>
) =>
previewjs.inWorkspace({
versionCode,
absoluteFilePath: transformAbsoluteFilePath(absoluteFilePath),
run,
});

endpoint<CrawlFileRequest, CrawlFileResponse>(
"/crawl-file",
async ({ workspaceId, absoluteFilePath }) => {
const workspace = workspaces[workspaceId];
if (!workspace) {
throw new NotFoundError();
}
const { components, stories } = await workspace.crawlFiles([
path
.relative(
workspace.rootDir,
transformAbsoluteFilePath(absoluteFilePath)
)
.replace(/\\/g, "/"),
]);
return {
previewables: [...components, ...stories].map((c) => ({
id: c.id,
start: c.sourcePosition.start,
end: c.sourcePosition.end,
})),
};
}
async ({ absoluteFilePath }) =>
inWorkspace<CrawlFileResponse>(absoluteFilePath, async (workspace) => {
if (!workspace) {
return { rootDir: null, previewables: [] };
}
const { components, stories } = await workspace.crawlFiles([
path
.relative(
workspace.rootDir,
transformAbsoluteFilePath(absoluteFilePath)
)
.replace(/\\/g, "/"),
]);
return {
rootDir: workspace.rootDir,
previewables: [...components, ...stories].map((c) => ({
id: c.id,
start: c.sourcePosition.start,
end: c.sourcePosition.end,
})),
};
})
);

endpoint<StartPreviewRequest, StartPreviewResponse>(
"/previews/start",
async (req) => {
const workspace = workspaces[req.workspaceId];
if (!workspace) {
throw new NotFoundError();
}
const previewServer =
previewServers[req.workspaceId] ||
(await workspace.startServer({
onStop: () => {
delete previewServers[req.workspaceId];
},
}));
previewServers[req.workspaceId] = previewServer;
return {
url: `http://localhost:${previewServer.port}`,
};
}
async ({ rootDir }) =>
inWorkspace<StartPreviewResponse>(rootDir, async (workspace) => {
if (workspace?.rootDir !== rootDir) {
throw new NotFoundError();
}
let previewServer = previewServers[rootDir];
if (!previewServer) {
previewServer = previewServers[rootDir] = await workspace.startServer(
{
onStop: () => {
delete previewServers[rootDir];
},
}
);
}
return {
url: `http://localhost:${previewServer.port}`,
};
})
);

endpoint<CheckPreviewStatusRequest, CheckPreviewStatusResponse>(
"/previews/status",
async (req) => {
const workspace = workspaces[req.workspaceId];
if (!workspace) {
return {
running: false,
};
}
return {
running: Boolean(previewServers[req.workspaceId]),
running: Boolean(previewServers[req.rootDir]),
};
}
);

endpoint<StopPreviewRequest, StopPreviewResponse>(
"/previews/stop",
async (req) => {
const previewServer = previewServers[req.workspaceId];
if (!previewServer) {
throw new NotFoundError();
}
await previewServer.stop();
const previewServer = previewServers[req.rootDir];
await previewServer?.stop();
return {};
}
);
Expand Down
Loading

0 comments on commit abc1dcb

Please sign in to comment.