Skip to content

Commit db8aded

Browse files
fix: Add retries to all idempotent endpoints and added parallel file writing (#140)
1 parent 20a4e53 commit db8aded

File tree

3 files changed

+130
-128
lines changed

3 files changed

+130
-128
lines changed

src/Sandboxes.ts

Lines changed: 58 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import {
1313
getStartOptions,
1414
getStartResponse,
1515
handleResponse,
16-
withCustomTimeout,
16+
retryWithDelay,
1717
} from "./utils/api";
1818

1919
import {
@@ -33,22 +33,24 @@ export async function startVm(
3333
sandboxId: string,
3434
startOpts?: StartSandboxOpts
3535
): Promise<PitcherManagerResponse> {
36-
const startResult = await withCustomTimeout((signal) =>
37-
vmStart({
38-
client: apiClient,
39-
body: startOpts
40-
? {
41-
ipcountry: startOpts.ipcountry,
42-
tier: startOpts.vmTier?.name,
43-
hibernation_timeout_seconds: startOpts.hibernationTimeoutSeconds,
44-
automatic_wakeup_config: startOpts.automaticWakeupConfig,
45-
}
46-
: undefined,
47-
path: {
48-
id: sandboxId,
49-
},
50-
signal,
51-
})
36+
const startResult = await retryWithDelay(
37+
() =>
38+
vmStart({
39+
client: apiClient,
40+
body: startOpts
41+
? {
42+
ipcountry: startOpts.ipcountry,
43+
tier: startOpts.vmTier?.name,
44+
hibernation_timeout_seconds: startOpts.hibernationTimeoutSeconds,
45+
automatic_wakeup_config: startOpts.automaticWakeupConfig,
46+
}
47+
: undefined,
48+
path: {
49+
id: sandboxId,
50+
},
51+
}),
52+
3,
53+
200
5254
);
5355

5456
const response = handleResponse(
@@ -91,7 +93,6 @@ export class Sandboxes {
9193
description: opts?.description,
9294
tags: tagsWithSdk,
9395
path,
94-
start_options: getStartOptions(opts),
9596
},
9697
path: {
9798
id: templateId,
@@ -100,11 +101,13 @@ export class Sandboxes {
100101

101102
const sandbox = handleResponse(result, "Failed to create sandbox");
102103

103-
return new Sandbox(
104-
sandbox.id,
105-
this.apiClient,
106-
getStartResponse(sandbox.start_response)
104+
const startResponse = await retryWithDelay(
105+
() => startVm(this.apiClient, sandbox.id, getStartOptions(opts)),
106+
3,
107+
200
107108
);
109+
110+
return new Sandbox(sandbox.id, this.apiClient, startResponse);
108111
}
109112

110113
/**
@@ -125,14 +128,16 @@ export class Sandboxes {
125128
* Shuts down a sandbox. Files will be saved, and the sandbox will be stopped.
126129
*/
127130
async shutdown(sandboxId: string): Promise<void> {
128-
const response = await withCustomTimeout((signal) =>
129-
vmShutdown({
130-
client: this.apiClient,
131-
path: {
132-
id: sandboxId,
133-
},
134-
signal,
135-
})
131+
const response = await retryWithDelay(
132+
() =>
133+
vmShutdown({
134+
client: this.apiClient,
135+
path: {
136+
id: sandboxId,
137+
},
138+
}),
139+
3,
140+
200
136141
);
137142

138143
handleResponse(response, `Failed to shutdown sandbox ${sandboxId}`);
@@ -156,53 +161,39 @@ export class Sandboxes {
156161
* Will resolve once the sandbox is restarted with its setup running.
157162
*/
158163
public async restart(sandboxId: string, opts?: StartSandboxOpts) {
159-
let didRestart = false;
160-
161-
for (let attempt = 1; attempt <= 3; attempt++) {
162-
try {
163-
await this.shutdown(sandboxId);
164-
didRestart = true;
165-
break;
166-
} catch (e) {
167-
await sleep(500);
168-
}
164+
try {
165+
await this.shutdown(sandboxId);
166+
} catch (e) {
167+
throw new Error("Failed to shutdown VM, " + String(e));
169168
}
170169

171-
if (!didRestart) {
172-
throw new Error("Failed to shutdown VM after 3 attempts");
173-
}
174-
175-
let startResponse: PitcherManagerResponse | undefined;
176-
177-
for (let attempt = 1; attempt <= 3; attempt++) {
178-
try {
179-
startResponse = await startVm(this.apiClient, sandboxId, opts);
180-
break;
181-
} catch (e) {
182-
await sleep(500);
183-
}
184-
}
170+
try {
171+
const startResponse = await retryWithDelay(
172+
() => startVm(this.apiClient, sandboxId, opts),
173+
3,
174+
200
175+
);
185176

186-
if (!startResponse) {
187-
throw new Error("Failed to start VM after 3 attempts");
177+
return new Sandbox(sandboxId, this.apiClient, startResponse);
178+
} catch (e) {
179+
throw new Error("Failed to start VM, " + String(e));
188180
}
189-
190-
return new Sandbox(sandboxId, this.apiClient, startResponse);
191181
}
192-
193182
/**
194183
* Hibernates a sandbox. Files will be saved, and the sandbox will be put to sleep. Next time
195184
* you resume the sandbox it will continue from the last state it was in.
196185
*/
197186
async hibernate(sandboxId: string): Promise<void> {
198-
const response = await withCustomTimeout((signal) =>
199-
vmHibernate({
200-
client: this.apiClient,
201-
path: {
202-
id: sandboxId,
203-
},
204-
signal,
205-
})
187+
const response = await retryWithDelay(
188+
() =>
189+
vmHibernate({
190+
client: this.apiClient,
191+
path: {
192+
id: sandboxId,
193+
},
194+
}),
195+
3,
196+
200
206197
);
207198

208199
handleResponse(response, `Failed to hibernate sandbox ${sandboxId}`);

src/bin/commands/build.ts

Lines changed: 48 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,13 @@ import {
1616
createApiClient,
1717
getDefaultTemplateId,
1818
handleResponse,
19+
retryWithDelay,
1920
} from "../../utils/api";
2021
import { getInferredApiKey } from "../../utils/constants";
2122
import { hashDirectory as getFilePaths } from "../utils/files";
2223
import { startVm } from "../../Sandboxes";
2324
import { mkdir, writeFile } from "fs/promises";
25+
import { sleep } from "../../utils/sleep";
2426

2527
export type BuildCommandArgs = {
2628
directory: string;
@@ -196,49 +198,58 @@ export const buildCommand: yargs.CommandModule<
196198
try {
197199
spinner.start(updateSpinnerMessage(index, "Starting sandbox..."));
198200

199-
const startResponse = await withCustomError(
200-
startVm(apiClient, id),
201-
"Failed to start sandbox"
201+
const startResponse = await retryWithDelay(() =>
202+
withCustomError(
203+
startVm(apiClient, id),
204+
"Failed to start sandbox at all"
205+
)
202206
);
203207
let sandboxVM = new Sandbox(id, apiClient, startResponse);
204208

205-
let session = await sandboxVM.connect();
209+
let session = await retryWithDelay(() => sandboxVM.connect(), 3, 100);
206210

207211
spinner.start(
208212
updateSpinnerMessage(index, "Writing files to sandbox...")
209213
);
210214

211-
let i = 0;
212-
for (const filePath of filePaths) {
213-
i++;
214-
try {
215-
const fullPath = path.join(argv.directory, filePath);
216-
const content = await fs.readFile(fullPath);
217-
const dirname = path.dirname(filePath);
218-
await session.fs.mkdir(dirname, true);
219-
await session.fs.writeFile(filePath, content, {
220-
create: true,
221-
overwrite: true,
222-
});
223-
} catch (error) {
224-
throw new Error(
225-
`Failed to write "${filePath}" to sandbox: ${error}`
226-
);
227-
}
228-
}
215+
await Promise.all(
216+
filePaths.map((filePath) =>
217+
retryWithDelay(
218+
async () => {
219+
const fullPath = path.join(argv.directory, filePath);
220+
const content = await fs.readFile(fullPath);
221+
const dirname = path.dirname(filePath);
222+
await session.fs.mkdir(dirname, true);
223+
await session.fs.writeFile(filePath, content, {
224+
create: true,
225+
overwrite: true,
226+
});
227+
},
228+
3,
229+
200
230+
)
231+
)
232+
).catch((error) => {
233+
throw new Error(`Failed to write files to sandbox: ${error}`);
234+
});
229235

230236
spinner.start(updateSpinnerMessage(index, "Building sandbox..."));
231237

232238
sandboxVM = await withCustomError(
233239
sdk.sandboxes.restart(id, {
234240
vmTier: buildTier,
235241
}),
236-
"Failed to restart sandbox"
242+
"Failed to restart sandbox after building"
237243
);
238244

239-
session = await withCustomError(
240-
sandboxVM.connect(),
241-
"Failed to connect to sandbox"
245+
session = await retryWithDelay(
246+
() =>
247+
withCustomError(
248+
sandboxVM.connect(),
249+
"Failed to connect to sandbox after building"
250+
),
251+
3,
252+
100
242253
);
243254

244255
await waitForSetup(session, index);
@@ -250,12 +261,17 @@ export const buildCommand: yargs.CommandModule<
250261
sdk.sandboxes.restart(id, {
251262
vmTier: sandboxTier,
252263
}),
253-
"Failed to restart sandbox"
264+
"Failed to restart sandbox after optimizing initial state"
254265
);
255266

256-
session = await withCustomError(
257-
sandboxVM.connect(),
258-
"Failed to connect to sandbox"
267+
session = await retryWithDelay(
268+
() =>
269+
withCustomError(
270+
sandboxVM.connect(),
271+
"Failed to connect to sandbox after optimizing initial state"
272+
),
273+
3,
274+
100
259275
);
260276

261277
await waitForSetup(session, index);
@@ -289,7 +305,7 @@ export const buildCommand: yargs.CommandModule<
289305
break;
290306
}
291307

292-
await new Promise((resolve) => setTimeout(resolve, 1000));
308+
await sleep(1000);
293309
}
294310

295311
updatePortSpinner();
@@ -308,7 +324,7 @@ export const buildCommand: yargs.CommandModule<
308324
spinner.start(updateSpinnerMessage(index, "Creating snapshot..."));
309325
await withCustomError(
310326
sdk.sandboxes.hibernate(id),
311-
"Failed to hibernate"
327+
"Failed to hibernate after building and optimizing sandbox"
312328
);
313329
spinner.start(updateSpinnerMessage(index, "Snapshot created"));
314330

src/utils/api.ts

Lines changed: 24 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -98,35 +98,6 @@ export function getStartResponse(
9898
};
9999
}
100100

101-
/**
102-
* Our infra has 2 min timeout, so we use that as default
103-
*/
104-
export async function withCustomTimeout<T>(
105-
cb: (signal: AbortSignal) => Promise<T>,
106-
timeoutSeconds: number = 120
107-
) {
108-
const controller = new AbortController();
109-
const signal = controller.signal;
110-
const timeoutHandle = setTimeout(() => {
111-
controller.abort();
112-
}, timeoutSeconds * 1000);
113-
114-
try {
115-
// We have to await for the finally to run
116-
return await cb(signal);
117-
} catch (err) {
118-
if (err instanceof Error && err.name === "AbortError") {
119-
throw new Error(
120-
`Request took longer than ${timeoutSeconds}s, so we aborted.`
121-
);
122-
}
123-
124-
throw err;
125-
} finally {
126-
clearTimeout(timeoutHandle);
127-
}
128-
}
129-
130101
export function getDefaultTemplateTag(apiClient: Client): string {
131102
if (apiClient.getConfig().baseUrl?.includes("codesandbox.stream")) {
132103
return "7ngcrf";
@@ -145,6 +116,30 @@ export function getDefaultTemplateId(apiClient: Client): string {
145116
return "pcz35m";
146117
}
147118

119+
export async function retryWithDelay<T>(
120+
callback: () => Promise<T>,
121+
retries: number = 3,
122+
delay: number = 500
123+
): Promise<T> {
124+
let lastError: Error;
125+
126+
for (let attempt = 1; attempt <= retries; attempt++) {
127+
try {
128+
return await callback();
129+
} catch (error) {
130+
lastError = error as Error;
131+
132+
if (attempt === retries) {
133+
throw lastError;
134+
}
135+
136+
await new Promise((resolve) => setTimeout(resolve, delay));
137+
}
138+
}
139+
140+
throw lastError!;
141+
}
142+
148143
export function handleResponse<D, E>(
149144
result: Awaited<{ data?: { data?: D }; error?: E; response: Response }>,
150145
errorPrefix: string

0 commit comments

Comments
 (0)