Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
- [Fixed] Report the GCFv2-to-GCFv1 downgrade error during validation instead of a misleading CPU error (#5461).
- Fixed `functions:delete` recreating an already-deleted Cloud Tasks queue for task queue functions. (#9305)
10 changes: 4 additions & 6 deletions src/deploy/functions/release/fabricator.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@
tasks.upsertQueue.rejects(new Error("unexpected tasks.upsertQueue"));
tasks.createQueue.rejects(new Error("unexpected tasks.createQueue"));
tasks.updateQueue.rejects(new Error("unexpected tasks.updateQueue"));
tasks.disableQueue.rejects(new Error("unexpected tasks.disableQueue"));
tasks.deleteQueue.rejects(new Error("unexpected tasks.deleteQueue"));
tasks.setEnqueuer.rejects(new Error("unexpected tasks.setEnqueuer"));
tasks.setIamPolicy.rejects(new Error("unexpected tasks.setIamPolicy"));
Expand Down Expand Up @@ -452,7 +453,7 @@
it("handles topics that already exist", async () => {
pubsub.createTopic.callsFake(() => {
const err = new Error("Already exists");
(err as any).status = 409;

Check warning on line 456 in src/deploy/functions/release/fabricator.spec.ts

View workflow job for this annotation

GitHub Actions / lint (24)

Unexpected any. Specify a different type

Check warning on line 456 in src/deploy/functions/release/fabricator.spec.ts

View workflow job for this annotation

GitHub Actions / lint (24)

Unsafe member access .status on an `any` value
return Promise.reject(err);
});
gcfv2.createFunction.resolves({ name: "op", done: false });
Expand Down Expand Up @@ -527,7 +528,7 @@
eventarc.createChannel.callsFake(({ name }) => {
expect(name).to.equal("channel");
const err = new Error("Already exists");
(err as any).status = 409;

Check warning on line 531 in src/deploy/functions/release/fabricator.spec.ts

View workflow job for this annotation

GitHub Actions / lint (24)

Unexpected any. Specify a different type

Check warning on line 531 in src/deploy/functions/release/fabricator.spec.ts

View workflow job for this annotation

GitHub Actions / lint (24)

Unsafe member access .status on an `any` value
return Promise.reject(err);
});
gcfv2.createFunction.resolves({ name: "op", done: false });
Expand Down Expand Up @@ -593,7 +594,7 @@
eventarc.getChannel.resolves(undefined);
eventarc.createChannel.callsFake(() => {
const err = new Error("🤷‍♂️");
(err as any).status = 400;

Check warning on line 597 in src/deploy/functions/release/fabricator.spec.ts

View workflow job for this annotation

GitHub Actions / lint (24)

Unexpected any. Specify a different type

Check warning on line 597 in src/deploy/functions/release/fabricator.spec.ts

View workflow job for this annotation

GitHub Actions / lint (24)

Unsafe member access .status on an `any` value
return Promise.reject(err);
});

Expand Down Expand Up @@ -1310,19 +1311,16 @@
const ep = endpoint({
taskQueueTrigger: {},
}) as backend.Endpoint & backend.TaskQueueTriggered;
tasks.updateQueue.resolves();
tasks.disableQueue.resolves();
await fab.disableTaskQueue(ep);
expect(tasks.updateQueue).to.have.been.calledWith({
name: tasks.queueNameForEndpoint(ep),
state: "DISABLED",
});
expect(tasks.disableQueue).to.have.been.calledWith(tasks.queueNameForEndpoint(ep));
});

it("wraps errors", async () => {
const ep = endpoint({
taskQueueTrigger: {},
}) as backend.Endpoint & backend.TaskQueueTriggered;
tasks.updateQueue.rejects(new Error("Not today"));
tasks.disableQueue.rejects(new Error("Not today"));
await expect(fab.disableTaskQueue(ep)).to.eventually.be.rejectedWith(
reporter.DeploymentError,
"disable task queue",
Expand Down Expand Up @@ -1907,7 +1905,7 @@
expect(deleteEndpoint).to.not.have.been.called;

// Resolve the create operation
resolveCreate!();

Check warning on line 1908 in src/deploy/functions/release/fabricator.spec.ts

View workflow job for this annotation

GitHub Actions / lint (24)

Forbidden non-null assertion

await applyPlanPromise;

Expand Down Expand Up @@ -1954,7 +1952,7 @@

describe("createRunFunction", () => {
it("creates a Cloud Run service with correct configuration", async () => {
runv2.createService.resolves({ uri: "https://service", name: "service" } as any);

Check warning on line 1955 in src/deploy/functions/release/fabricator.spec.ts

View workflow job for this annotation

GitHub Actions / lint (24)

Unexpected any. Specify a different type

Check warning on line 1955 in src/deploy/functions/release/fabricator.spec.ts

View workflow job for this annotation

GitHub Actions / lint (24)

Unsafe argument of type `any` assigned to a parameter of type `Service | undefined`
run.setInvokerCreate.resolves();

const ep = endpoint(
Expand Down Expand Up @@ -1995,7 +1993,7 @@
});

it("always sets callable triggers to public on creation", async () => {
runv2.createService.resolves({ uri: "https://service", name: "service" } as any);

Check warning on line 1996 in src/deploy/functions/release/fabricator.spec.ts

View workflow job for this annotation

GitHub Actions / lint (24)

Unsafe argument of type `any` assigned to a parameter of type `Service | undefined`
run.setInvokerCreate.resolves();

const ep = endpoint(
Expand Down
6 changes: 1 addition & 5 deletions src/deploy/functions/release/fabricator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1163,12 +1163,8 @@ export class Fabricator {
}

async disableTaskQueue(endpoint: backend.Endpoint & backend.TaskQueueTriggered): Promise<void> {
const update = {
name: cloudtasks.queueNameForEndpoint(endpoint),
state: "DISABLED" as cloudtasks.State,
};
await this.executor
.run(() => cloudtasks.updateQueue(update))
.run(() => cloudtasks.disableQueue(cloudtasks.queueNameForEndpoint(endpoint)))
.catch(rethrowAs(endpoint, "disable task queue"));
}

Expand Down
35 changes: 35 additions & 0 deletions src/gcp/cloudtasks.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import * as sinon from "sinon";

import * as iam from "./iam";
import * as backend from "../deploy/functions/backend";
import { FirebaseError } from "../error";
import * as cloudtasks from "./cloudtasks";
import * as proto from "./proto";

Expand All @@ -25,6 +26,7 @@ describe("CloudTasks", () => {
ct.triggerFromQueue.restore();
ct.setEnqueuer.restore();
ct.upsertQueue.restore();
ct.disableQueue.restore();
});

afterEach(() => {
Expand Down Expand Up @@ -179,6 +181,39 @@ describe("CloudTasks", () => {
});
});

describe("disableQueue", () => {
const NAME = "projects/p/locations/r/queues/f";

it("issues the update only when the queue exists", async () => {
ct.getQueue.resolves({ name: NAME, ...cloudtasks.DEFAULT_SETTINGS });
ct.updateQueue.resolves({ name: NAME });

await cloudtasks.disableQueue(NAME);

// queues.patch cannot actually change `state` (it is output only); we only
// assert that the long-standing PATCH is still issued for an existing queue.
expect(ct.getQueue).to.have.been.calledWith(NAME);
expect(ct.updateQueue).to.have.been.calledWith({ name: NAME, state: "DISABLED" });
});

it("is a no-op when the queue no longer exists", async () => {
ct.getQueue.rejects({ context: { response: { statusCode: 404 } } });

await cloudtasks.disableQueue(NAME);

expect(ct.getQueue).to.have.been.calledWith(NAME);
expect(ct.updateQueue).to.not.have.been.called;
});

it("rethrows non-404 errors without patching", async () => {
const err = new FirebaseError("boom", { context: { response: { statusCode: 500 } } });
ct.getQueue.rejects(err);

await expect(cloudtasks.disableQueue(NAME)).to.be.rejectedWith(err);
expect(ct.updateQueue).to.not.have.been.called;
});
});

describe("setEnqueuer", () => {
const NAME = "projects/p/locations/r/queues/f";
const ADMIN_BINDING: iam.Binding = {
Expand Down
26 changes: 26 additions & 0 deletions src/gcp/cloudtasks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,32 @@ export async function upsertQueue(queue: Queue): Promise<boolean> {
}
}

/**
* Best-effort update issued when a task queue triggered function is deleted.
* Skips the call when the queue no longer exists: updateQueue issues a PATCH,
* which creates the queue if it is absent, so patching an already-deleted queue
* would recreate it -- or fail with "existed too recently" when it was just
* deleted out of band (issue #9305).
*
* Note: queues.patch cannot change `state` (it is output only in the Cloud Tasks
* API; state changes go through pause/resume or queue.yaml), so this preserves
* the long-standing PATCH behavior while no longer resurrecting a queue the user
* already removed.
*/
export async function disableQueue(name: string): Promise<void> {
try {
// Here and throughout we use module.exports to ensure late binding & enable stubs in unit tests.
await (module.exports.getQueue as typeof getQueue)(name);
} catch (err) {
const responseError = err as { context?: { response?: { statusCode?: number } } };
if (responseError.context?.response?.statusCode === 404) {
return;
}
throw err;
}
await (module.exports.updateQueue as typeof updateQueue)({ name, state: "DISABLED" });
}

/** Purges all messages in a queue with a given name. */
export async function purgeQueue(name: string): Promise<void> {
await client.post(`${name}:purge`);
Expand Down
Loading