Skip to content

Commit 23a98f7

Browse files
committed
Use lower level invocation model
1 parent f6a67d8 commit 23a98f7

File tree

3 files changed

+47
-35
lines changed

3 files changed

+47
-35
lines changed

modal-js/src/function.ts

Lines changed: 38 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -5,23 +5,24 @@ import { createHash } from "node:crypto";
55
import {
66
DataFormat,
77
DeploymentNamespace,
8-
FunctionPutInputsItem,
8+
FunctionCallInvocationType,
9+
FunctionInput,
910
} from "../proto/modal_proto/api";
1011
import type { LookupOptions } from "./app";
1112
import { client } from "./client";
1213
import { FunctionCall } from "./function_call";
1314
import { environmentName } from "./config";
14-
import { NotFoundError } from "./errors";
15+
import { InternalFailure, NotFoundError } from "./errors";
1516
import { dumps } from "./pickle";
1617
import { ClientError, Status } from "nice-grpc";
17-
import {
18-
ControlPlaneStrategy,
19-
pollControlPlaneForOutput,
20-
} from "./invocation_strategy";
18+
import { ControlPlaneInvocation } from "./invocation";
2119

2220
// From: modal/_utils/blob_utils.py
2321
const maxObjectSizeBytes = 2 * 1024 * 1024; // 2 MiB
2422

23+
// From: client/modal/_functions.py
24+
const maxSystemRetries = 8;
25+
2526
/** Represents a deployed Modal Function, which can be invoked remotely. */
2627
export class Function_ {
2728
readonly functionId: string;
@@ -59,8 +60,25 @@ export class Function_ {
5960
kwargs: Record<string, any> = {},
6061
): Promise<any> {
6162
const input = await this.#createInput(args, kwargs);
62-
const invocationStrategy = new ControlPlaneStrategy(this.functionId, input);
63-
return await invocationStrategy.remote();
63+
const invocation = await ControlPlaneInvocation.create(
64+
this.functionId,
65+
input,
66+
FunctionCallInvocationType.FUNCTION_CALL_INVOCATION_TYPE_SYNC,
67+
);
68+
// TODO(ryan): Add tests for retries.
69+
let retryCount = 0;
70+
while (true) {
71+
try {
72+
return await invocation.await();
73+
} catch (err) {
74+
if (err instanceof InternalFailure && retryCount <= maxSystemRetries) {
75+
await invocation.retry(retryCount);
76+
retryCount++;
77+
} else {
78+
throw err;
79+
}
80+
}
81+
}
6482
}
6583

6684
// Spawn a single input into a remote function.
@@ -69,15 +87,18 @@ export class Function_ {
6987
kwargs: Record<string, any> = {},
7088
): Promise<FunctionCall> {
7189
const input = await this.#createInput(args, kwargs);
72-
const invocationStrategy = new ControlPlaneStrategy(this.functionId, input);
73-
const functionCallId = await invocationStrategy.spawn();
74-
return new FunctionCall(functionCallId);
90+
const invocation = await ControlPlaneInvocation.create(
91+
this.functionId,
92+
input,
93+
FunctionCallInvocationType.FUNCTION_CALL_INVOCATION_TYPE_ASYNC,
94+
);
95+
return new FunctionCall(invocation.functionCallId);
7596
}
7697

7798
async #createInput(
7899
args: any[] = [],
79100
kwargs: Record<string, any> = {},
80-
): Promise<FunctionPutInputsItem> {
101+
): Promise<FunctionInput> {
81102
const payload = dumps([args, kwargs]);
82103

83104
let argsBlobId: string | undefined = undefined;
@@ -87,25 +108,15 @@ export class Function_ {
87108

88109
// Single input sync invocation
89110
return {
90-
idx: 0,
91-
input: {
92-
args: argsBlobId ? undefined : payload,
93-
argsBlobId,
94-
dataFormat: DataFormat.DATA_FORMAT_PICKLE,
95-
methodName: this.methodName,
96-
finalInput: false, // This field isn't specified in the Python client, so it defaults to false.
97-
},
111+
args: argsBlobId ? undefined : payload,
112+
argsBlobId,
113+
dataFormat: DataFormat.DATA_FORMAT_PICKLE,
114+
methodName: this.methodName,
115+
finalInput: false, // This field isn't specified in the Python client, so it defaults to false.
98116
};
99117
}
100118
}
101119

102-
export async function pollFunctionOutput(
103-
functionCallId: string,
104-
timeout?: number, // in milliseconds
105-
): Promise<any> {
106-
return pollControlPlaneForOutput(functionCallId, timeout);
107-
}
108-
109120
async function blobUpload(data: Uint8Array): Promise<string> {
110121
const contentMd5 = createHash("md5").update(data).digest("base64");
111122
const contentSha256 = createHash("sha256").update(data).digest("base64");

modal-js/src/function_call.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// Manage existing Function Calls (look-ups, polling for output, cancellation).
22

33
import { client } from "./client";
4-
import { pollFunctionOutput } from "./function";
4+
import { ControlPlaneInvocation } from "./invocation";
55

66
/** Options for `FunctionCall.get()`. */
77
export type FunctionCallGetOptions = {
@@ -34,7 +34,10 @@ export class FunctionCall {
3434
/** Get the result of a function call, optionally waiting with a timeout. */
3535
async get(options: FunctionCallGetOptions = {}): Promise<any> {
3636
const timeout = options.timeout;
37-
return await pollFunctionOutput(this.functionCallId, timeout);
37+
const invocation = ControlPlaneInvocation.fromFunctionCallId(
38+
this.functionCallId,
39+
);
40+
return invocation.await(timeout);
3841
}
3942

4043
/** Cancel a running function call. */

modal-js/src/invocation.ts

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -30,18 +30,17 @@ export interface Invocation {
3030
*/
3131
await(): Promise<any>;
3232

33-
retry(): Promise<void>;
33+
retry(retryCount: number): Promise<void>;
3434
}
3535

3636
/**
37-
* Implementation of InvocationStrategy which sends inputs to the control plane.
37+
* Implementation of Invocation which sends inputs to the control plane.
3838
*/
3939
export class ControlPlaneInvocation implements Invocation {
4040
readonly functionCallId: string;
4141
private readonly input?: FunctionInput;
4242
private readonly functionCallJwt?: string;
4343
private inputJwt?: string;
44-
private retryCount: number = 0;
4544

4645
private constructor(
4746
functionCallId: string,
@@ -81,7 +80,7 @@ export class ControlPlaneInvocation implements Invocation {
8180
return await pollControlPlaneForOutput(this.functionCallId, timeout);
8281
}
8382

84-
async retry(): Promise<void> {
83+
async retry(retryCount: number): Promise<void> {
8584
// we do not expect this to happen
8685
if (!this.input) {
8786
throw new Error("Cannot retry function invocation - input missing");
@@ -90,15 +89,14 @@ export class ControlPlaneInvocation implements Invocation {
9089
const retryItem: FunctionRetryInputsItem = {
9190
inputJwt: this.inputJwt!,
9291
input: this.input,
93-
retryCount: this.retryCount,
92+
retryCount: retryCount,
9493
};
9594

9695
const functionRetryResponse = await client.functionRetryInputs({
9796
functionCallJwt: this.functionCallJwt,
9897
inputs: [retryItem],
9998
});
10099
this.inputJwt = functionRetryResponse.inputJwts[0];
101-
this.retryCount += 1;
102100
}
103101

104102
private static async execFunctionCall(

0 commit comments

Comments
 (0)