From 92e793d5b93984ba3cad6746c6097109ff95cac5 Mon Sep 17 00:00:00 2001 From: Brian Li Date: Tue, 13 Jun 2023 17:24:52 -0400 Subject: [PATCH 1/3] augment task context interface & pass in headers --- spec/common/providers/tasks.spec.ts | 49 ++++++++++++++++- spec/v1/providers/tasks.spec.ts | 5 ++ src/common/providers/tasks.ts | 85 ++++++++++++++++++++++++++--- 3 files changed, 129 insertions(+), 10 deletions(-) diff --git a/spec/common/providers/tasks.spec.ts b/spec/common/providers/tasks.spec.ts index 0e1058e7b..ce2497faf 100644 --- a/spec/common/providers/tasks.spec.ts +++ b/spec/common/providers/tasks.spec.ts @@ -75,9 +75,10 @@ describe("onEnqueueHandler", () => { function mockEnqueueRequest( data: unknown, contentType = "application/json", - context: { authorization?: string } = { authorization: "Bearer abc" } + context: { authorization?: string } = { authorization: "Bearer abc" }, + headers: Record = {} ): ReturnType { - return mockRequest(data, contentType, context); + return mockRequest(data, contentType, context, headers); } before(() => { @@ -194,6 +195,50 @@ describe("onEnqueueHandler", () => { }); }); + it("should populate context with values from header", () => { + const headers = { + "x-cloudtasks-queuename": "x", + "x-cloudtasks-taskname": "x", + "x-cloudtasks-taskretrycount": "1", + "x-cloudtasks-taskexecutioncount": "1", + "x-cloudtasks-tasketa": "timestamp", + "x-cloudtasks-taskpreviousresponse": "400", + "x-cloudtasks-taskretryreason": "something broke", + }; + const expectedContext = { + queueName: "x", + id: "x", + retryCount: 1, + executionCount: 1, + scheduledTime: "timestamp", + previousResponse: 400, + retryReason: "something broke", + }; + + const projectId = getApp().options.projectId; + const idToken = generateIdToken(projectId); + return runTaskTest({ + httpRequest: mockEnqueueRequest( + {}, + "application/json", + { authorization: "Bearer " + idToken }, + headers + ), + expectedData: {}, + taskFunction: (data, context) => { + checkAuthContext(context, projectId, mocks.user_id); + expect(context).to.include(expectedContext); + return null; + }, + taskFunction2: (request) => { + checkAuthContext(request, projectId, mocks.user_id); + expect(request).to.include(expectedContext); + return null; + }, + expectedStatus: 204, + }); + }); + it("should handle auth", async () => { const projectId = getApp().options.projectId; const idToken = generateIdToken(projectId); diff --git a/spec/v1/providers/tasks.spec.ts b/spec/v1/providers/tasks.spec.ts index 040e30388..eccdd3ab8 100644 --- a/spec/v1/providers/tasks.spec.ts +++ b/spec/v1/providers/tasks.spec.ts @@ -161,6 +161,11 @@ describe("#onDispatch", () => { uid: "abc", token: "token" as any, }, + queueName: "fn", + id: "task0", + retryCount: 0, + executionCount: 0, + scheduledTime: "timestamp", }; let done = false; const cf = taskQueue().onDispatch((d, c) => { diff --git a/src/common/providers/tasks.ts b/src/common/providers/tasks.ts index aaa8ff910..3a04c568e 100644 --- a/src/common/providers/tasks.ts +++ b/src/common/providers/tasks.ts @@ -88,22 +88,72 @@ export interface TaskContext { * The result of decoding and verifying an ODIC token. */ auth?: AuthData; + + /** + * The name of the queue. + * Populated via the X-CloudTasks-QueueName header. + */ + queueName: string; + + /** + * The "short" name of the task, or, if no name was specified at creation, a unique + * system-generated id. + * This is the my-task-id value in the complete task name, ie, task_name = + * projects/my-project-id/locations/my-location/queues/my-queue-id/tasks/my-task-id. + * Populated via the X-CloudTasks-TaskName header. + */ + id: string; + + /** + * The number of times this task has been retried. + * For the first attempt, this value is 0. This number includes attempts where the task failed + * due to 5XX error codes and never reached the execution phase. + * Populated via the X-CloudTasks-TaskRetryCount header. + */ + retryCount: number; + + /** + * The total number of times that the task has received a response from the handler. + * Since Cloud Tasks deletes the task once a successful response has been received, all + * previous handler responses were failures. This number does not include failures due to 5XX + * error codes. + * Populated via the X-CluodTasks-TaskExecutionCount header. + */ + executionCount: number; + + /** + * The schedule time of the task, as an RFC 3339 string in UTC time zone + * Populated via the X-CloudTasks-TaskETA header, which uses seconds since January 1 1970. + */ + scheduledTime: string; + + /** + * The HTTP response code from the previous retry. + * Populated via the X-CloudTasks-TaskPreviousResponse header + */ + previousResponse?: number; + + /** + * The reason for retrying the task. + * Populated via the X-CloudTasks-TaskRetryReason header. + */ + retryReason?: string; + + /** + * Raw request headers. + */ + headers?: Record; } /** * The request used to call a Task Queue function. */ -export interface Request { +export type Request = TaskContext & { /** * The parameters used by a client when calling this function. */ data: T; - - /** - * The result of decoding and verifying an ODIC token. - */ - auth?: AuthData; -} +}; type v1TaskHandler = (data: any, context: TaskContext) => void | Promise; type v2TaskHandler = (request: Request) => void | Promise; @@ -119,7 +169,26 @@ export function onDispatchHandler( throw new https.HttpsError("invalid-argument", "Bad Request"); } - const context: TaskContext = {}; + const headers: Record = {}; + for (const [key, value] of Object.entries(req.headers)) { + if (!Array.isArray(value)) { + headers[key] = value; + } + } + + const context: TaskContext = { + queueName: req.header("X-CloudTasks-QueueName"), + id: req.header("X-CloudTasks-TaskName"), + retryCount: Number(req.header("X-CloudTasks-TaskRetryCount")), + executionCount: Number(req.header("X-CloudTasks-TaskExecutionCount")), + scheduledTime: req.header("X-CloudTasks-TaskETA"), + previousResponse: req.header("X-CloudTasks-TaskPreviousResponse") + ? Number(req.header("X-CloudTasks-TaskPreviousResponse")) + : undefined, + retryReason: req.header("X-CloudTasks-TaskRetryReason"), + headers, + }; + if (!process.env.FUNCTIONS_EMULATOR) { const authHeader = req.header("Authorization") || ""; const token = authHeader.match(/^Bearer (.*)$/)?.[1]; From 136d159e3f4d07d6e92968afb86da5940191849d Mon Sep 17 00:00:00 2001 From: Brian Li Date: Tue, 13 Jun 2023 17:46:42 -0400 Subject: [PATCH 2/3] update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0733803be..cc82b0606 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1 +1,2 @@ +- Add features to task queue functions. (#1423) - Update list of supported regions for 2nd Gen Functions. (#1402) From 35e40fa8c614cf1a80ea11b62d99e589bab331c8 Mon Sep 17 00:00:00 2001 From: Brian Li Date: Mon, 26 Jun 2023 15:46:55 -0400 Subject: [PATCH 3/3] update docstrings & handle header edge cases --- src/common/providers/tasks.ts | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/src/common/providers/tasks.ts b/src/common/providers/tasks.ts index 3a04c568e..67ac1794a 100644 --- a/src/common/providers/tasks.ts +++ b/src/common/providers/tasks.ts @@ -91,7 +91,7 @@ export interface TaskContext { /** * The name of the queue. - * Populated via the X-CloudTasks-QueueName header. + * Populated via the `X-CloudTasks-QueueName` header. */ queueName: string; @@ -100,7 +100,7 @@ export interface TaskContext { * system-generated id. * This is the my-task-id value in the complete task name, ie, task_name = * projects/my-project-id/locations/my-location/queues/my-queue-id/tasks/my-task-id. - * Populated via the X-CloudTasks-TaskName header. + * Populated via the `X-CloudTasks-TaskName` header. */ id: string; @@ -108,7 +108,7 @@ export interface TaskContext { * The number of times this task has been retried. * For the first attempt, this value is 0. This number includes attempts where the task failed * due to 5XX error codes and never reached the execution phase. - * Populated via the X-CloudTasks-TaskRetryCount header. + * Populated via the `X-CloudTasks-TaskRetryCount` header. */ retryCount: number; @@ -117,25 +117,25 @@ export interface TaskContext { * Since Cloud Tasks deletes the task once a successful response has been received, all * previous handler responses were failures. This number does not include failures due to 5XX * error codes. - * Populated via the X-CluodTasks-TaskExecutionCount header. + * Populated via the `X-CloudTasks-TaskExecutionCount` header. */ executionCount: number; /** - * The schedule time of the task, as an RFC 3339 string in UTC time zone - * Populated via the X-CloudTasks-TaskETA header, which uses seconds since January 1 1970. + * The schedule time of the task, as an RFC 3339 string in UTC time zone. + * Populated via the `X-CloudTasks-TaskETA` header, which uses seconds since January 1 1970. */ scheduledTime: string; /** * The HTTP response code from the previous retry. - * Populated via the X-CloudTasks-TaskPreviousResponse header + * Populated via the `X-CloudTasks-TaskPreviousResponse` header */ previousResponse?: number; /** * The reason for retrying the task. - * Populated via the X-CloudTasks-TaskRetryReason header. + * Populated via the `X-CloudTasks-TaskRetryReason` header. */ retryReason?: string; @@ -146,7 +146,7 @@ export interface TaskContext { } /** - * The request used to call a Task Queue function. + * The request used to call a task queue function. */ export type Request = TaskContext & { /** @@ -179,8 +179,12 @@ export function onDispatchHandler( const context: TaskContext = { queueName: req.header("X-CloudTasks-QueueName"), id: req.header("X-CloudTasks-TaskName"), - retryCount: Number(req.header("X-CloudTasks-TaskRetryCount")), - executionCount: Number(req.header("X-CloudTasks-TaskExecutionCount")), + retryCount: req.header("X-CloudTasks-TaskRetryCount") + ? Number(req.header("X-CloudTasks-TaskRetryCount")) + : undefined, + executionCount: req.header("X-CloudTasks-TaskExecutionCount") + ? Number(req.header("X-CloudTasks-TaskExecutionCount")) + : undefined, scheduledTime: req.header("X-CloudTasks-TaskETA"), previousResponse: req.header("X-CloudTasks-TaskPreviousResponse") ? Number(req.header("X-CloudTasks-TaskPreviousResponse"))