@empyria/restate 0.4.7 → 0.4.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/Admin.js +41 -0
- package/lib/Cron.js +13 -0
- package/package.json +1 -1
- package/test/Admin.test.js +35 -0
- package/test/Cron.test.js +8 -0
package/lib/Admin.js
CHANGED
|
@@ -409,6 +409,44 @@ export async function pollInvocation({ restateURL, invocationId, intervalMs = 25
|
|
|
409
409
|
}
|
|
410
410
|
}
|
|
411
411
|
|
|
412
|
+
/**
|
|
413
|
+
* Looks once, without waiting, at the result of an invocation (the `/output` endpoint).
|
|
414
|
+
*
|
|
415
|
+
* - `running`: no result yet
|
|
416
|
+
* - `done`: it finished; `output` is its result
|
|
417
|
+
* - `failed`: it ended in an error; `code` and `message` come from Restate
|
|
418
|
+
* - `unknown`: Restate has no such invocation (never existed, or its result was kept long enough and is gone)
|
|
419
|
+
* @param {{restateURL: string, invocationId: string}} params
|
|
420
|
+
* @returns {Promise<{state: 'running'}|{state: 'done', output: unknown}|{state: 'failed', code: number, message: string}|{state: 'unknown'}>}
|
|
421
|
+
*/
|
|
422
|
+
export async function getInvocationOutput({ restateURL, invocationId }) {
|
|
423
|
+
const response = await fetch(`${restateURL}/restate/invocation/${invocationId}/output`)
|
|
424
|
+
if (response.status === 470) return { state: 'running' }
|
|
425
|
+
|
|
426
|
+
const text = await response.text()
|
|
427
|
+
let body
|
|
428
|
+
try {
|
|
429
|
+
body = text ? JSON.parse(text) : undefined
|
|
430
|
+
} catch {
|
|
431
|
+
body = undefined
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
if (response.ok) return { state: 'done', output: body }
|
|
435
|
+
// An error of the invocation itself carries `source: 'invocation'`; anything else is the ingress saying it
|
|
436
|
+
// does not know the invocation, or could not look.
|
|
437
|
+
if (body?.source === 'invocation') {
|
|
438
|
+
return {
|
|
439
|
+
state: 'failed',
|
|
440
|
+
code: Number(body.code ?? response.status),
|
|
441
|
+
message: String(body.message ?? response.statusText),
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
if (response.status === 400 || response.status === 404) return { state: 'unknown' }
|
|
445
|
+
throw new Error(
|
|
446
|
+
`Unexpected status ${response.status} reading invocation '${invocationId}': ${response.statusText}`,
|
|
447
|
+
)
|
|
448
|
+
}
|
|
449
|
+
|
|
412
450
|
/**
|
|
413
451
|
* Request headers for an ingress call, adding Restate's `idempotency-key` when given.
|
|
414
452
|
* @param {string} [idempotencyKey]
|
|
@@ -791,6 +829,7 @@ export const sendMessageAsynSafe = async ({ restateURL, callerParams, timeout })
|
|
|
791
829
|
* @property {() => (message: WorkflowMessage) => Promise<InvocationSubmission>} submitWorkflowDiscovery
|
|
792
830
|
* @property {(params: {invocationId: string}) => Promise<*>} waitForInvocation
|
|
793
831
|
* @property {(params: {invocationId: string, intervalMs?: number}) => Promise<*>} pollInvocation
|
|
832
|
+
* @property {(params: {invocationId: string}) => Promise<Awaited<ReturnType<typeof getInvocationOutput>>>} getInvocationOutput
|
|
794
833
|
* @property {(params: Omit<ServiceMessage, 'restateURL'>) => Promise<*>} sendMessage
|
|
795
834
|
* @property {(params: Omit<ServiceMessage, 'restateURL'>) => Promise<InvocationSubmission>} sendMessageAsync
|
|
796
835
|
* @property {() => (message: ServiceMessage) => Promise<*>} sendMessageWithDiscovery
|
|
@@ -833,6 +872,8 @@ export function createRestateAdmin({ restateAdminURL, restateURL }) {
|
|
|
833
872
|
pollInvocation: ({ invocationId, intervalMs }) =>
|
|
834
873
|
pollInvocation({ restateURL, invocationId, intervalMs }),
|
|
835
874
|
|
|
875
|
+
getInvocationOutput: ({ invocationId }) => getInvocationOutput({ restateURL, invocationId }),
|
|
876
|
+
|
|
836
877
|
sendMessage: ({ name, message, key, payload, idempotencyKey }) =>
|
|
837
878
|
sendMessage({ restateURL, name, message, key, payload, idempotencyKey }),
|
|
838
879
|
sendMessageAsync: ({ name, message, key, payload, idempotencyKey }) =>
|
package/lib/Cron.js
CHANGED
|
@@ -10,6 +10,18 @@ export const setJobStateName = (jobStateName) => {
|
|
|
10
10
|
return JOB_STATE_NAME
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
+
/**
|
|
14
|
+
* A retry policy that never gives up: it backs off to one attempt a minute and keeps trying. Restate's default
|
|
15
|
+
* stops after a few attempts and *pauses* the invocation, and a paused invocation of a virtual object holds the
|
|
16
|
+
* object's lock, so every later call queues behind it for ever. A schedule that was due while the services were
|
|
17
|
+
* down (a restart, a crash, a night off) must simply run when they are back, so jobs use this.
|
|
18
|
+
*/
|
|
19
|
+
export const RETRY_FOREVER = {
|
|
20
|
+
initialInterval: { seconds: 1 },
|
|
21
|
+
maxInterval: { minutes: 1 },
|
|
22
|
+
exponentiationFactor: 2,
|
|
23
|
+
}
|
|
24
|
+
|
|
13
25
|
export const cronJobInitiator = restate.service({
|
|
14
26
|
name: 'CronJobInitiator',
|
|
15
27
|
handlers: {
|
|
@@ -25,6 +37,7 @@ export const cronJobInitiator = restate.service({
|
|
|
25
37
|
|
|
26
38
|
export const cronJob = restate.object({
|
|
27
39
|
name: 'CronJob',
|
|
40
|
+
options: { retryPolicy: RETRY_FOREVER },
|
|
28
41
|
handlers: {
|
|
29
42
|
initiate: async (ctx, request) => {
|
|
30
43
|
if (await ctx.get(JOB_STATE_NAME)) {
|
package/package.json
CHANGED
package/test/Admin.test.js
CHANGED
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
submitWorkflowDiscovery,
|
|
15
15
|
waitForInvocation,
|
|
16
16
|
pollInvocation,
|
|
17
|
+
getInvocationOutput,
|
|
17
18
|
sendMessage,
|
|
18
19
|
sendMessageAsync,
|
|
19
20
|
sendMessageWithDiscovery,
|
|
@@ -314,6 +315,40 @@ describe('submitWorkflowDiscovery', () => {
|
|
|
314
315
|
})
|
|
315
316
|
})
|
|
316
317
|
|
|
318
|
+
describe('getInvocationOutput', () => {
|
|
319
|
+
const look = (response) => {
|
|
320
|
+
globalThis.fetch = mock(async () => response)
|
|
321
|
+
return getInvocationOutput({ restateURL: 'http://ingress', invocationId: 'inv-1' })
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
test('done: returns the output', async () => {
|
|
325
|
+
expect(await look(jsonResponse({ a: 1 }))).toEqual({ state: 'done', output: { a: 1 } })
|
|
326
|
+
})
|
|
327
|
+
|
|
328
|
+
test('running: 470 means no result yet', async () => {
|
|
329
|
+
expect(await look(jsonResponse(null, false, 470))).toEqual({ state: 'running' })
|
|
330
|
+
})
|
|
331
|
+
|
|
332
|
+
test('failed: an error of the invocation itself', async () => {
|
|
333
|
+
const body = { code: 404, message: 'no such skill', source: 'invocation' }
|
|
334
|
+
expect(await look(jsonResponse(body, false, 404))).toEqual({
|
|
335
|
+
state: 'failed',
|
|
336
|
+
code: 404,
|
|
337
|
+
message: 'no such skill',
|
|
338
|
+
})
|
|
339
|
+
})
|
|
340
|
+
|
|
341
|
+
test('unknown: the ingress does not know the invocation', async () => {
|
|
342
|
+
expect(await look(jsonResponse({ message: 'x', source: 'ingress' }, false, 400))).toEqual({
|
|
343
|
+
state: 'unknown',
|
|
344
|
+
})
|
|
345
|
+
})
|
|
346
|
+
|
|
347
|
+
test('anything else is an error', async () => {
|
|
348
|
+
await expect(look(jsonResponse(null, false, 503))).rejects.toThrow('503')
|
|
349
|
+
})
|
|
350
|
+
})
|
|
351
|
+
|
|
317
352
|
describe('waitForInvocation / pollInvocation', () => {
|
|
318
353
|
test('waitForInvocation returns the attach result', async () => {
|
|
319
354
|
globalThis.fetch = mock(async () => jsonResponse({ done: true }))
|
package/test/Cron.test.js
CHANGED
|
@@ -291,3 +291,11 @@ describe('ensureCronJob', () => {
|
|
|
291
291
|
).rejects.toThrow('500')
|
|
292
292
|
})
|
|
293
293
|
})
|
|
294
|
+
|
|
295
|
+
describe('retry policy', () => {
|
|
296
|
+
test('a job never gives up: no attempt limit, so it is never paused', async () => {
|
|
297
|
+
const { RETRY_FOREVER } = await import('../lib/Cron.js')
|
|
298
|
+
expect(RETRY_FOREVER.maxAttempts).toBeUndefined()
|
|
299
|
+
expect(cronJob.options?.retryPolicy ?? cronJob.retryPolicy).toBeDefined()
|
|
300
|
+
})
|
|
301
|
+
})
|