@webpieces/cloudtasks-client 0.4.893 → 0.4.895
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/README.md +74 -13
- package/package.json +10 -5
- package/src/ClientCloudTasksFactory.d.ts +1 -1
- package/src/ClientCloudTasksFactory.js +1 -1
- package/src/ClientCloudTasksFactory.js.map +1 -1
- package/src/CloudTaskScheduler.d.ts +37 -13
- package/src/CloudTaskScheduler.js +38 -14
- package/src/CloudTaskScheduler.js.map +1 -1
- package/src/CloudTasksClientProvider.d.ts +29 -0
- package/src/CloudTasksClientProvider.js +41 -0
- package/src/CloudTasksClientProvider.js.map +1 -0
- package/src/GcpTaskInvoker.d.ts +27 -4
- package/src/GcpTaskInvoker.js +61 -19
- package/src/GcpTaskInvoker.js.map +1 -1
- package/src/GcpTaskRegistry.d.ts +31 -0
- package/src/GcpTaskRegistry.js +79 -0
- package/src/GcpTaskRegistry.js.map +1 -0
- package/src/GrpcErrorMapper.d.ts +27 -0
- package/src/GrpcErrorMapper.js +52 -0
- package/src/GrpcErrorMapper.js.map +1 -0
- package/src/InMemoryTaskInvoker.d.ts +9 -1
- package/src/InMemoryTaskInvoker.js +16 -3
- package/src/InMemoryTaskInvoker.js.map +1 -1
- package/src/TaskAlreadyExistsError.d.ts +18 -0
- package/src/TaskAlreadyExistsError.js +26 -0
- package/src/TaskAlreadyExistsError.js.map +1 -0
- package/src/TaskName.d.ts +33 -0
- package/src/TaskName.js +71 -0
- package/src/TaskName.js.map +1 -0
- package/src/TaskProxyClient.d.ts +32 -1
- package/src/TaskProxyClient.js +77 -27
- package/src/TaskProxyClient.js.map +1 -1
- package/src/TaskTypes.d.ts +110 -23
- package/src/TaskTypes.js +72 -22
- package/src/TaskTypes.js.map +1 -1
- package/src/__tests__/FakeCloudTasksServer.d.ts +51 -0
- package/src/__tests__/FakeCloudTasksServer.js +121 -0
- package/src/__tests__/FakeCloudTasksServer.js.map +1 -0
- package/src/index.d.ts +7 -1
- package/src/index.js +13 -1
- package/src/index.js.map +1 -1
package/src/GcpTaskInvoker.js
CHANGED
|
@@ -2,30 +2,48 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.GcpTaskInvoker = void 0;
|
|
4
4
|
const tslib_1 = require("tslib");
|
|
5
|
+
const core_util_1 = require("@webpieces/core-util");
|
|
5
6
|
const inversify_1 = require("inversify");
|
|
6
|
-
const tasks_1 = require("@google-cloud/tasks");
|
|
7
7
|
const core_context_1 = require("@webpieces/core-context");
|
|
8
8
|
const gcp_identity_1 = require("@webpieces/gcp-identity");
|
|
9
|
-
const
|
|
9
|
+
const core_util_2 = require("@webpieces/core-util");
|
|
10
10
|
const TaskTypes_1 = require("./TaskTypes");
|
|
11
|
-
const
|
|
11
|
+
const CloudTasksClientProvider_1 = require("./CloudTasksClientProvider");
|
|
12
|
+
const GrpcErrorMapper_1 = require("./GrpcErrorMapper");
|
|
13
|
+
const log = core_util_2.LogManager.getLogger('GcpTaskInvoker');
|
|
12
14
|
/**
|
|
13
15
|
* TaskInvoker that enqueues to Google Cloud Tasks (`@google-cloud/tasks`). Builds an
|
|
14
16
|
* HTTP-target task delivered as POST to `targetUrl + path`, authenticated per the
|
|
15
17
|
* endpoint's auth mode (OIDC token minted as this service's runtime SA, or a
|
|
16
|
-
* shared-secret header).
|
|
17
|
-
*
|
|
18
|
+
* shared-secret header).
|
|
19
|
+
*
|
|
20
|
+
* ONE attempt per call, never a retry of its own:
|
|
21
|
+
* - it builds a FRESH `ICreateTaskRequest` (gax mutates what it is handed), names the task with the
|
|
22
|
+
* id `TaskProxyClient` resolved once for the whole logical enqueue, then applies the effective
|
|
23
|
+
* {@link TaskCustomizer} last;
|
|
24
|
+
* - it sends `createTask(request, { timeout: timeoutMs })` — a real gRPC deadline per attempt, and no
|
|
25
|
+
* gax retry — unless the caller owns timing (`plan.gax` set), in which case it sends a shallow copy
|
|
26
|
+
* of the caller's `CallOptions`, untouched;
|
|
27
|
+
* - it maps the gRPC failure with {@link GrpcErrorMapper}: ALREADY_EXISTS becomes
|
|
28
|
+
* `TaskAlreadyExistsError` (success, upstream), UNAVAILABLE / DEADLINE_EXCEEDED /
|
|
29
|
+
* RESOURCE_EXHAUSTED become transient webpieces errors that `RetryStrategy` retries.
|
|
30
|
+
*
|
|
31
|
+
* Cancellation: the gRPC deadline IS the attempt deadline, so on Path A gRPC itself cancels the RPC
|
|
32
|
+
* at the same moment `CallDeadline` gives up on it. `createTask`'s public promise carries no
|
|
33
|
+
* `cancel()` (the client chains `.then` on gax's cancellable promise, which drops it), so the abort
|
|
34
|
+
* signal is checked before sending rather than wired to a cancel.
|
|
18
35
|
*/
|
|
19
36
|
// Default (overridable) impl of the TaskInvoker contract: binds `TaskInvoker -> GcpTaskInvoker`
|
|
20
37
|
// as a framework singleton. Tests/local dev override it via appOverrides:
|
|
21
38
|
// `(await options.rebind(TaskInvoker)).to(InMemoryTaskInvoker)` — same idiom as AuthConfig.
|
|
22
39
|
let GcpTaskInvoker = class GcpTaskInvoker extends TaskTypes_1.TaskInvoker {
|
|
23
40
|
secrets;
|
|
24
|
-
client
|
|
25
|
-
|
|
26
|
-
constructor(secrets) {
|
|
41
|
+
client;
|
|
42
|
+
grpcErrorMapper = new GrpcErrorMapper_1.GrpcErrorMapper();
|
|
43
|
+
constructor(clientProvider, secrets) {
|
|
27
44
|
super();
|
|
28
45
|
this.secrets = secrets;
|
|
46
|
+
this.client = clientProvider.create();
|
|
29
47
|
}
|
|
30
48
|
selectAuthentication(meta) {
|
|
31
49
|
const selected = meta.methods.find((method) => method.kind === 'oidc' || method.kind === 'public' ||
|
|
@@ -34,20 +52,42 @@ let GcpTaskInvoker = class GcpTaskInvoker extends TaskTypes_1.TaskInvoker {
|
|
|
34
52
|
throw new Error('No declared task credential has a configured delivery mechanism.');
|
|
35
53
|
return selected;
|
|
36
54
|
}
|
|
37
|
-
async enqueue(request) {
|
|
55
|
+
async enqueue(request, timeoutMs, signal) {
|
|
38
56
|
const projectId = await (0, gcp_identity_1.getProjectId)();
|
|
39
57
|
const region = await (0, gcp_identity_1.getRegion)();
|
|
40
58
|
const parent = this.client.queuePath(projectId, region, request.queueName);
|
|
41
|
-
const
|
|
42
|
-
const
|
|
43
|
-
const
|
|
44
|
-
const
|
|
59
|
+
const built = { parent: parent, task: await this.buildTask(request, parent) };
|
|
60
|
+
const customize = request.plan.customizeTask;
|
|
61
|
+
const createRequest = customize ? customize(built) : built;
|
|
62
|
+
const taskName = createRequest.task?.name ?? undefined;
|
|
63
|
+
core_util_1.CallDeadline.throwIfAborted(signal);
|
|
64
|
+
let created;
|
|
65
|
+
// webpieces-disable no-unmanaged-exceptions -- translate the gRPC failure into the webpieces taxonomy
|
|
66
|
+
// eslint-disable-next-line @webpieces/no-unmanaged-exceptions
|
|
67
|
+
try {
|
|
68
|
+
const result = await this.client.createTask(createRequest, this.callOptions(request, timeoutMs));
|
|
69
|
+
created = result[0];
|
|
70
|
+
}
|
|
71
|
+
catch (err) {
|
|
72
|
+
const error = (0, core_util_1.toError)(err);
|
|
73
|
+
throw this.grpcErrorMapper.map(error, taskName);
|
|
74
|
+
}
|
|
75
|
+
const name = created.name ?? taskName ?? 'unknown';
|
|
45
76
|
log.info(`enqueued cloud task ${name}`);
|
|
46
|
-
return new TaskTypes_1.JobReference(name);
|
|
77
|
+
return new TaskTypes_1.JobReference(name, false);
|
|
47
78
|
}
|
|
48
79
|
async delete(ref) {
|
|
49
80
|
await this.client.deleteTask({ name: ref.taskId });
|
|
50
81
|
}
|
|
82
|
+
/**
|
|
83
|
+
* A NEW object on every attempt. webpieces owns timing → `{ timeout }` only: a real gRPC deadline
|
|
84
|
+
* and no gax retry loop. The caller owns timing → a shallow copy of their options, so gax's own
|
|
85
|
+
* mutation (`otherArgs.headers`) never lands on an object the caller may reuse.
|
|
86
|
+
*/
|
|
87
|
+
callOptions(request, timeoutMs) {
|
|
88
|
+
const gax = request.plan.gax;
|
|
89
|
+
return gax === undefined ? { timeout: timeoutMs } : { ...gax };
|
|
90
|
+
}
|
|
51
91
|
async buildTask(request, parent) {
|
|
52
92
|
const headers = new Map(request.contextHeaders);
|
|
53
93
|
headers.set('content-type', 'application/json');
|
|
@@ -59,8 +99,8 @@ let GcpTaskInvoker = class GcpTaskInvoker extends TaskTypes_1.TaskInvoker {
|
|
|
59
99
|
};
|
|
60
100
|
await this.applyAuth(request, httpRequest, headers);
|
|
61
101
|
const task = { httpRequest: httpRequest };
|
|
62
|
-
if (request.
|
|
63
|
-
task.name = `${parent}/tasks/${request.
|
|
102
|
+
if (request.plan.taskId !== undefined) {
|
|
103
|
+
task.name = `${parent}/tasks/${request.plan.taskId}`;
|
|
64
104
|
}
|
|
65
105
|
if (request.scheduleInfo.epochMsToRunAt) {
|
|
66
106
|
task.scheduleTime = { seconds: Math.floor(request.scheduleInfo.epochMsToRunAt / 1000) };
|
|
@@ -92,8 +132,10 @@ let GcpTaskInvoker = class GcpTaskInvoker extends TaskTypes_1.TaskInvoker {
|
|
|
92
132
|
exports.GcpTaskInvoker = GcpTaskInvoker;
|
|
93
133
|
exports.GcpTaskInvoker = GcpTaskInvoker = tslib_1.__decorate([
|
|
94
134
|
(0, core_context_1.provideFrameworkSingletonDefaultForApi)(TaskTypes_1.TaskInvoker),
|
|
95
|
-
tslib_1.__param(0, (0, inversify_1.
|
|
96
|
-
tslib_1.__param(
|
|
97
|
-
tslib_1.
|
|
135
|
+
tslib_1.__param(0, (0, inversify_1.inject)(CloudTasksClientProvider_1.CloudTasksClientProvider)),
|
|
136
|
+
tslib_1.__param(1, (0, inversify_1.optional)()),
|
|
137
|
+
tslib_1.__param(1, (0, inversify_1.inject)(core_util_2.SECRETS)),
|
|
138
|
+
tslib_1.__metadata("design:paramtypes", [CloudTasksClientProvider_1.CloudTasksClientProvider,
|
|
139
|
+
core_util_2.Secrets])
|
|
98
140
|
], GcpTaskInvoker);
|
|
99
141
|
//# sourceMappingURL=GcpTaskInvoker.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"GcpTaskInvoker.js","sourceRoot":"","sources":["../../../../../packages/cloud/cloudtasks-client/src/GcpTaskInvoker.ts"],"names":[],"mappings":"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","sourcesContent":["import { AuthMeta, AuthMode } from '@webpieces/core-util';\nimport { inject, optional } from 'inversify';\nimport { CloudTasksClient, protos } from '@google-cloud/tasks';\nimport { provideFrameworkSingletonDefaultForApi } from '@webpieces/core-context';\nimport { getProjectId, getRegion, getRuntimeServiceAccountEmail } from '@webpieces/gcp-identity';\nimport { LogManager, Secrets, SECRETS } from '@webpieces/core-util';\nimport { TaskInvoker, TaskRequest, JobReference } from './TaskTypes';\n\nconst log = LogManager.getLogger('GcpTaskInvoker');\n\ntype ITask = protos.google.cloud.tasks.v2.ITask;\n\n/**\n * TaskInvoker that enqueues to Google Cloud Tasks (`@google-cloud/tasks`). Builds an\n * HTTP-target task delivered as POST to `targetUrl + path`, authenticated per the\n * endpoint's auth mode (OIDC token minted as this service's runtime SA, or a\n * shared-secret header). The task name is derived from the dedup name for idempotency\n * (a duplicate enqueue is rejected ALREADY_EXISTS = idempotent success upstream).\n */\n// Default (overridable) impl of the TaskInvoker contract: binds `TaskInvoker -> GcpTaskInvoker`\n// as a framework singleton. Tests/local dev override it via appOverrides:\n// `(await options.rebind(TaskInvoker)).to(InMemoryTaskInvoker)` — same idiom as AuthConfig.\n@provideFrameworkSingletonDefaultForApi(TaskInvoker)\nexport class GcpTaskInvoker extends TaskInvoker {\n private readonly client = new CloudTasksClient();\n\n // @optional: only sharedSecret(...) task endpoints need it; the client sends its bound value.\n constructor(@optional() @inject(SECRETS) private readonly secrets?: Secrets) {\n super();\n }\n\n override selectAuthentication(meta: AuthMeta): AuthMode {\n const selected = meta.methods.find((method: AuthMode) => method.kind === 'oidc' || method.kind === 'public' ||\n (method.kind === 'shared-secret' && this.secrets?.get(method.secretKey) !== undefined));\n if (!selected) throw new Error('No declared task credential has a configured delivery mechanism.');\n return selected;\n }\n\n override async enqueue(request: TaskRequest): Promise<JobReference> {\n const projectId = await getProjectId();\n const region = await getRegion();\n const parent = this.client.queuePath(projectId, region, request.queueName);\n\n const task = await this.buildTask(request, parent);\n const result = await this.client.createTask({ parent: parent, task: task });\n const created = result[0];\n const name = created.name ?? request.scheduleInfo.dedupName ?? 'unknown';\n log.info(`enqueued cloud task ${name}`);\n return new JobReference(name);\n }\n\n override async delete(ref: JobReference): Promise<void> {\n await this.client.deleteTask({ name: ref.taskId });\n }\n\n private async buildTask(request: TaskRequest, parent: string): Promise<ITask> {\n const headers = new Map<string, string>(request.contextHeaders);\n headers.set('content-type', 'application/json');\n\n const httpRequest: protos.google.cloud.tasks.v2.IHttpRequest = {\n httpMethod: 'POST',\n url: request.targetUrl + request.path,\n headers: Object.fromEntries(headers),\n body: Buffer.from(JSON.stringify(request.body ?? {}), 'utf8').toString('base64'),\n };\n await this.applyAuth(request, httpRequest, headers);\n\n const task: ITask = { httpRequest: httpRequest };\n if (request.scheduleInfo.dedupName) {\n task.name = `${parent}/tasks/${request.scheduleInfo.dedupName}`;\n }\n if (request.scheduleInfo.epochMsToRunAt) {\n task.scheduleTime = { seconds: Math.floor(request.scheduleInfo.epochMsToRunAt / 1000) };\n }\n if (request.scheduleInfo.taskTimeoutSeconds) {\n task.dispatchDeadline = { seconds: request.scheduleInfo.taskTimeoutSeconds };\n }\n return task;\n }\n\n private async applyAuth(\n request: TaskRequest,\n httpRequest: protos.google.cloud.tasks.v2.IHttpRequest,\n headers: Map<string, string>,\n ): Promise<void> {\n const mode = request.authMode;\n if (mode.kind === 'oidc') {\n httpRequest.oidcToken = {\n serviceAccountEmail: await getRuntimeServiceAccountEmail(),\n audience: request.targetUrl,\n };\n return;\n }\n if (mode.kind === 'shared-secret') {\n const secret = this.secrets?.get(mode.secretKey);\n if (secret) {\n // One credential header; the 'Webpieces' scheme says a shared secret follows, not a token.\n headers.set('authorization', `Webpieces ${secret}`);\n httpRequest.headers = Object.fromEntries(headers);\n }\n }\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"GcpTaskInvoker.js","sourceRoot":"","sources":["../../../../../packages/cloud/cloudtasks-client/src/GcpTaskInvoker.ts"],"names":[],"mappings":"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","sourcesContent":["import { AuthMeta, AuthMode, CallDeadline, toError } from '@webpieces/core-util';\nimport { inject, optional } from 'inversify';\nimport { CloudTasksClient, protos } from '@google-cloud/tasks';\nimport type { CallOptions } from 'google-gax';\nimport { provideFrameworkSingletonDefaultForApi } from '@webpieces/core-context';\nimport { getProjectId, getRegion, getRuntimeServiceAccountEmail } from '@webpieces/gcp-identity';\nimport { LogManager, Secrets, SECRETS } from '@webpieces/core-util';\nimport { TaskInvoker, TaskRequest, JobReference, ICreateTaskRequest } from './TaskTypes';\nimport { CloudTasksClientProvider } from './CloudTasksClientProvider';\nimport { GrpcErrorMapper } from './GrpcErrorMapper';\n\nconst log = LogManager.getLogger('GcpTaskInvoker');\n\ntype ITask = protos.google.cloud.tasks.v2.ITask;\n\n/**\n * TaskInvoker that enqueues to Google Cloud Tasks (`@google-cloud/tasks`). Builds an\n * HTTP-target task delivered as POST to `targetUrl + path`, authenticated per the\n * endpoint's auth mode (OIDC token minted as this service's runtime SA, or a\n * shared-secret header).\n *\n * ONE attempt per call, never a retry of its own:\n * - it builds a FRESH `ICreateTaskRequest` (gax mutates what it is handed), names the task with the\n * id `TaskProxyClient` resolved once for the whole logical enqueue, then applies the effective\n * {@link TaskCustomizer} last;\n * - it sends `createTask(request, { timeout: timeoutMs })` — a real gRPC deadline per attempt, and no\n * gax retry — unless the caller owns timing (`plan.gax` set), in which case it sends a shallow copy\n * of the caller's `CallOptions`, untouched;\n * - it maps the gRPC failure with {@link GrpcErrorMapper}: ALREADY_EXISTS becomes\n * `TaskAlreadyExistsError` (success, upstream), UNAVAILABLE / DEADLINE_EXCEEDED /\n * RESOURCE_EXHAUSTED become transient webpieces errors that `RetryStrategy` retries.\n *\n * Cancellation: the gRPC deadline IS the attempt deadline, so on Path A gRPC itself cancels the RPC\n * at the same moment `CallDeadline` gives up on it. `createTask`'s public promise carries no\n * `cancel()` (the client chains `.then` on gax's cancellable promise, which drops it), so the abort\n * signal is checked before sending rather than wired to a cancel.\n */\n// Default (overridable) impl of the TaskInvoker contract: binds `TaskInvoker -> GcpTaskInvoker`\n// as a framework singleton. Tests/local dev override it via appOverrides:\n// `(await options.rebind(TaskInvoker)).to(InMemoryTaskInvoker)` — same idiom as AuthConfig.\n@provideFrameworkSingletonDefaultForApi(TaskInvoker)\nexport class GcpTaskInvoker extends TaskInvoker {\n private readonly client: CloudTasksClient;\n private readonly grpcErrorMapper = new GrpcErrorMapper();\n\n constructor(\n // webpieces-disable inject-annotation-not-needed-for-concrete-class -- DI-resolved param; the esbuild/vitest path elides type-only imports (no design:paramtypes), so the explicit token is required\n @inject(CloudTasksClientProvider) clientProvider: CloudTasksClientProvider,\n // @optional: only sharedSecret(...) task endpoints need it; the client sends its bound value.\n @optional() @inject(SECRETS) private readonly secrets?: Secrets,\n ) {\n super();\n this.client = clientProvider.create();\n }\n\n override selectAuthentication(meta: AuthMeta): AuthMode {\n const selected = meta.methods.find((method: AuthMode) => method.kind === 'oidc' || method.kind === 'public' ||\n (method.kind === 'shared-secret' && this.secrets?.get(method.secretKey) !== undefined));\n if (!selected) throw new Error('No declared task credential has a configured delivery mechanism.');\n return selected;\n }\n\n override async enqueue(\n request: TaskRequest,\n timeoutMs: number,\n signal: AbortSignal,\n ): Promise<JobReference> {\n const projectId = await getProjectId();\n const region = await getRegion();\n const parent = this.client.queuePath(projectId, region, request.queueName);\n\n const built: ICreateTaskRequest = { parent: parent, task: await this.buildTask(request, parent) };\n const customize = request.plan.customizeTask;\n const createRequest = customize ? customize(built) : built;\n const taskName = createRequest.task?.name ?? undefined;\n CallDeadline.throwIfAborted(signal);\n\n let created: protos.google.cloud.tasks.v2.ITask;\n // webpieces-disable no-unmanaged-exceptions -- translate the gRPC failure into the webpieces taxonomy\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n const result = await this.client.createTask(createRequest, this.callOptions(request, timeoutMs));\n created = result[0];\n } catch (err: unknown) {\n const error = toError(err);\n throw this.grpcErrorMapper.map(error, taskName);\n }\n const name = created.name ?? taskName ?? 'unknown';\n log.info(`enqueued cloud task ${name}`);\n return new JobReference(name, false);\n }\n\n override async delete(ref: JobReference): Promise<void> {\n await this.client.deleteTask({ name: ref.taskId });\n }\n\n /**\n * A NEW object on every attempt. webpieces owns timing → `{ timeout }` only: a real gRPC deadline\n * and no gax retry loop. The caller owns timing → a shallow copy of their options, so gax's own\n * mutation (`otherArgs.headers`) never lands on an object the caller may reuse.\n */\n private callOptions(request: TaskRequest, timeoutMs: number): CallOptions {\n const gax = request.plan.gax;\n return gax === undefined ? { timeout: timeoutMs } : { ...gax };\n }\n\n private async buildTask(request: TaskRequest, parent: string): Promise<ITask> {\n const headers = new Map<string, string>(request.contextHeaders);\n headers.set('content-type', 'application/json');\n\n const httpRequest: protos.google.cloud.tasks.v2.IHttpRequest = {\n httpMethod: 'POST',\n url: request.targetUrl + request.path,\n headers: Object.fromEntries(headers),\n body: Buffer.from(JSON.stringify(request.body ?? {}), 'utf8').toString('base64'),\n };\n await this.applyAuth(request, httpRequest, headers);\n\n const task: ITask = { httpRequest: httpRequest };\n if (request.plan.taskId !== undefined) {\n task.name = `${parent}/tasks/${request.plan.taskId}`;\n }\n if (request.scheduleInfo.epochMsToRunAt) {\n task.scheduleTime = { seconds: Math.floor(request.scheduleInfo.epochMsToRunAt / 1000) };\n }\n if (request.scheduleInfo.taskTimeoutSeconds) {\n task.dispatchDeadline = { seconds: request.scheduleInfo.taskTimeoutSeconds };\n }\n return task;\n }\n\n private async applyAuth(\n request: TaskRequest,\n httpRequest: protos.google.cloud.tasks.v2.IHttpRequest,\n headers: Map<string, string>,\n ): Promise<void> {\n const mode = request.authMode;\n if (mode.kind === 'oidc') {\n httpRequest.oidcToken = {\n serviceAccountEmail: await getRuntimeServiceAccountEmail(),\n audience: request.targetUrl,\n };\n return;\n }\n if (mode.kind === 'shared-secret') {\n const secret = this.secrets?.get(mode.secretKey);\n if (secret) {\n // One credential header; the 'Webpieces' scheme says a shared secret follows, not a token.\n headers.set('authorization', `Webpieces ${secret}`);\n httpRequest.headers = Object.fromEntries(headers);\n }\n }\n }\n}\n"]}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { CallOptions } from 'google-gax';
|
|
2
|
+
import type { CallScope } from '@webpieces/core-util';
|
|
3
|
+
import { TaskCustomizer } from './TaskTypes';
|
|
4
|
+
/**
|
|
5
|
+
* Process-wide Cloud Tasks enqueue defaults, set at startup and resolved method -> API -> ALL — the
|
|
6
|
+
* same {@link CallScope} ladder as `CallRegistry`. A call site's `ScheduleOptions` wins over both.
|
|
7
|
+
*
|
|
8
|
+
* - {@link GcpTaskRegistry.setCustomizer} — a {@link TaskCustomizer} applied to the Google
|
|
9
|
+
* `ICreateTaskRequest` of every matching enqueue whose call site set none. It changes the REQUEST
|
|
10
|
+
* only (headers, dispatch deadline, ...); webpieces still owns timing.
|
|
11
|
+
* - {@link GcpTaskRegistry.setGax} — gax `CallOptions` for matching enqueues. Setting them means
|
|
12
|
+
* YOU own timing for that scope: no webpieces retry strategy runs, there is exactly one attempt,
|
|
13
|
+
* `createTask(request, yourGax)` is sent as given, and `CallRegistry`'s timeout ladder (method ->
|
|
14
|
+
* API -> ALL -> 30 s) is a safety cap only. Raise the cap with `CallRegistry.setTimeout(...)` to
|
|
15
|
+
* give gax retries room.
|
|
16
|
+
*
|
|
17
|
+
* Read only by `TaskProxyClient` (the customizer is applied by `GcpTaskInvoker`; the in-memory
|
|
18
|
+
* invoker has no Google request and ignores both).
|
|
19
|
+
*/
|
|
20
|
+
export declare class GcpTaskRegistry {
|
|
21
|
+
private static all;
|
|
22
|
+
private static readonly apis;
|
|
23
|
+
static setCustomizer(customizer: TaskCustomizer | undefined, ...scope: CallScope): void;
|
|
24
|
+
static setGax(gax: CallOptions | undefined, ...scope: CallScope): void;
|
|
25
|
+
/** The registered customizer, method -> API -> ALL, or undefined. */
|
|
26
|
+
static customizerFor(api: Function, methodName: string): TaskCustomizer | undefined;
|
|
27
|
+
/** The registered gax CallOptions, method -> API -> ALL, or undefined. */
|
|
28
|
+
static gaxFor(api: Function, methodName: string): CallOptions | undefined;
|
|
29
|
+
static clear(): void;
|
|
30
|
+
private static policy;
|
|
31
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.GcpTaskRegistry = void 0;
|
|
4
|
+
class GcpTaskPolicy {
|
|
5
|
+
customizer;
|
|
6
|
+
gax;
|
|
7
|
+
}
|
|
8
|
+
class ApiGcpTaskPolicies {
|
|
9
|
+
policy = new GcpTaskPolicy();
|
|
10
|
+
methods = new Map();
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Process-wide Cloud Tasks enqueue defaults, set at startup and resolved method -> API -> ALL — the
|
|
14
|
+
* same {@link CallScope} ladder as `CallRegistry`. A call site's `ScheduleOptions` wins over both.
|
|
15
|
+
*
|
|
16
|
+
* - {@link GcpTaskRegistry.setCustomizer} — a {@link TaskCustomizer} applied to the Google
|
|
17
|
+
* `ICreateTaskRequest` of every matching enqueue whose call site set none. It changes the REQUEST
|
|
18
|
+
* only (headers, dispatch deadline, ...); webpieces still owns timing.
|
|
19
|
+
* - {@link GcpTaskRegistry.setGax} — gax `CallOptions` for matching enqueues. Setting them means
|
|
20
|
+
* YOU own timing for that scope: no webpieces retry strategy runs, there is exactly one attempt,
|
|
21
|
+
* `createTask(request, yourGax)` is sent as given, and `CallRegistry`'s timeout ladder (method ->
|
|
22
|
+
* API -> ALL -> 30 s) is a safety cap only. Raise the cap with `CallRegistry.setTimeout(...)` to
|
|
23
|
+
* give gax retries room.
|
|
24
|
+
*
|
|
25
|
+
* Read only by `TaskProxyClient` (the customizer is applied by `GcpTaskInvoker`; the in-memory
|
|
26
|
+
* invoker has no Google request and ignores both).
|
|
27
|
+
*/
|
|
28
|
+
class GcpTaskRegistry {
|
|
29
|
+
static all = new GcpTaskPolicy();
|
|
30
|
+
static apis = new Map();
|
|
31
|
+
// webpieces-disable no-function-outside-class -- process-global startup registry
|
|
32
|
+
static setCustomizer(customizer, ...scope) {
|
|
33
|
+
GcpTaskRegistry.policy(scope[0], scope[1]).customizer = customizer;
|
|
34
|
+
}
|
|
35
|
+
// webpieces-disable no-function-outside-class -- process-global startup registry
|
|
36
|
+
static setGax(gax, ...scope) {
|
|
37
|
+
GcpTaskRegistry.policy(scope[0], scope[1]).gax = gax;
|
|
38
|
+
}
|
|
39
|
+
/** The registered customizer, method -> API -> ALL, or undefined. */
|
|
40
|
+
// webpieces-disable no-function-outside-class -- process-global registry lookup
|
|
41
|
+
static customizerFor(api, methodName) {
|
|
42
|
+
const policies = GcpTaskRegistry.apis.get(api);
|
|
43
|
+
return (policies?.methods.get(methodName)?.customizer ??
|
|
44
|
+
policies?.policy.customizer ??
|
|
45
|
+
GcpTaskRegistry.all.customizer);
|
|
46
|
+
}
|
|
47
|
+
/** The registered gax CallOptions, method -> API -> ALL, or undefined. */
|
|
48
|
+
// webpieces-disable no-function-outside-class -- process-global registry lookup
|
|
49
|
+
static gaxFor(api, methodName) {
|
|
50
|
+
const policies = GcpTaskRegistry.apis.get(api);
|
|
51
|
+
return policies?.methods.get(methodName)?.gax ?? policies?.policy.gax ?? GcpTaskRegistry.all.gax;
|
|
52
|
+
}
|
|
53
|
+
// webpieces-disable no-function-outside-class -- process-global registry reset for tests
|
|
54
|
+
static clear() {
|
|
55
|
+
GcpTaskRegistry.all = new GcpTaskPolicy();
|
|
56
|
+
GcpTaskRegistry.apis.clear();
|
|
57
|
+
}
|
|
58
|
+
// webpieces-disable no-function-outside-class -- process-global registry storage
|
|
59
|
+
static policy(api, methodName) {
|
|
60
|
+
if (api === 'ALL') {
|
|
61
|
+
return GcpTaskRegistry.all;
|
|
62
|
+
}
|
|
63
|
+
let policies = GcpTaskRegistry.apis.get(api);
|
|
64
|
+
if (!policies) {
|
|
65
|
+
policies = new ApiGcpTaskPolicies();
|
|
66
|
+
GcpTaskRegistry.apis.set(api, policies);
|
|
67
|
+
}
|
|
68
|
+
if (methodName === undefined)
|
|
69
|
+
return policies.policy;
|
|
70
|
+
let policy = policies.methods.get(methodName);
|
|
71
|
+
if (!policy) {
|
|
72
|
+
policy = new GcpTaskPolicy();
|
|
73
|
+
policies.methods.set(methodName, policy);
|
|
74
|
+
}
|
|
75
|
+
return policy;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
exports.GcpTaskRegistry = GcpTaskRegistry;
|
|
79
|
+
//# sourceMappingURL=GcpTaskRegistry.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"GcpTaskRegistry.js","sourceRoot":"","sources":["../../../../../packages/cloud/cloudtasks-client/src/GcpTaskRegistry.ts"],"names":[],"mappings":";;;AAIA,MAAM,aAAa;IACf,UAAU,CAAkB;IAC5B,GAAG,CAAe;CACrB;AAED,MAAM,kBAAkB;IACX,MAAM,GAAG,IAAI,aAAa,EAAE,CAAC;IAC7B,OAAO,GAAG,IAAI,GAAG,EAAyB,CAAC;CACvD;AAED;;;;;;;;;;;;;;;GAeG;AACH,MAAa,eAAe;IAChB,MAAM,CAAC,GAAG,GAAG,IAAI,aAAa,EAAE,CAAC;IACjC,MAAM,CAAU,IAAI,GAAG,IAAI,GAAG,EAAgC,CAAC;IAEvE,iFAAiF;IACjF,MAAM,CAAC,aAAa,CAAC,UAAsC,EAAE,GAAG,KAAgB;QAC5E,eAAe,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,UAAU,GAAG,UAAU,CAAC;IACvE,CAAC;IAED,iFAAiF;IACjF,MAAM,CAAC,MAAM,CAAC,GAA4B,EAAE,GAAG,KAAgB;QAC3D,eAAe,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,GAAG,GAAG,CAAC;IACzD,CAAC;IAED,qEAAqE;IACrE,gFAAgF;IAChF,MAAM,CAAC,aAAa,CAAC,GAAa,EAAE,UAAkB;QAClD,MAAM,QAAQ,GAAG,eAAe,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QAC/C,OAAO,CACH,QAAQ,EAAE,OAAO,CAAC,GAAG,CAAC,UAAU,CAAC,EAAE,UAAU;YAC7C,QAAQ,EAAE,MAAM,CAAC,UAAU;YAC3B,eAAe,CAAC,GAAG,CAAC,UAAU,CACjC,CAAC;IACN,CAAC;IAED,0EAA0E;IAC1E,gFAAgF;IAChF,MAAM,CAAC,MAAM,CAAC,GAAa,EAAE,UAAkB;QAC3C,MAAM,QAAQ,GAAG,eAAe,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QAC/C,OAAO,QAAQ,EAAE,OAAO,CAAC,GAAG,CAAC,UAAU,CAAC,EAAE,GAAG,IAAI,QAAQ,EAAE,MAAM,CAAC,GAAG,IAAI,eAAe,CAAC,GAAG,CAAC,GAAG,CAAC;IACrG,CAAC;IAED,yFAAyF;IACzF,MAAM,CAAC,KAAK;QACR,eAAe,CAAC,GAAG,GAAG,IAAI,aAAa,EAAE,CAAC;QAC1C,eAAe,CAAC,IAAI,CAAC,KAAK,EAAE,CAAC;IACjC,CAAC;IAED,iFAAiF;IACzE,MAAM,CAAC,MAAM,CAAC,GAAqB,EAAE,UAAmB;QAC5D,IAAI,GAAG,KAAK,KAAK,EAAE,CAAC;YAChB,OAAO,eAAe,CAAC,GAAG,CAAC;QAC/B,CAAC;QACD,IAAI,QAAQ,GAAG,eAAe,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QAC7C,IAAI,CAAC,QAAQ,EAAE,CAAC;YACZ,QAAQ,GAAG,IAAI,kBAAkB,EAAE,CAAC;YACpC,eAAe,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,EAAE,QAAQ,CAAC,CAAC;QAC5C,CAAC;QACD,IAAI,UAAU,KAAK,SAAS;YAAE,OAAO,QAAQ,CAAC,MAAM,CAAC;QACrD,IAAI,MAAM,GAAG,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC;QAC9C,IAAI,CAAC,MAAM,EAAE,CAAC;YACV,MAAM,GAAG,IAAI,aAAa,EAAE,CAAC;YAC7B,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,UAAU,EAAE,MAAM,CAAC,CAAC;QAC7C,CAAC;QACD,OAAO,MAAM,CAAC;IAClB,CAAC;;AAvDL,0CAwDC","sourcesContent":["import type { CallOptions } from 'google-gax';\nimport type { CallScope } from '@webpieces/core-util';\nimport { TaskCustomizer } from './TaskTypes';\n\nclass GcpTaskPolicy {\n customizer?: TaskCustomizer;\n gax?: CallOptions;\n}\n\nclass ApiGcpTaskPolicies {\n readonly policy = new GcpTaskPolicy();\n readonly methods = new Map<string, GcpTaskPolicy>();\n}\n\n/**\n * Process-wide Cloud Tasks enqueue defaults, set at startup and resolved method -> API -> ALL — the\n * same {@link CallScope} ladder as `CallRegistry`. A call site's `ScheduleOptions` wins over both.\n *\n * - {@link GcpTaskRegistry.setCustomizer} — a {@link TaskCustomizer} applied to the Google\n * `ICreateTaskRequest` of every matching enqueue whose call site set none. It changes the REQUEST\n * only (headers, dispatch deadline, ...); webpieces still owns timing.\n * - {@link GcpTaskRegistry.setGax} — gax `CallOptions` for matching enqueues. Setting them means\n * YOU own timing for that scope: no webpieces retry strategy runs, there is exactly one attempt,\n * `createTask(request, yourGax)` is sent as given, and `CallRegistry`'s timeout ladder (method ->\n * API -> ALL -> 30 s) is a safety cap only. Raise the cap with `CallRegistry.setTimeout(...)` to\n * give gax retries room.\n *\n * Read only by `TaskProxyClient` (the customizer is applied by `GcpTaskInvoker`; the in-memory\n * invoker has no Google request and ignores both).\n */\nexport class GcpTaskRegistry {\n private static all = new GcpTaskPolicy();\n private static readonly apis = new Map<Function, ApiGcpTaskPolicies>();\n\n // webpieces-disable no-function-outside-class -- process-global startup registry\n static setCustomizer(customizer: TaskCustomizer | undefined, ...scope: CallScope): void {\n GcpTaskRegistry.policy(scope[0], scope[1]).customizer = customizer;\n }\n\n // webpieces-disable no-function-outside-class -- process-global startup registry\n static setGax(gax: CallOptions | undefined, ...scope: CallScope): void {\n GcpTaskRegistry.policy(scope[0], scope[1]).gax = gax;\n }\n\n /** The registered customizer, method -> API -> ALL, or undefined. */\n // webpieces-disable no-function-outside-class -- process-global registry lookup\n static customizerFor(api: Function, methodName: string): TaskCustomizer | undefined {\n const policies = GcpTaskRegistry.apis.get(api);\n return (\n policies?.methods.get(methodName)?.customizer ??\n policies?.policy.customizer ??\n GcpTaskRegistry.all.customizer\n );\n }\n\n /** The registered gax CallOptions, method -> API -> ALL, or undefined. */\n // webpieces-disable no-function-outside-class -- process-global registry lookup\n static gaxFor(api: Function, methodName: string): CallOptions | undefined {\n const policies = GcpTaskRegistry.apis.get(api);\n return policies?.methods.get(methodName)?.gax ?? policies?.policy.gax ?? GcpTaskRegistry.all.gax;\n }\n\n // webpieces-disable no-function-outside-class -- process-global registry reset for tests\n static clear(): void {\n GcpTaskRegistry.all = new GcpTaskPolicy();\n GcpTaskRegistry.apis.clear();\n }\n\n // webpieces-disable no-function-outside-class -- process-global registry storage\n private static policy(api: Function | 'ALL', methodName?: string): GcpTaskPolicy {\n if (api === 'ALL') {\n return GcpTaskRegistry.all;\n }\n let policies = GcpTaskRegistry.apis.get(api);\n if (!policies) {\n policies = new ApiGcpTaskPolicies();\n GcpTaskRegistry.apis.set(api, policies);\n }\n if (methodName === undefined) return policies.policy;\n let policy = policies.methods.get(methodName);\n if (!policy) {\n policy = new GcpTaskPolicy();\n policies.methods.set(methodName, policy);\n }\n return policy;\n }\n}\n"]}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Translates a gRPC failure from the Cloud Tasks API into the portable webpieces error taxonomy, so
|
|
3
|
+
* the ONE classifier (`ClientRegistry.classify`) — and therefore `RetryStrategy` and `LogApiCall` —
|
|
4
|
+
* judges an enqueue exactly like any other call:
|
|
5
|
+
*
|
|
6
|
+
* | gRPC code | becomes | outcome |
|
|
7
|
+
* |---|---|---|
|
|
8
|
+
* | 14 UNAVAILABLE (connection reset, channel down) | `ApiUnavailableError` | transient |
|
|
9
|
+
* | 4 DEADLINE_EXCEEDED | `ApiDependencyTimeoutError` | transient |
|
|
10
|
+
* | 8 RESOURCE_EXHAUSTED (quota, throttling) | `ApiDependencyBackoffError` | transient |
|
|
11
|
+
* | 6 ALREADY_EXISTS | `TaskAlreadyExistsError` | success, see that class |
|
|
12
|
+
* | anything else | `ApiDependencyError` | failure |
|
|
13
|
+
*
|
|
14
|
+
* The original gRPC error is kept as `cause`. An error with no numeric gRPC `code` (a local failure
|
|
15
|
+
* before any RPC was sent) is returned unchanged.
|
|
16
|
+
*/
|
|
17
|
+
export declare class GrpcErrorMapper {
|
|
18
|
+
static readonly DEADLINE_EXCEEDED = 4;
|
|
19
|
+
static readonly ALREADY_EXISTS = 6;
|
|
20
|
+
static readonly RESOURCE_EXHAUSTED = 8;
|
|
21
|
+
static readonly UNAVAILABLE = 14;
|
|
22
|
+
/**
|
|
23
|
+
* @param error - the already-normalized error `createTask` rejected with
|
|
24
|
+
* @param taskName - the full task name that was sent, reported by an ALREADY_EXISTS
|
|
25
|
+
*/
|
|
26
|
+
map(error: Error, taskName: string | undefined): Error;
|
|
27
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.GrpcErrorMapper = void 0;
|
|
4
|
+
const core_util_1 = require("@webpieces/core-util");
|
|
5
|
+
const TaskAlreadyExistsError_1 = require("./TaskAlreadyExistsError");
|
|
6
|
+
/**
|
|
7
|
+
* Translates a gRPC failure from the Cloud Tasks API into the portable webpieces error taxonomy, so
|
|
8
|
+
* the ONE classifier (`ClientRegistry.classify`) — and therefore `RetryStrategy` and `LogApiCall` —
|
|
9
|
+
* judges an enqueue exactly like any other call:
|
|
10
|
+
*
|
|
11
|
+
* | gRPC code | becomes | outcome |
|
|
12
|
+
* |---|---|---|
|
|
13
|
+
* | 14 UNAVAILABLE (connection reset, channel down) | `ApiUnavailableError` | transient |
|
|
14
|
+
* | 4 DEADLINE_EXCEEDED | `ApiDependencyTimeoutError` | transient |
|
|
15
|
+
* | 8 RESOURCE_EXHAUSTED (quota, throttling) | `ApiDependencyBackoffError` | transient |
|
|
16
|
+
* | 6 ALREADY_EXISTS | `TaskAlreadyExistsError` | success, see that class |
|
|
17
|
+
* | anything else | `ApiDependencyError` | failure |
|
|
18
|
+
*
|
|
19
|
+
* The original gRPC error is kept as `cause`. An error with no numeric gRPC `code` (a local failure
|
|
20
|
+
* before any RPC was sent) is returned unchanged.
|
|
21
|
+
*/
|
|
22
|
+
class GrpcErrorMapper {
|
|
23
|
+
static DEADLINE_EXCEEDED = 4;
|
|
24
|
+
static ALREADY_EXISTS = 6;
|
|
25
|
+
static RESOURCE_EXHAUSTED = 8;
|
|
26
|
+
static UNAVAILABLE = 14;
|
|
27
|
+
/**
|
|
28
|
+
* @param error - the already-normalized error `createTask` rejected with
|
|
29
|
+
* @param taskName - the full task name that was sent, reported by an ALREADY_EXISTS
|
|
30
|
+
*/
|
|
31
|
+
map(error, taskName) {
|
|
32
|
+
const code = Object.getOwnPropertyDescriptor(error, 'code')?.value;
|
|
33
|
+
if (typeof code !== 'number') {
|
|
34
|
+
return error;
|
|
35
|
+
}
|
|
36
|
+
const message = `Cloud Tasks createTask failed with gRPC ${code}: ${error.message}`;
|
|
37
|
+
switch (code) {
|
|
38
|
+
case GrpcErrorMapper.UNAVAILABLE:
|
|
39
|
+
return new core_util_1.ApiUnavailableError(message, error);
|
|
40
|
+
case GrpcErrorMapper.DEADLINE_EXCEEDED:
|
|
41
|
+
return new core_util_1.ApiDependencyTimeoutError(message, error);
|
|
42
|
+
case GrpcErrorMapper.RESOURCE_EXHAUSTED:
|
|
43
|
+
return new core_util_1.ApiDependencyBackoffError(message, 30, error);
|
|
44
|
+
case GrpcErrorMapper.ALREADY_EXISTS:
|
|
45
|
+
return new TaskAlreadyExistsError_1.TaskAlreadyExistsError(taskName ?? 'unknown', error);
|
|
46
|
+
default:
|
|
47
|
+
return new core_util_1.ApiDependencyError(message, error);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
exports.GrpcErrorMapper = GrpcErrorMapper;
|
|
52
|
+
//# sourceMappingURL=GrpcErrorMapper.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"GrpcErrorMapper.js","sourceRoot":"","sources":["../../../../../packages/cloud/cloudtasks-client/src/GrpcErrorMapper.ts"],"names":[],"mappings":";;;AAAA,oDAK8B;AAC9B,qEAAkE;AAElE;;;;;;;;;;;;;;;GAeG;AACH,MAAa,eAAe;IACxB,MAAM,CAAU,iBAAiB,GAAG,CAAC,CAAC;IACtC,MAAM,CAAU,cAAc,GAAG,CAAC,CAAC;IACnC,MAAM,CAAU,kBAAkB,GAAG,CAAC,CAAC;IACvC,MAAM,CAAU,WAAW,GAAG,EAAE,CAAC;IAEjC;;;OAGG;IACH,GAAG,CAAC,KAAY,EAAE,QAA4B;QAC1C,MAAM,IAAI,GAAG,MAAM,CAAC,wBAAwB,CAAC,KAAK,EAAE,MAAM,CAAC,EAAE,KAAK,CAAC;QACnE,IAAI,OAAO,IAAI,KAAK,QAAQ,EAAE,CAAC;YAC3B,OAAO,KAAK,CAAC;QACjB,CAAC;QACD,MAAM,OAAO,GAAG,2CAA2C,IAAI,KAAK,KAAK,CAAC,OAAO,EAAE,CAAC;QACpF,QAAQ,IAAI,EAAE,CAAC;YACX,KAAK,eAAe,CAAC,WAAW;gBAC5B,OAAO,IAAI,+BAAmB,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC;YACnD,KAAK,eAAe,CAAC,iBAAiB;gBAClC,OAAO,IAAI,qCAAyB,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC;YACzD,KAAK,eAAe,CAAC,kBAAkB;gBACnC,OAAO,IAAI,qCAAyB,CAAC,OAAO,EAAE,EAAE,EAAE,KAAK,CAAC,CAAC;YAC7D,KAAK,eAAe,CAAC,cAAc;gBAC/B,OAAO,IAAI,+CAAsB,CAAC,QAAQ,IAAI,SAAS,EAAE,KAAK,CAAC,CAAC;YACpE;gBACI,OAAO,IAAI,8BAAkB,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC;QACtD,CAAC;IACL,CAAC;;AA5BL,0CA6BC","sourcesContent":["import {\n ApiDependencyBackoffError,\n ApiDependencyError,\n ApiDependencyTimeoutError,\n ApiUnavailableError,\n} from '@webpieces/core-util';\nimport { TaskAlreadyExistsError } from './TaskAlreadyExistsError';\n\n/**\n * Translates a gRPC failure from the Cloud Tasks API into the portable webpieces error taxonomy, so\n * the ONE classifier (`ClientRegistry.classify`) — and therefore `RetryStrategy` and `LogApiCall` —\n * judges an enqueue exactly like any other call:\n *\n * | gRPC code | becomes | outcome |\n * |---|---|---|\n * | 14 UNAVAILABLE (connection reset, channel down) | `ApiUnavailableError` | transient |\n * | 4 DEADLINE_EXCEEDED | `ApiDependencyTimeoutError` | transient |\n * | 8 RESOURCE_EXHAUSTED (quota, throttling) | `ApiDependencyBackoffError` | transient |\n * | 6 ALREADY_EXISTS | `TaskAlreadyExistsError` | success, see that class |\n * | anything else | `ApiDependencyError` | failure |\n *\n * The original gRPC error is kept as `cause`. An error with no numeric gRPC `code` (a local failure\n * before any RPC was sent) is returned unchanged.\n */\nexport class GrpcErrorMapper {\n static readonly DEADLINE_EXCEEDED = 4;\n static readonly ALREADY_EXISTS = 6;\n static readonly RESOURCE_EXHAUSTED = 8;\n static readonly UNAVAILABLE = 14;\n\n /**\n * @param error - the already-normalized error `createTask` rejected with\n * @param taskName - the full task name that was sent, reported by an ALREADY_EXISTS\n */\n map(error: Error, taskName: string | undefined): Error {\n const code = Object.getOwnPropertyDescriptor(error, 'code')?.value;\n if (typeof code !== 'number') {\n return error;\n }\n const message = `Cloud Tasks createTask failed with gRPC ${code}: ${error.message}`;\n switch (code) {\n case GrpcErrorMapper.UNAVAILABLE:\n return new ApiUnavailableError(message, error);\n case GrpcErrorMapper.DEADLINE_EXCEEDED:\n return new ApiDependencyTimeoutError(message, error);\n case GrpcErrorMapper.RESOURCE_EXHAUSTED:\n return new ApiDependencyBackoffError(message, 30, error);\n case GrpcErrorMapper.ALREADY_EXISTS:\n return new TaskAlreadyExistsError(taskName ?? 'unknown', error);\n default:\n return new ApiDependencyError(message, error);\n }\n }\n}\n"]}
|
|
@@ -24,11 +24,19 @@ export declare class InMemoryTaskInvoker extends TaskInvoker {
|
|
|
24
24
|
private counter;
|
|
25
25
|
/** Scheduled (not-yet-delivered) jobs, so delete() can cancel them. */
|
|
26
26
|
private readonly pending;
|
|
27
|
+
/** Every task name this process enqueued: a reuse is ALREADY_EXISTS, like Cloud Tasks. */
|
|
28
|
+
private readonly usedNames;
|
|
27
29
|
/** Sharpens the delivery-failure LOG: "unreachable target" vs a bare "Failed to fetch". */
|
|
28
30
|
private readonly networkRejectClassifier;
|
|
29
31
|
constructor(gcpOidc: GcpOidc, secrets?: Secrets | undefined);
|
|
30
32
|
selectAuthentication(meta: AuthMeta): AuthMode;
|
|
31
|
-
|
|
33
|
+
/**
|
|
34
|
+
* ONE attempt, like the GCP invoker — `TaskProxyClient` still runs Path A (webpieces retries) or
|
|
35
|
+
* Path B (one capped attempt) above it, so local timing matches prod. The customizer and gax are
|
|
36
|
+
* IGNORED: there is no Google request here. A reused task name throws
|
|
37
|
+
* {@link TaskAlreadyExistsError}, which the proxy reports as success, exactly as ALREADY_EXISTS is.
|
|
38
|
+
*/
|
|
39
|
+
enqueue(request: TaskRequest, _timeoutMs: number, _signal: AbortSignal): Promise<JobReference>;
|
|
32
40
|
delete(ref: JobReference): Promise<void>;
|
|
33
41
|
/** ms until the scheduled run time (0 = as soon as possible / already due). */
|
|
34
42
|
private computeDelayMs;
|
|
@@ -7,6 +7,7 @@ const core_context_1 = require("@webpieces/core-context");
|
|
|
7
7
|
const gcp_identity_1 = require("@webpieces/gcp-identity");
|
|
8
8
|
const core_util_1 = require("@webpieces/core-util");
|
|
9
9
|
const TaskTypes_1 = require("./TaskTypes");
|
|
10
|
+
const TaskAlreadyExistsError_1 = require("./TaskAlreadyExistsError");
|
|
10
11
|
const log = core_util_1.LogManager.getLogger('InMemoryTaskInvoker');
|
|
11
12
|
/**
|
|
12
13
|
* TaskInvoker for tests + local dev — the in-memory-queue twin of GcpTaskInvoker
|
|
@@ -30,6 +31,8 @@ let InMemoryTaskInvoker = class InMemoryTaskInvoker extends TaskTypes_1.TaskInvo
|
|
|
30
31
|
counter = 0;
|
|
31
32
|
/** Scheduled (not-yet-delivered) jobs, so delete() can cancel them. */
|
|
32
33
|
pending = new Map();
|
|
34
|
+
/** Every task name this process enqueued: a reuse is ALREADY_EXISTS, like Cloud Tasks. */
|
|
35
|
+
usedNames = new Set();
|
|
33
36
|
/** Sharpens the delivery-failure LOG: "unreachable target" vs a bare "Failed to fetch". */
|
|
34
37
|
networkRejectClassifier = new core_util_1.NetworkRejectClassifier();
|
|
35
38
|
constructor(gcpOidc, secrets) {
|
|
@@ -44,9 +47,19 @@ let InMemoryTaskInvoker = class InMemoryTaskInvoker extends TaskTypes_1.TaskInvo
|
|
|
44
47
|
throw new Error('No declared task credential has a configured delivery mechanism.');
|
|
45
48
|
return selected;
|
|
46
49
|
}
|
|
47
|
-
|
|
50
|
+
/**
|
|
51
|
+
* ONE attempt, like the GCP invoker — `TaskProxyClient` still runs Path A (webpieces retries) or
|
|
52
|
+
* Path B (one capped attempt) above it, so local timing matches prod. The customizer and gax are
|
|
53
|
+
* IGNORED: there is no Google request here. A reused task name throws
|
|
54
|
+
* {@link TaskAlreadyExistsError}, which the proxy reports as success, exactly as ALREADY_EXISTS is.
|
|
55
|
+
*/
|
|
56
|
+
async enqueue(request, _timeoutMs, _signal) {
|
|
48
57
|
this.counter += 1;
|
|
49
|
-
const taskId = request.
|
|
58
|
+
const taskId = request.plan.taskId ?? `inmem-${request.queueName}-${this.counter}`;
|
|
59
|
+
if (this.usedNames.has(taskId)) {
|
|
60
|
+
throw new TaskAlreadyExistsError_1.TaskAlreadyExistsError(taskId);
|
|
61
|
+
}
|
|
62
|
+
this.usedNames.add(taskId);
|
|
50
63
|
const delayMs = this.computeDelayMs(request.scheduleInfo.epochMsToRunAt);
|
|
51
64
|
// Break from the caller: queue the HTTP delivery and return the reference NOW.
|
|
52
65
|
const timer = setTimeout(() => {
|
|
@@ -57,7 +70,7 @@ let InMemoryTaskInvoker = class InMemoryTaskInvoker extends TaskTypes_1.TaskInvo
|
|
|
57
70
|
timer.unref?.();
|
|
58
71
|
this.pending.set(taskId, timer);
|
|
59
72
|
log.debug(`queued local task ${taskId} -> ${request.targetUrl}${request.path} (delay ${delayMs}ms)`);
|
|
60
|
-
return new TaskTypes_1.JobReference(taskId);
|
|
73
|
+
return new TaskTypes_1.JobReference(taskId, false);
|
|
61
74
|
}
|
|
62
75
|
async delete(ref) {
|
|
63
76
|
const timer = this.pending.get(ref.taskId);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"InMemoryTaskInvoker.js","sourceRoot":"","sources":["../../../../../packages/cloud/cloudtasks-client/src/InMemoryTaskInvoker.ts"],"names":[],"mappings":"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","sourcesContent":["import { AuthMeta, AuthMode } from '@webpieces/core-util';\nimport { inject, optional } from 'inversify';\nimport { provideFrameworkSingleton } from '@webpieces/core-context';\nimport { GcpOidc } from '@webpieces/gcp-identity';\nimport {\n LogManager,\n toError,\n NetworkRejectClassifier,\n Secrets,\n SECRETS,\n} from '@webpieces/core-util';\nimport { TaskInvoker, TaskRequest, JobReference } from './TaskTypes';\n\nconst log = LogManager.getLogger('InMemoryTaskInvoker');\n\n/**\n * TaskInvoker for tests + local dev — the in-memory-queue twin of GcpTaskInvoker\n * (a TypeScript port of webpieces-java's LocalRemoteInvoker).\n *\n * Instead of enqueuing to Google Cloud Tasks, it drops the delivery onto an in-process\n * timer queue and returns IMMEDIATELY (breaking from the caller, exactly like handing a\n * task to Cloud Tasks). The queued job then delivers the task the SAME way real Cloud\n * Tasks does: a plain HTTP POST to `targetUrl + path` with the JSON body and the\n * synthesized delivery auth (OIDC bearer / shared-secret). It does NOT invoke the server\n * in-process — the target may be a different process entirely — so the request travels\n * over real HTTP (e.g. http://localhost:{port}/queue/path) and is served by the target's\n * real routing + filter chain, giving production parity without GCP.\n *\n * Bind it (via appOverrides) in place of GcpTaskInvoker:\n * bind(TaskInvoker).to(InMemoryTaskInvoker)\n */\n@provideFrameworkSingleton()\nexport class InMemoryTaskInvoker extends TaskInvoker {\n private counter = 0;\n /** Scheduled (not-yet-delivered) jobs, so delete() can cancel them. */\n private readonly pending = new Map<string, ReturnType<typeof setTimeout>>();\n /** Sharpens the delivery-failure LOG: \"unreachable target\" vs a bare \"Failed to fetch\". */\n private readonly networkRejectClassifier = new NetworkRejectClassifier();\n\n constructor(\n // webpieces-disable inject-annotation-not-needed-for-concrete-class -- DI-resolved param; the esbuild/vitest path elides type-only imports (no design:paramtypes), so the explicit token is required\n @inject(GcpOidc) private readonly gcpOidc: GcpOidc,\n // @optional: only sharedSecret(...) task endpoints need it; the client sends its bound value.\n // webpieces-disable inject-annotation-not-needed-for-concrete-class -- DI-resolved param; the esbuild/vitest path elides type-only imports (no design:paramtypes), so the explicit token is required\n @optional() @inject(SECRETS) private readonly secrets?: Secrets,\n ) {\n super();\n }\n\n override selectAuthentication(meta: AuthMeta): AuthMode {\n const selected = meta.methods.find((method: AuthMode) => method.kind === 'oidc' || method.kind === 'public' ||\n (method.kind === 'shared-secret' && this.secrets?.get(method.secretKey) !== undefined));\n if (!selected) throw new Error('No declared task credential has a configured delivery mechanism.');\n return selected;\n }\n\n override async enqueue(request: TaskRequest): Promise<JobReference> {\n this.counter += 1;\n const taskId =\n request.scheduleInfo.dedupName ?? `inmem-${request.queueName}-${this.counter}`;\n const delayMs = this.computeDelayMs(request.scheduleInfo.epochMsToRunAt);\n\n // Break from the caller: queue the HTTP delivery and return the reference NOW.\n const timer = setTimeout(() => {\n this.pending.delete(taskId);\n void this.deliver(request, taskId);\n }, delayMs);\n // A queued task must not keep the process alive on its own.\n timer.unref?.();\n this.pending.set(taskId, timer);\n\n log.debug(\n `queued local task ${taskId} -> ${request.targetUrl}${request.path} (delay ${delayMs}ms)`,\n );\n return new JobReference(taskId);\n }\n\n override async delete(ref: JobReference): Promise<void> {\n const timer = this.pending.get(ref.taskId);\n if (timer) {\n clearTimeout(timer);\n this.pending.delete(ref.taskId);\n }\n }\n\n /** ms until the scheduled run time (0 = as soon as possible / already due). */\n private computeDelayMs(epochMsToRunAt?: number): number {\n if (epochMsToRunAt === undefined) {\n return 0;\n }\n const delay = epochMsToRunAt - Date.now();\n return delay > 0 ? delay : 0;\n }\n\n /** Deliver the queued task over real HTTP, mirroring Cloud Tasks' HTTP callback. */\n private async deliver(request: TaskRequest, taskId: string): Promise<void> {\n const url = `${request.targetUrl}${request.path}`;\n const headers = await this.buildHeaders(request);\n log.info(`delivering local task ${taskId}: POST ${url}`);\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- detached queue job: log delivery failure, never crash the queue\n try {\n // webpieces-disable no-fetch -- in-memory queue delivers over real HTTP like Cloud Tasks (no server to invoke in-process)\n const response = await fetch(url, {\n method: 'POST',\n headers: headers,\n body: JSON.stringify(request.body ?? {}),\n });\n if (!response.ok) {\n log.error(\n `local task ${taskId} delivery to ${url} failed: HTTP ${response.status}`,\n );\n return;\n }\n log.debug(`local task ${taskId} delivered: HTTP ${response.status}`);\n } catch (err: unknown) {\n const error = toError(err);\n // Detached job — we never rethrow, so classify only to sharpen the LOG: an operator reads\n // \"the target was unreachable\" (ApiConnectionError) instead of a bare \"Failed to fetch\".\n const deliveryError = this.networkRejectClassifier.toNetworkError(error, url);\n log.error(`local task ${taskId} delivery to ${url} threw: ${deliveryError.message}`);\n }\n }\n\n /** content-type + propagated context headers + synthesized delivery credential. */\n private async buildHeaders(request: TaskRequest): Promise<Record<string, string>> {\n const headers: Record<string, string> = { 'content-type': 'application/json' };\n for (const entry of request.contextHeaders.entries()) {\n headers[entry[0]] = entry[1];\n }\n await this.attachAuth(request, headers);\n return headers;\n }\n\n /** Synthesize the delivery credential Cloud Tasks would attach, per the auth mode. */\n private async attachAuth(request: TaskRequest, headers: Record<string, string>): Promise<void> {\n const mode = request.authMode;\n if (mode.kind === 'oidc') {\n headers['authorization'] =\n `Bearer ${await this.gcpOidc.mintIdToken(request.targetUrl)}`;\n return;\n }\n if (mode.kind === 'shared-secret') {\n const secret = this.secrets?.get(mode.secretKey);\n if (secret) {\n // One credential header; the 'Webpieces' scheme says a shared secret follows, not a token.\n headers['authorization'] = `Webpieces ${secret}`;\n }\n }\n // public / jwt → no service credential is synthesized.\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"InMemoryTaskInvoker.js","sourceRoot":"","sources":["../../../../../packages/cloud/cloudtasks-client/src/InMemoryTaskInvoker.ts"],"names":[],"mappings":"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","sourcesContent":["import { AuthMeta, AuthMode } from '@webpieces/core-util';\nimport { inject, optional } from 'inversify';\nimport { provideFrameworkSingleton } from '@webpieces/core-context';\nimport { GcpOidc } from '@webpieces/gcp-identity';\nimport {\n LogManager,\n toError,\n NetworkRejectClassifier,\n Secrets,\n SECRETS,\n} from '@webpieces/core-util';\nimport { TaskInvoker, TaskRequest, JobReference } from './TaskTypes';\nimport { TaskAlreadyExistsError } from './TaskAlreadyExistsError';\n\nconst log = LogManager.getLogger('InMemoryTaskInvoker');\n\n/**\n * TaskInvoker for tests + local dev — the in-memory-queue twin of GcpTaskInvoker\n * (a TypeScript port of webpieces-java's LocalRemoteInvoker).\n *\n * Instead of enqueuing to Google Cloud Tasks, it drops the delivery onto an in-process\n * timer queue and returns IMMEDIATELY (breaking from the caller, exactly like handing a\n * task to Cloud Tasks). The queued job then delivers the task the SAME way real Cloud\n * Tasks does: a plain HTTP POST to `targetUrl + path` with the JSON body and the\n * synthesized delivery auth (OIDC bearer / shared-secret). It does NOT invoke the server\n * in-process — the target may be a different process entirely — so the request travels\n * over real HTTP (e.g. http://localhost:{port}/queue/path) and is served by the target's\n * real routing + filter chain, giving production parity without GCP.\n *\n * Bind it (via appOverrides) in place of GcpTaskInvoker:\n * bind(TaskInvoker).to(InMemoryTaskInvoker)\n */\n@provideFrameworkSingleton()\nexport class InMemoryTaskInvoker extends TaskInvoker {\n private counter = 0;\n /** Scheduled (not-yet-delivered) jobs, so delete() can cancel them. */\n private readonly pending = new Map<string, ReturnType<typeof setTimeout>>();\n /** Every task name this process enqueued: a reuse is ALREADY_EXISTS, like Cloud Tasks. */\n private readonly usedNames = new Set<string>();\n /** Sharpens the delivery-failure LOG: \"unreachable target\" vs a bare \"Failed to fetch\". */\n private readonly networkRejectClassifier = new NetworkRejectClassifier();\n\n constructor(\n // webpieces-disable inject-annotation-not-needed-for-concrete-class -- DI-resolved param; the esbuild/vitest path elides type-only imports (no design:paramtypes), so the explicit token is required\n @inject(GcpOidc) private readonly gcpOidc: GcpOidc,\n // @optional: only sharedSecret(...) task endpoints need it; the client sends its bound value.\n // webpieces-disable inject-annotation-not-needed-for-concrete-class -- DI-resolved param; the esbuild/vitest path elides type-only imports (no design:paramtypes), so the explicit token is required\n @optional() @inject(SECRETS) private readonly secrets?: Secrets,\n ) {\n super();\n }\n\n override selectAuthentication(meta: AuthMeta): AuthMode {\n const selected = meta.methods.find((method: AuthMode) => method.kind === 'oidc' || method.kind === 'public' ||\n (method.kind === 'shared-secret' && this.secrets?.get(method.secretKey) !== undefined));\n if (!selected) throw new Error('No declared task credential has a configured delivery mechanism.');\n return selected;\n }\n\n /**\n * ONE attempt, like the GCP invoker — `TaskProxyClient` still runs Path A (webpieces retries) or\n * Path B (one capped attempt) above it, so local timing matches prod. The customizer and gax are\n * IGNORED: there is no Google request here. A reused task name throws\n * {@link TaskAlreadyExistsError}, which the proxy reports as success, exactly as ALREADY_EXISTS is.\n */\n override async enqueue(\n request: TaskRequest,\n _timeoutMs: number,\n _signal: AbortSignal,\n ): Promise<JobReference> {\n this.counter += 1;\n const taskId = request.plan.taskId ?? `inmem-${request.queueName}-${this.counter}`;\n if (this.usedNames.has(taskId)) {\n throw new TaskAlreadyExistsError(taskId);\n }\n this.usedNames.add(taskId);\n const delayMs = this.computeDelayMs(request.scheduleInfo.epochMsToRunAt);\n\n // Break from the caller: queue the HTTP delivery and return the reference NOW.\n const timer = setTimeout(() => {\n this.pending.delete(taskId);\n void this.deliver(request, taskId);\n }, delayMs);\n // A queued task must not keep the process alive on its own.\n timer.unref?.();\n this.pending.set(taskId, timer);\n\n log.debug(\n `queued local task ${taskId} -> ${request.targetUrl}${request.path} (delay ${delayMs}ms)`,\n );\n return new JobReference(taskId, false);\n }\n\n override async delete(ref: JobReference): Promise<void> {\n const timer = this.pending.get(ref.taskId);\n if (timer) {\n clearTimeout(timer);\n this.pending.delete(ref.taskId);\n }\n }\n\n /** ms until the scheduled run time (0 = as soon as possible / already due). */\n private computeDelayMs(epochMsToRunAt?: number): number {\n if (epochMsToRunAt === undefined) {\n return 0;\n }\n const delay = epochMsToRunAt - Date.now();\n return delay > 0 ? delay : 0;\n }\n\n /** Deliver the queued task over real HTTP, mirroring Cloud Tasks' HTTP callback. */\n private async deliver(request: TaskRequest, taskId: string): Promise<void> {\n const url = `${request.targetUrl}${request.path}`;\n const headers = await this.buildHeaders(request);\n log.info(`delivering local task ${taskId}: POST ${url}`);\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- detached queue job: log delivery failure, never crash the queue\n try {\n // webpieces-disable no-fetch -- in-memory queue delivers over real HTTP like Cloud Tasks (no server to invoke in-process)\n const response = await fetch(url, {\n method: 'POST',\n headers: headers,\n body: JSON.stringify(request.body ?? {}),\n });\n if (!response.ok) {\n log.error(\n `local task ${taskId} delivery to ${url} failed: HTTP ${response.status}`,\n );\n return;\n }\n log.debug(`local task ${taskId} delivered: HTTP ${response.status}`);\n } catch (err: unknown) {\n const error = toError(err);\n // Detached job — we never rethrow, so classify only to sharpen the LOG: an operator reads\n // \"the target was unreachable\" (ApiConnectionError) instead of a bare \"Failed to fetch\".\n const deliveryError = this.networkRejectClassifier.toNetworkError(error, url);\n log.error(`local task ${taskId} delivery to ${url} threw: ${deliveryError.message}`);\n }\n }\n\n /** content-type + propagated context headers + synthesized delivery credential. */\n private async buildHeaders(request: TaskRequest): Promise<Record<string, string>> {\n const headers: Record<string, string> = { 'content-type': 'application/json' };\n for (const entry of request.contextHeaders.entries()) {\n headers[entry[0]] = entry[1];\n }\n await this.attachAuth(request, headers);\n return headers;\n }\n\n /** Synthesize the delivery credential Cloud Tasks would attach, per the auth mode. */\n private async attachAuth(request: TaskRequest, headers: Record<string, string>): Promise<void> {\n const mode = request.authMode;\n if (mode.kind === 'oidc') {\n headers['authorization'] =\n `Bearer ${await this.gcpOidc.mintIdToken(request.targetUrl)}`;\n return;\n }\n if (mode.kind === 'shared-secret') {\n const secret = this.secrets?.get(mode.secretKey);\n if (secret) {\n // One credential header; the 'Webpieces' scheme says a shared secret follows, not a token.\n headers['authorization'] = `Webpieces ${secret}`;\n }\n }\n // public / jwt → no service credential is synthesized.\n }\n}\n"]}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A task with this name already exists (Cloud Tasks ALREADY_EXISTS, gRPC code 6), or was used within
|
|
3
|
+
* the last hour or so. Thrown by any {@link TaskInvoker}: `GcpTaskInvoker` on gRPC 6,
|
|
4
|
+
* `InMemoryTaskInvoker` on a reused name.
|
|
5
|
+
*
|
|
6
|
+
* It is not a failure. Every attempt of one logical enqueue carries the same task name, so this is
|
|
7
|
+
* either a retry whose earlier attempt actually landed, or a deliberate {@link TaskName.dedup}
|
|
8
|
+
* collision — in both cases the task the caller asked for exists. `TaskProxyClient.enqueue` catches
|
|
9
|
+
* it ONCE, above the retry layer, logs `WARN [ENQUEUE-DEDUP]`, and returns
|
|
10
|
+
* `new JobReference(taskName, true)`. It never reaches application code.
|
|
11
|
+
*/
|
|
12
|
+
export declare class TaskAlreadyExistsError extends Error {
|
|
13
|
+
/** The full task resource name (`projects/.../tasks/<id>`), or the in-memory task id. */
|
|
14
|
+
readonly taskName: string;
|
|
15
|
+
constructor(
|
|
16
|
+
/** The full task resource name (`projects/.../tasks/<id>`), or the in-memory task id. */
|
|
17
|
+
taskName: string, cause?: Error);
|
|
18
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.TaskAlreadyExistsError = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* A task with this name already exists (Cloud Tasks ALREADY_EXISTS, gRPC code 6), or was used within
|
|
6
|
+
* the last hour or so. Thrown by any {@link TaskInvoker}: `GcpTaskInvoker` on gRPC 6,
|
|
7
|
+
* `InMemoryTaskInvoker` on a reused name.
|
|
8
|
+
*
|
|
9
|
+
* It is not a failure. Every attempt of one logical enqueue carries the same task name, so this is
|
|
10
|
+
* either a retry whose earlier attempt actually landed, or a deliberate {@link TaskName.dedup}
|
|
11
|
+
* collision — in both cases the task the caller asked for exists. `TaskProxyClient.enqueue` catches
|
|
12
|
+
* it ONCE, above the retry layer, logs `WARN [ENQUEUE-DEDUP]`, and returns
|
|
13
|
+
* `new JobReference(taskName, true)`. It never reaches application code.
|
|
14
|
+
*/
|
|
15
|
+
class TaskAlreadyExistsError extends Error {
|
|
16
|
+
taskName;
|
|
17
|
+
constructor(
|
|
18
|
+
/** The full task resource name (`projects/.../tasks/<id>`), or the in-memory task id. */
|
|
19
|
+
taskName, cause) {
|
|
20
|
+
super(`task ${taskName} already exists`, { cause });
|
|
21
|
+
this.taskName = taskName;
|
|
22
|
+
this.name = 'TaskAlreadyExistsError';
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
exports.TaskAlreadyExistsError = TaskAlreadyExistsError;
|
|
26
|
+
//# sourceMappingURL=TaskAlreadyExistsError.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"TaskAlreadyExistsError.js","sourceRoot":"","sources":["../../../../../packages/cloud/cloudtasks-client/src/TaskAlreadyExistsError.ts"],"names":[],"mappings":";;;AAAA;;;;;;;;;;GAUG;AACH,MAAa,sBAAuB,SAAQ,KAAK;IAGzB;IAFpB;IACI,yFAAyF;IACzE,QAAgB,EAChC,KAAa;QAEb,KAAK,CAAC,QAAQ,QAAQ,iBAAiB,EAAE,EAAE,KAAK,EAAE,CAAC,CAAC;QAHpC,aAAQ,GAAR,QAAQ,CAAQ;QAIhC,IAAI,CAAC,IAAI,GAAG,wBAAwB,CAAC;IACzC,CAAC;CACJ;AATD,wDASC","sourcesContent":["/**\n * A task with this name already exists (Cloud Tasks ALREADY_EXISTS, gRPC code 6), or was used within\n * the last hour or so. Thrown by any {@link TaskInvoker}: `GcpTaskInvoker` on gRPC 6,\n * `InMemoryTaskInvoker` on a reused name.\n *\n * It is not a failure. Every attempt of one logical enqueue carries the same task name, so this is\n * either a retry whose earlier attempt actually landed, or a deliberate {@link TaskName.dedup}\n * collision — in both cases the task the caller asked for exists. `TaskProxyClient.enqueue` catches\n * it ONCE, above the retry layer, logs `WARN [ENQUEUE-DEDUP]`, and returns\n * `new JobReference(taskName, true)`. It never reaches application code.\n */\nexport class TaskAlreadyExistsError extends Error {\n constructor(\n /** The full task resource name (`projects/.../tasks/<id>`), or the in-memory task id. */\n public readonly taskName: string,\n cause?: Error,\n ) {\n super(`task ${taskName} already exists`, { cause });\n this.name = 'TaskAlreadyExistsError';\n }\n}\n"]}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How an enqueue names its Cloud Task — the id after `.../queues/<queue>/tasks/`.
|
|
3
|
+
*
|
|
4
|
+
* The name is resolved ONCE per logical enqueue (in `TaskProxyClient.enqueue`, before any attempt),
|
|
5
|
+
* so every retry of that enqueue carries the SAME name. That is what makes the enqueue retry safe: an
|
|
6
|
+
* attempt whose response was lost but whose task landed makes the retry answer ALREADY_EXISTS, which
|
|
7
|
+
* is reported as success (`JobReference.alreadyExisted`), not as a second task.
|
|
8
|
+
*
|
|
9
|
+
* - {@link TaskName.generated} — the DEFAULT. One fresh UUID per logical enqueue, reused on every retry.
|
|
10
|
+
* - {@link TaskName.dedup} — the CALLER's key: true dedup across callers and processes (a Twilio
|
|
11
|
+
* MessageSid, `org + period`, ...). A used name stays blocked for about an hour after its task runs
|
|
12
|
+
* or is deleted.
|
|
13
|
+
* - {@link TaskName.serverAssigned} — no name: the fastest `createTask` (Google documents that named
|
|
14
|
+
* tasks add enqueue latency), at the price that a retry after a lost response may create a duplicate.
|
|
15
|
+
*/
|
|
16
|
+
export declare class TaskName {
|
|
17
|
+
private readonly mode;
|
|
18
|
+
private readonly id?;
|
|
19
|
+
/** Cloud Tasks task ids: letters, digits, hyphens and underscores, at most 500 characters. */
|
|
20
|
+
private static readonly VALID_ID;
|
|
21
|
+
private constructor();
|
|
22
|
+
/** The caller's own dedup key, used verbatim as the task id. */
|
|
23
|
+
static dedup(name: string): TaskName;
|
|
24
|
+
/** The default: a fresh UUID for this logical enqueue, shared by every retry of it. */
|
|
25
|
+
static generated(): TaskName;
|
|
26
|
+
/** No name: Cloud Tasks assigns one. Fastest; a retry after a lost response may duplicate. */
|
|
27
|
+
static serverAssigned(): TaskName;
|
|
28
|
+
/**
|
|
29
|
+
* The task id to send, or `undefined` for a server-assigned name. Call ONCE per logical
|
|
30
|
+
* enqueue: a generated name is a new UUID on every call.
|
|
31
|
+
*/
|
|
32
|
+
resolve(): string | undefined;
|
|
33
|
+
}
|