Skip to content

Commit 04dd951

Browse files
Some feedback about workflow api (#261)
* Remove the message feature * Use clearAll to dispose the workflow * start -> submit rename * Add check on set state if state is null * Add support for empty value in awakeables/durable promise resolution * Make DurablePromise extend CombineablePromise
1 parent ac43e6c commit 04dd951

9 files changed

Lines changed: 122 additions & 221 deletions

‎examples/workflow_example.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ const myworkflow = restate.workflow.workflow("acme.myworkflow", {
4242

4343
// to listen to signals, also use promises
4444
const signal = ctx.promise<string>("thesignal");
45-
const message = await signal.promise();
45+
const message = await signal;
4646

4747
const result = `${message} my dear ${params.name}`;
4848
ctx.console.log(">>>>>>>>>>> Finishing workflow with: " + result);
@@ -66,7 +66,7 @@ const myworkflow = restate.workflow.workflow("acme.myworkflow", {
6666
},
6767

6868
awaitName: async (ctx: restate.workflow.SharedWfContext): Promise<string> => {
69-
return ctx.promise<string>("name_promise").promise();
69+
return ctx.promise<string>("name_promise");
7070
},
7171
});
7272

‎src/clients/workflow_client.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@ export function connect(restateUri: string): RestateClient {
128128

129129
let result: restate.workflow.WorkflowStartResult;
130130
try {
131-
result = await makeCall(restateUri, path, "start", workflowId, params);
131+
result = await makeCall(restateUri, path, "submit", workflowId, params);
132132
} catch (err) {
133133
const error = ensureError(err);
134134
throw new Error("Cannot start workflow: " + error.message, {

‎src/context.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -186,7 +186,7 @@ export interface Context {
186186
* // The sleeping service should have sent the awakeableIdentifier string to this service.
187187
* ctx.resolveAwakeable(awakeableIdentifier, "hello");
188188
*/
189-
resolveAwakeable<T>(id: string, payload: T): void;
189+
resolveAwakeable<T>(id: string, payload?: T): void;
190190

191191
/**
192192
* Reject an awakeable of another service. When rejecting, the service waiting on this awakeable will be woken up with a terminal error with the provided reason.

‎src/context_impl.ts‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -491,10 +491,13 @@ export class ContextImpl implements KeyedContext, RestateGrpcChannel {
491491
};
492492
}
493493

494-
public resolveAwakeable<T>(id: string, payload: T): void {
494+
public resolveAwakeable<T>(id: string, payload?: T): void {
495+
// We coerce undefined to null as null can be stringified by JSON.stringify
496+
const payloadToWrite = payload === undefined ? null : payload;
497+
495498
this.checkState("resolveAwakeable");
496499
this.completeAwakeable(id, {
497-
value: Buffer.from(JSON.stringify(payload)),
500+
value: Buffer.from(JSON.stringify(payloadToWrite)),
498501
});
499502
}
500503

‎src/workflows/workflow.ts‎

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -99,13 +99,12 @@ export interface WorkflowServices<R, T, U> extends restate.ServiceBundle {
9999
// workflow-specific types (promises, contexts)
100100
// ----------------------------------------------------------------------------
101101

102-
export interface DurablePromise<T> {
103-
promise(): Promise<T>;
102+
export type DurablePromise<T> = restate.CombineablePromise<T> & {
104103
peek(): Promise<T | null>;
105104

106105
resolve(value?: T): void;
107106
fail(errorMsg: string): void;
108-
}
107+
};
109108

110109
/**
111110
* The context for the workflow's interaction methods, which are all methods
@@ -142,12 +141,6 @@ export enum WorkflowStartResult {
142141
ALREADY_FINISHED = "ALREADY_FINISHED",
143142
}
144143

145-
export type StatusMessage = {
146-
sequenceNum: number;
147-
message: string;
148-
timestamp: Date;
149-
};
150-
151144
// ----------------------------------------------------------------------------
152145
// types and signatures for typed clients
153146
// ----------------------------------------------------------------------------

‎src/workflows/workflow_state_service.ts‎

Lines changed: 12 additions & 113 deletions
Original file line numberDiff line numberDiff line change
@@ -10,31 +10,14 @@
1010
*/
1111

1212
import * as restate from "../public_api";
13-
import {
14-
LifecycleStatus,
15-
StatusMessage,
16-
WorkflowStartResult,
17-
} from "./workflow";
13+
import { LifecycleStatus, WorkflowStartResult } from "./workflow";
1814

1915
const LIFECYCLE_STATUS_STATE_NAME = "status";
2016
const RESULT_STATE_NAME = "result";
2117
const RESULT_LISTENERS_NAME = "result_listeners";
22-
const STATUS_MESSAGES_STATE_NAME = "messages";
23-
const STATUS_MESSAGE_LISTENERS = "message_listeners";
2418
const PROMISE_STATE_PREFIX = "prom_s_";
19+
const USER_STATE_PREFIX = "state_";
2520
const PROMISE_AWAKEABLE_PREFIX = "prom_l_";
26-
const ALL_NAMES_STATE_NAME = "all_state_names";
27-
28-
const RESERVED_STATE_NAMES = [
29-
LIFECYCLE_STATUS_STATE_NAME,
30-
RESULT_STATE_NAME,
31-
RESULT_LISTENERS_NAME,
32-
ALL_NAMES_STATE_NAME,
33-
];
34-
const RESERVED_STATE_PREFIXES = [
35-
PROMISE_STATE_PREFIX,
36-
PROMISE_AWAKEABLE_PREFIX,
37-
];
3821

3922
export type ValueOrError<T> = {
4023
value?: T;
@@ -168,7 +151,7 @@ export const workflowStateService = restate.keyedRouter({
168151
_workflowId: string,
169152
stateName: string
170153
): Promise<T | null> => {
171-
return ctx.get(stateName);
154+
return ctx.get(USER_STATE_PREFIX + stateName);
172155
},
173156

174157
setState: async <T>(
@@ -190,107 +173,36 @@ export const workflowStateService = restate.keyedRouter({
190173
return;
191174
}
192175

193-
const stateName = request.stateName;
194-
195-
// guard against overwriting built-in states
196-
for (const reservedStateName of RESERVED_STATE_NAMES) {
197-
if (stateName === reservedStateName) {
198-
throw new restate.TerminalError(
199-
"State name is reserved: " + reservedStateName
200-
);
201-
}
202-
}
203-
for (const reservedStatePrefix of RESERVED_STATE_PREFIXES) {
204-
if (stateName.startsWith(reservedStatePrefix)) {
205-
throw new restate.TerminalError(
206-
"State prefix is reserved: " + reservedStatePrefix
207-
);
208-
}
209-
}
176+
const stateName = USER_STATE_PREFIX + request.stateName;
210177

211178
ctx.set(stateName, request.value);
212-
await rememberNewStateName(ctx, stateName);
213179
},
214180

215181
clearState: async (
216182
ctx: restate.KeyedContext,
217183
_workflowId: string,
218184
stateName: string
219185
): Promise<void> => {
220-
ctx.clear(stateName);
186+
ctx.clear(USER_STATE_PREFIX + stateName);
221187
},
222188

223189
stateKeys: async (ctx: restate.KeyedContext): Promise<Array<string>> => {
224-
return (await ctx.get<string[]>(ALL_NAMES_STATE_NAME)) ?? [];
190+
return (await ctx.stateKeys()).filter((name) =>
191+
name.startsWith(USER_STATE_PREFIX)
192+
);
225193
},
226194

227195
clearAllState: async (ctx: restate.KeyedContext): Promise<void> => {
228-
const stateNames = (await ctx.get<string[]>(ALL_NAMES_STATE_NAME)) ?? [];
196+
const stateNames = (await ctx.stateKeys()).filter((name) =>
197+
name.startsWith(USER_STATE_PREFIX)
198+
);
229199
for (const stateName of stateNames) {
230200
ctx.clear(stateName);
231201
}
232202
},
233203

234-
publishMessage: async (
235-
ctx: restate.KeyedContext,
236-
_workflowId: string,
237-
msg: { message: string; timestamp: Date }
238-
): Promise<void> => {
239-
// append message
240-
const msgs =
241-
(await ctx.get<StatusMessage[]>(STATUS_MESSAGES_STATE_NAME)) ?? [];
242-
msgs.push({ sequenceNum: msgs.length, ...msg });
243-
ctx.set(STATUS_MESSAGES_STATE_NAME, msgs);
244-
245-
// wake up all listeners
246-
const listeners = (await ctx.get<string[]>(STATUS_MESSAGE_LISTENERS)) ?? [];
247-
for (const awkId of listeners) {
248-
ctx.resolveAwakeable(awkId, {});
249-
}
250-
ctx.clear(STATUS_MESSAGE_LISTENERS);
251-
},
252-
253-
getLatestMessage: async (
254-
ctx: restate.KeyedContext
255-
): Promise<StatusMessage | null> => {
256-
const msgs =
257-
(await ctx.get<StatusMessage[]>(STATUS_MESSAGES_STATE_NAME)) ?? [];
258-
if (msgs.length === 0) {
259-
return null;
260-
} else {
261-
return msgs[msgs.length - 1];
262-
}
263-
},
264-
265-
pollNextMessages: async (
266-
ctx: restate.KeyedContext,
267-
_workflowId: string,
268-
req: { from: number; awakId: string }
269-
): Promise<StatusMessage[] | null> => {
270-
const msgs =
271-
(await ctx.get<StatusMessage[]>(STATUS_MESSAGES_STATE_NAME)) ?? [];
272-
if (msgs.length > req.from) {
273-
return msgs.slice(req.from);
274-
}
275-
276-
// not yet available, register a listener to be woken up when more is available
277-
const listeners = (await ctx.get<string[]>(STATUS_MESSAGE_LISTENERS)) ?? [];
278-
listeners.push(req.awakId);
279-
ctx.set(STATUS_MESSAGE_LISTENERS, listeners);
280-
return null;
281-
},
282-
283204
dispose: async (ctx: restate.KeyedContext): Promise<void> => {
284-
const stateNames = (await ctx.get<string[]>(ALL_NAMES_STATE_NAME)) ?? [];
285-
for (const stateName of stateNames) {
286-
ctx.clear(stateName);
287-
}
288-
ctx.clear(ALL_NAMES_STATE_NAME);
289-
ctx.clear(STATUS_MESSAGE_LISTENERS);
290-
ctx.clear(STATUS_MESSAGES_STATE_NAME);
291-
ctx.clear(RESULT_LISTENERS_NAME);
292-
ctx.clear(RESULT_STATE_NAME);
293-
ctx.clear(LIFECYCLE_STATUS_STATE_NAME);
205+
ctx.clearAll();
294206
},
295207
});
296208

@@ -325,7 +237,6 @@ async function completePromise<T>(
325237
// first completor
326238
// (a) set state
327239
ctx.set(stateName, completion);
328-
await rememberNewStateName(ctx, stateName);
329240

330241
// (b) complete awaiting awakeables
331242
const listeners = (await ctx.get<string[]>(awakeableStateName)) ?? [];
@@ -370,9 +281,6 @@ async function subscribePromise<T>(
370281
}
371282

372283
const listeners = (await ctx.get<string[]>(awakeableStateName)) ?? [];
373-
if (listeners.length === 0) {
374-
await rememberNewStateName(ctx, awakeableStateName);
375-
}
376284
listeners.push(awakeableId);
377285
ctx.set(awakeableStateName, listeners);
378286
return null;
@@ -385,15 +293,6 @@ async function peekPromise<T>(
385293
return ctx.get<ValueOrError<T>>(stateName);
386294
}
387295

388-
async function rememberNewStateName(
389-
ctx: restate.KeyedContext,
390-
stateName: string
391-
) {
392-
const names = (await ctx.get<string[]>(ALL_NAMES_STATE_NAME)) ?? [];
393-
names.push(stateName);
394-
ctx.set(ALL_NAMES_STATE_NAME, names);
395-
}
396-
397296
async function checkIfRunning(ctx: restate.KeyedContext): Promise<boolean> {
398297
const status = await ctx.get<LifecycleStatus>(LIFECYCLE_STATUS_STATE_NAME);
399298
return status === LifecycleStatus.RUNNING;

0 commit comments

Comments
 (0)