firebase / firebase-tools

The Firebase Command Line Tools
MIT License
4.02k stars 943 forks source link

Cloud Tasks is not supported by the Firebase Emulator Suite #4884

Closed NuclearGandhi closed 2 months ago

NuclearGandhi commented 2 years ago

Environment info

firebase-tools: 11.6.0

Platform: Windows 11

Test case

Tasken from this question on StackOverflow.

I'm using the Firebase Emulator to emulate Firebase's Firestore, Functions and Authentication. By following the documentation, Enqueue functions with Cloud Tasks, I created a task queue named removeGroupCode():

exports.removeGroupCode = functions.tasks
  .taskQueue({
    .
    .
    .
  })
  .onDispatch(async (data) => {
    .
    .
    .
    }
  });

This removeGroupCode() function works fine both in production and in the local emulator. But for some reason it just doesn't get called when I'm calling it from another function in the local emulator:

exports.generateInviteCode = functions.https.onCall(async (data, context) => {
  .
  .
  .
  const queue = getFunctions(app).taskQueue("removeGroupCode");
  await queue.enqueue(
    {groupCode: groupCode},
    {scheduleDelaySeconds: 30, dispatchDeadlineSeconds: 60},
  );
  return {groupCode: groupCode};
});

Note: The above code also works fine in production, but I still would like it to work in the emulated environment for testing purposes.

Steps to reproduce

  1. Setup and create a Firebase project environment for deploying Firebase Functions as shown in the documentation.
  2. Create a Cloud Task function as shown here.
  3. Create a Firebase function that calls the above Cloud Task function.
  4. Run, and call the function from step 3 inside a local emulator.

Expected behavior

The Cloud Task function to get called in the local emulator.

Actual behavior

Function not getting called - below are the logs for the emulated environment and production environment for when createGroupCode() gets called:

Emulated: image

Production (deployed):

image

taeold commented 2 years ago

@NuclearGandhi Thanks for writing up a detailed issue.

Sadly, Emulator doesn't support Cloud Tasks functions today - there isn't a "Google Cloud Task Emulator" that's hooked up to the Firebase Emulator Suite.

We also think it would be a great addition to the Emulator Suite, but we are focusing on other areas of improvement at the moment. I'm going to convert this issue into a feature request and will try to remember to post update here.

trex-quo commented 2 years ago

The way my team handled this was to create an internal mock for the CloudTaskClient that is used when our local flag is set. It just sends the request straight to the local emulated function instead of interacting with the Cloud Tasks client at all, which allows the function to be ran locally. It bypasses the queue functionality entirely, but that has been fine for us during local development since the rate of requests are so low and we usually just need the functions to run. Here is my implementation of the mock file if it is useful to you all.

const fetch = require("node-fetch");

class CloudTasksMock {
  constructor() {}

  queuePath(project, location, queue) {
    return `projects/${project}/locations/${location}/queues/${queue}`;
  }

  /**
   *
   * @param { import ("@google-cloud/tasks").protos.google.cloud.tasks.v2.ICreateTaskRequest } request
   */
  async createTask(request) {
    const { parent } = request;
    const httpRequest = request.task.httpRequest;

    console.log(
      `Received local queue request for parent ${parent}. Sending straight to function...`
    );

    if (httpRequest) {
      const { httpMethod, url, body } = httpRequest;

      const res = await fetch(url, {
        method: httpMethod,
        headers: {
          "content-type": "application/json",
        },
        //need to make sure to undo the base64 encoding before dispatching
        body: Buffer.from(body, "base64").toString("ascii"),
      });
      return [res];
    } else {
      throw new Error("cloudTasksMock - httpRequest is undefined");
    }
  }
}

module.exports = {
  CloudTasksMock,
};

Note that our Cloud Task processor functions are http.onRequest functions, and we have only been able to get these working properly with Cloud Task queues when the body is encoded in base64. Looking at my code it doesn't make much sense that I convert it back to ascii before sending to the local function and then perform another conversion in my local cloud function, but it works fine from what I can tell. Here is a small snippet of my Cloud Task processor function for context:

exports.cloudTaskProcessor = functions
  .runWith({
    memory: "512MB",
    timeoutSeconds: 540,
  })
  .https.onRequest(async (req, res) => {
    const reqBody = JSON.parse(
      Buffer.from(req.rawBody, "base64").toString("ascii")
    );
    ...
});
Bastianowicz commented 1 year ago

Thanks @trex-quo that inspired me but also led me in the wrong direction. On my local setup (for whatever reason) the port of the functions was already blocked by the request that should trigger the task in first place. This took me hours to figure out. But I have come up with a more direct solution for my case. This is a nest.js server but it will work likeways for any other environment.

I use the following as my functions factory in the dependency injection framework

                if (process.env.FUNCTIONS_EMULATOR) {
                    const logger = new Logger("Functions Factory");
                    logger.debug("Recognized Emulator environment. Stubbing Queues");
                    Object.assign(TaskQueue.prototype, {
                        enqueue: async function (data: never): Promise<void> {
                            return new Promise((resolve, reject) => {
                                const queue = main[this.functionName];
                                if (queue) {
                                    const httpBody = JSON.parse(JSON.stringify(data)); // important to experience comparable behaviour to prod env
                                    return resolve(queue.run(httpBody));
                                } else {
                                    logger.error("No such queue" + this.functionName);
                                    reject();
                                }
                            });
                        }
                    });
                }

                return getFunctions(adminApp);

It overrides (overwrites?) the original enqueue function. This new function checks if there is a function exportet with the name of the queue this task is being dispatched to. If so the data is being passed on.

This was a tough one. Hope it helps someone else. Maybe this could be a solution for @joehan as well. idk.

moifort commented 1 year ago

Like @Bastianowicz, this is my workaround. Put the code below just after your initializeApp(), it will display the message send to your queue.

Console

i  functions: Loaded environment variables from .env, .env.meeting-work-01, .env.local.
i  functions: Beginning execution of "meeting.task.sendCreatedAndFinishedEvents"
>  tasks:  { id: '4vu1i5g4cn4v93t2dvmfnt9ar6@google.com', status: 'started' } { scheduleTime: 2023-03-07T08:00:00.000Z }
>  tasks:  { id: '4vu1i5g4cn4v93t2dvmfnt9ar6@google.com', status: 'finished' } { scheduleTime: 2023-03-07T09:00:00.000Z }
i  functions: Finished "meeting.task.sendCreatedAndFinishedEvents" in 5.472542ms

Solution

import { initializeApp } from 'firebase-admin/app'
import { getFunctions, TaskQueue } from 'firebase-admin/functions'

const app = initializeApp()
if (process.env.FUNCTIONS_EMULATOR) {
    Object.assign(TaskQueue.prototype, {
      enqueue: (data: any, params: any) =>
        console.debug('tasks: ', data, params),
  })
}
const functions = getFunctions(app)
trylovetom commented 1 year ago

Any plan to support it? It's very useful.

christhompsongoogle commented 1 year ago

Hi @trylovetom, nothing to share at this time.

luicfrr commented 1 year ago

+1

emiliobasualdo commented 1 year ago

This was my solution in Typescript. I had to separate the task function from the actual task creation.

This is an example code for the emulator enqueuing the selectBiddingWinner task.

import {TaskQueue as FTaskQueue} from "firebase-admin/functions";

declare interface TaskQueue {
    functionName: string;
    client: unknown;
    extensionId?: string;
    enqueue(data: Record<string, string>, opts?: TaskOptions): Promise<void>
}

if (process.env.FUNCTIONS_EMULATOR) {
    FTaskQueue.prototype.enqueue = function (this: TaskQueue, data: Record<string, string>, opts?: TaskOptions) {
        logger.debug(this.functionName, data);
        return new Promise(() => {
            if (this.functionName == "locations/southamerica-east1/functions/selectBiddingWinnerTask") {
                return selectBiddingWinner(data.orderId);
            } else {
                return;
            }
        });
    };
}

This is an example of the task definition

exports.selectBiddingWinnerTask = tasks.taskQueue({
    retryConfig: {
        maxAttempts: 5,
        minBackoffSeconds: 3,
    },
    rateLimits: {
        maxConcurrentDispatches: 10,
    },
}).onDispatch(async (data: Record<string, string>) => {
    const orderId = data.orderId;
    return selectBiddingWinner(orderId)
        .then((resp) => logger.info(resp))
        .catch((e) => logger.error(`selectWinner failed for order:${orderId}`, e));
});

selectBiddingWinner is the actual function executed.

I'll be glad to receive comments/corrections that would help me generalize the code :)

daltyboy11 commented 1 year ago

@NuclearGandhi Thanks for writing up a detailed issue.

Sadly, Emulator doesn't support Cloud Tasks functions today - there isn't a "Google Cloud Task Emulator" that's hooked up to the Firebase Emulator Suite.

We also think it would be a great addition to the Emulator Suite, but we are focusing on other areas of improvement at the moment. I'm going to convert this issue into a feature request and will try to remember to post update here.

It's frankly ridiculous that this still isn't supported after almost a year, given that the official code example enqueues a task directly from a cloud function.

There's no way to test locally a cloud task being called from a cloud function, despite it being the main use case. Do I have that right?

Bastianowicz commented 1 year ago

I agree that this is disappointing but since you found this thread you may workaround using some of the solutions provided. I feel your disappointment here. But let's assume that the people that provided this useful toolset may just be pretty occupied and not simply mean.

eyal-mor commented 1 year ago

Thank you @daltyboy11 for the example!

Extended the example to also provide a nice Queue wrapper and resolver, with states.

I'm using this in an app that is running each task with a VERY rate limited API. As such, i'm sure there are tweaks that can be done to better fit other needs.

This of course isn't the best solution in mind, as the implementation runs during the HTTP dispatch and also processes the entire Queue, defeating the purpose of said Queue.

For development sake, I believe this is a start.

// Mock TaskQueue.enqueue implementation
// Extended from: https://github.com/firebase/firebase-tools/issues/4884#issuecomment-1485075479

import { TaskQueue as FTaskQueue, TaskOptions } from "firebase-admin/functions";

declare interface TaskQueue {
  functionName: string;
  client: unknown;
  extensionId?: string;
  enqueue(data: Record<string, string>, opts?: TaskOptions): Promise<void>
}

type QueueFunc = (args: Object | null) => Promise<void>;

if (process.env.FUNCTIONS_EMULATOR === 'true') {
  process.env.FIRESTORE_EMULATOR_HOST = 'localhost:8080';

  // Local Queue to run in the emulator
  // This queue is filled when the http call is invoked.
  let queue: {func: QueueFunc, args: Object | null, state: 'pending' | 'processing' | 'done'}[] = [];

  FTaskQueue.prototype.enqueue = function (this: TaskQueue, data: Record<string, string>, opts?: TaskOptions) {
    functions.logger.debug("enqueueing", this.functionName, data, "task count", queue.length);

    return new Promise(() => {
      if (this.functionName.endsWith('updateNearbySalesAverages')) {
        queue.push({
          func: updateNearbySalesAverages,
          args: { data, opts },
          state: 'pending',
        });
        return;
      } else {
        return;
      }
    });
  };

  // Process the queue every second
  setInterval(() => {
    functions.logger.debug("processing queue", queue.length);
    if (queue.length === 0) {
      return;
    };

    // Remove the latest done task.
    // This can be improved to get all done tasks.
    const taskDone = queue.findIndex(task => task.state === 'done');
    if (taskDone >= 0) {
      queue.splice(taskDone, 1);
    }

    // If a task is in processing, we can return early.
    // This may need more work to configure based on specific needs.
    const taskProcessing = queue.findIndex(task => task.state === 'processing');

    if (taskProcessing >= 0) {
      return;
    }

    // Get the first pending task
    const taskPending = queue.findIndex(task => task.state === 'pending');

    // If there are no pending tasks, return early
    if (taskPending === -1) {
      return;
    }

    // Get the function and args for the pending task
    const {func, args} = queue[taskPending];
    // Set the task to `processing` state.
    queue[taskPending].state = 'processing';

    // Implementation dependant, my code always uses an Async function.
    func(args).then(() => {
      // When the tasks is done, set the state to `done`
      queue[taskPending].state = 'done';
    }).catch((err) => {
      // If the task errors, set the state to `done`
      // Log the error
      functions.logger.error("error processing task", err);
      queue[taskPending].state = 'done';
    });
  }, 1000);
}
louismorgner commented 1 year ago

+1 for making this smoothly in the local emulator. Yes, there are workarounds which can get us there, but it is a friction point for working with this easily. Surely a useful addition which I hope will be shipped at some point by the team.

cadyfatcat commented 1 year ago

If nothing else, better explanation of the limitation of queueing tasks in the emulator environment should be included in the documentation. Would have saved a lot of time before finding this thread.

dzivoing commented 9 months ago

Any news on the state of the emulator ?

salehsed commented 8 months ago

+1

malcolmamal commented 8 months ago

I love the emulator but I agree that it would also be lovely to have the task queue there as well so a +1 from me :)

ssadel commented 6 months ago

bump

Maarten0110 commented 5 months ago

Would love to have this feature too!

johnneast commented 4 months ago

One of the things I've enjoyed the most about working with Firebase since adopting it is the developer experience of using the emulators for local developers. Having the ability to enqueue tasks natively in the emulator instead of having to use one of the work arounds shared above would be a great addition.

ChromeQ commented 2 months ago

I think this may now be supported in the latest firebase-tools https://github.com/firebase/firebase-tools/pull/7475 I've not had time to test it and if it works for the main use cases yet but sounds promising.

joehan commented 2 months ago

@ChromeQ is right - this was added in the most recent release thanks to the hard work of @GarrettBurroughs! Better documentation for it and examples will be coming in the next few weeks too. I'm gonna close this issue as fixed. If you run into any issues with the Cloud Task emulator or there is a use case that is not yet supported, please open a new issue.

tzappia commented 2 months ago

Ahead of documentation updates, here's some quick info about how I got things working:

  1. Update firebase.json
    "tasks": {
      "port": 9499
    },
  2. Following the instructions in the documentation for enqueuing functions with Cloud Tasks, add some code similar to:
    async function getFunctionUrl(name, location="us-central1") {
    if (process.env.FUNCTIONS_EMULATOR === 'true') { // or however you like to determine the emulator is running
    return `${YOUR_FUNCTIONS_ENDPOINT}/${name}`; // where your functions endpoint is something like http://127.0.0.1:5001/demo-project/us-central1
    }
    ... 

The task will run immediately and not respect scheduleTime or scheduleDelaySeconds should you have one set.

Edit: Also add this to .env.local

CLOUD_TASKS_EMULATOR_HOST=127.0.0.1:9499
ReinisSprogis commented 2 months ago

Hi. This is good timing. Thanks for feature implemented. I Have flutter project. Would be awesome to get emulator running for Cloud tasks.

Currently i initialize emulator in main like this.

 FirebaseDatabase.instance.useDatabaseEmulator('localhost', 9000);
      await FirebaseAuth.instance.useAuthEmulator('localhost', 9099);
      await FirebaseStorage.instance.useStorageEmulator('localhost', 9199);
      FirebaseFunctions.instance.useFunctionsEmulator('localhost', 5001);

I would assume have to initialize Cloud functions emulator somehow as well?
How do I update emulator to latest, to use cloud tasks emulator?

tzappia commented 2 months ago

@ReinisSprogis the Cloud Tasks emulator runs automatically and gets initialized if your functions code uses Cloud Tasks. See my comment above yours for some basic setup steps.

ReinisSprogis commented 2 months ago

I think that's for v2 functions. Couldn't figure out how to get v1 working.

jdziek commented 2 months ago

I've been following a combination of the Firebase Cloud Tasks documentation and what tzappia posted here, but I'm still running into an error.

The error I'm encountering:

FirebaseFunctionsError: Unexpected response with status: 404 and body ....
Cannot POST /projects/{projectName}/locations/{location}/queues/projects/{projectName}/locations/{location}/functions/sendUpdateEmailsTask/tasks

The URL looks confusing, as it repeats the values, so I'm really not sure if that's correct anymore. It's constructed using the method:

const queue = getFunctions().taskQueue(
  "projects/{projectName}/locations/{location}/functions/sendUpdateEmailsTask",
);

which according to their documentation within the module is correct.

When I just use the function name, I get this error:

Tried to queue a task into a non-existent queue

What I suspect:

I think the emulator might be trying to access a function on the hosted Firebase servers instead of accessing the local function. I haven't deployed the function yet because I want to get it working locally first.

Another indicator of this is that Firebase Admin throws authentication errors if I don't include API credentials when initializing the app via initializeApp, or if I haven't logged into the project using the CLI.

How I'm executing the call:

async function triggerSendUpdateEmail(data, emailType) {
  const queue = getFunctions().taskQueue(
    "projects/{projectName}/locations/{location}/functions/sendUpdateEmailsTask",
  );

  const date = new Date().toISOString().substring(0, 10);
  const targetUri = "http://127.0.0.1:5001/{projectName}/{location}/sendUpdateEmailsTask";

  try {
    await queue.enqueue(
      { date },
      {
        scheduleDelaySeconds: 5,
        dispatchDeadlineSeconds: 60 * 5, // 5 minutes
        uri: targetUri,
      },
    );
  } catch (err) {
    logger.error(err);
  }
}

What I've tried so far:


Any ideas or suggestions on how to resolve this issue? Thanks in advance for your help!


tzappia commented 2 months ago

@jdziek I definitely just used the function name when creating the queue. Here's my code:

const queue = getFunctions().taskQueue('expireWaitlistOffer');

Then when starting the emulators, I see this log:

✔  tasks: Created queue with key: queue:demo-project-us-central1-expireWaitlistOffer

I did at one point also see the error Tried to queue a task into a non-existent queue but I think to resolve it I just got all the config and code correct and started the emulators from scratch.

One additional tip, for local development, I always use a project name that starts with demo-. That's a special prefix that prevents the tooling from trying to connect to a real Firebase project. My Firebase config looks like:

firebase: {
    projectId: 'demo-project',
    apiKey: 'any',
    authDomain: 'demo-project.firebaseapp.com',
    databaseURL: 'https://demo-project.firebaseio.com',
    storageBucket: 'demo-project.appspot.com',
    messagingSenderId: 'any',
    appId: 'any',
    ...
}

And when I start the emulators I add the argument --project demo-project

ReinisSprogis commented 2 months ago

I couldn't get it running. But I realized I can get away without it.

I have function A an B on cloud functions. A schedules cloud task to execute B So what I ended up doing is just call B with delay.

jdziek commented 2 months ago

Ok, it worked. I think that either I cocked up somehow with location or the documentation is incorrect. When I set the receiving function to us-central and let the "taskQueue()" to default itself to that region it started sending the data through, so it works. I now need to figure out where it is failing to understand I want it to use eu servers. The function name in the snippet above SHOULD be correct according to the documentation in the module so not sure whats going on there. Thank you very much, I now have something to bounce off to at least. Works even when its not a demo project. Will need to make a separate request for configuring that as it seems like a better practice to use demo to begin with.

sacrosanctic commented 1 month ago

@ChromeQ is right - this was added in the most recent release thanks to the hard work of @GarrettBurroughs! Better documentation for it and examples will be coming in the next few weeks too. I'm gonna close this issue as fixed. If you run into any issues with the Cloud Task emulator or there is a use case that is not yet supported, please open a new issue.

@joehan how's the documentation coming along? I am stuck with the following error

{
  errorInfo: {
    code: 'functions/permission-denied',
    message: 'Permission denied on resource project demo-project.'
  },
  codePrefix: 'functions'
}
joehan commented 1 month ago

@sacrosanctic Would you mind opening a separate issue for this? It looks like you may have found a bug where the Tasks emulator doesn't correctly respect demo- projects. In the meantime, using a real project ID will likely workaround this issue for now.

tzappia commented 1 month ago

@joehan @sacrosanctic FWIW I am able to use the tasks emulator with a demo- project with v2 functions. I do not have any errors.

sacrosanctic commented 1 month ago

@joehan @sacrosanctic FWIW I am able to use the tasks emulator with a demo- project with v2 functions. I do not have any errors.

are you enqueueing using firebase or firebase-admin?

tzappia commented 1 month ago

are you enqueueing using firebase or firebase-admin?

firebase-admin

sacrosanctic commented 1 month ago

i figured something out, it works when i call it from the same node project that started the emulator

but if i run another node project along side it, i get permission denied

task queue is the only emulator service that behaves this way, as i have no issue doing this with auth, firestore, storage, or functions

i have 2 theories

BenJackGill commented 1 month ago

I was also having a lot of trouble with this. After a few days debugging I figured out some things that got it working:

1) You should use the demo- trick as described by others like this firebase emulators:start --project='demo-project' because it helps make sure everything is working only in the emulator, no live calls outside the emaultor.

2) You should use functionName when enqueuing the task functions with getFunctions().taskQueue(). Do not use the alternate formats of locations/{location}/functions/{functionName} or projects/{project}/locations/{location}/functions/{functionName} even though the docs suggest otherwise. I have opened an issue here for that: https://github.com/firebase/firebase-admin-node/issues/2725

3) Do not use the getFunctionUrl as described on this help document: https://firebase.google.com/docs/functions/task-functions?gen=2nd. The old idea from the help docs is that you need to programatically get the task url and pass it into as the uri of queue.enqueue() TaskOptions like this:

  await queue.enqueue(payload, {
    uri: getFunctionUrl() // <-- This will cause errors in the emulator
  });

But that does not work because it causes 404 errors when running in the emulator. I suspect this is because getFunctionUrl needs to make a call to Google to get the URL which is not allowed because we are now use demo- as the project ID (step 1 above).

Here is the full enqueue code that does work:

  // Get the queue
  const payload = { foo: "bar" }
  const queue = getFunctions().taskQueue("myExampleTaskFunctionName");

  // Schedule the task with the data provided
  // Note the lack of `uri` here in the TaskOptions
  await queue.enqueue(payload);
BenJackGill commented 1 month ago

I spoke too soon.

I am now seeing lots of errors for credentials like this:

Credential implementation provided to initializeApp() via the "credential" property failed to fetch a valid Google OAuth2 access token with the following error: "Error fetching access token: invalid_grant (reauth related error (invalidrapt))". There are two likely causes: (1) your server time is not properly synced or (2) your certificate key file has been revoked. To solve (1), re-sync the time on your server. To solve (2), make sure the key ID for your key file is still present at https://console.firebase.google.com/iam-admin/serviceaccounts/project. If not, generate a new key file at https://console.firebase.google.com/project//settings/serviceaccounts/adminsdk.

Here is my current testing code, can anyone spot the problem? I have tried adding back in the getFunctionUrl but it still doesn't work.

Here is a minimal reproduction: https://github.com/BenJackGill/firebase-task-emulator-bug

And here is the same code copy pasted:

index.ts

import * as admin from "firebase-admin";
import { HttpsError, onRequest } from "firebase-functions/v2/https";
import { getFunctions } from "firebase-admin/functions";
import { onTaskDispatched } from "firebase-functions/v2/tasks";
import * as logger from "firebase-functions/logger";
import { GoogleAuth } from "google-auth-library";

// Initialize the Firebase app
admin.initializeApp();

let auth: GoogleAuth | null = null;

// TypeScript remake of this function: https://firebase.google.com/docs/functions/task-functions?gen=2nd#retrieve_and_include_the_target_uri
const getFunctionUrl = async (region: string, name: string) => {
  if (process.env.FUNCTIONS_EMULATOR === "true") {
    console.log("Using emulator");
    return `http://127.0.0.1:5001/demo-project/${region}/${name}`;
  }

  if (!auth) {
    auth = new GoogleAuth({
      scopes: "https://www.googleapis.com/auth/cloud-platform",
    });
  }

  const projectId: string = await auth.getProjectId();
  const url = `https://cloudfunctions.googleapis.com/v2beta/projects/${projectId}/locations/${region}/functions/${name}`;

  interface ServiceConfig {
    uri?: string;
  }

  interface DataResponse {
    serviceConfig?: ServiceConfig;
  }

  interface ClientResponse {
    data: DataResponse;
  }

  const client = await auth.getClient();
  const res: ClientResponse = await client.request({ url });
  const uri: string | undefined = res.data?.serviceConfig?.uri;

  if (!uri) {
    throw new HttpsError(
      "unknown",
      `Unable to retrieve uri for function at ${url}`
    );
  }

  return uri;
};

// The http function
export const testOnRequest = onRequest(async (request, response) => {
  const taskPayload = {
    foo: "bar",
  };

  const taskFunctionName = "testOnTaskDispatched";
  const queue = getFunctions().taskQueue(taskFunctionName);
  const functionUrl = await getFunctionUrl("us-central1", taskFunctionName);

  try {
    await queue.enqueue(taskPayload, {
      uri: functionUrl,
    });
  } catch (error) {
    console.error("Error scheduling task", error);
    response.status(500).send("Error scheduling task");
    return;
  }
  response.send("Success. Hello from HTTP onRequest!");
});

// The task function
export const testOnTaskDispatched = onTaskDispatched((request) => {
  logger.info("Success. Hello logs from TASKS onTaskDispatched!", {
    foo: request.data,
  });
});

package.json

{
  "name": "functions",
  "scripts": {
    "build": "tsc",
    "build:watch": "tsc --watch",
    "serve": "npm run build && firebase emulators:start --project='demo-project' --debug",
    "shell": "npm run build && firebase functions:shell",
    "start": "npm run shell",
    "deploy": "firebase deploy --only functions",
    "logs": "firebase functions:log"
  },
  "engines": {
    "node": "20"
  },
  "main": "lib/index.js",
  "dependencies": {
    "firebase-admin": "^12.6.0",
    "firebase-functions": "^6.0.1",
    "google-auth-library": "^9.14.1"
  },
  "devDependencies": {
    "firebase-functions-test": "^3.1.0",
    "firebase-tools": "13.22.0",
    "typescript": "^4.9.0"
  },
  "private": true
}
styoe commented 2 weeks ago

If anyone is coming from python client world, here is what i had to do to make it work.


def enqueue_task(task_name: str, data: dict) -> str:
    credentials, project_id = google.auth.default(scopes=["https://www.googleapis.com/auth/cloud-platform"])
    task_queue = functions.task_queue(task_name)
    target_uri = get_function_url(project_id, credentials, task_name, config.region.value)
    task_options = functions.TaskOptions(
        schedule_delay_seconds=60,
        dispatch_deadline_seconds=60,
        uri=target_uri
    )
    task_data = {"data": data}

    if config.env.value == "local":
        # A series of things have to be modified in order to make this work in the emulator
        # So intead of just return task_queue.enqueue(data, task_options) we will do the following:

        # Patch the service account email to anything just so its not empty
        task_queue._credential.service_account_email = "test@test.com"

        # Do the steps from task_queue.enqueue method "manually" since its not working properly
        # 1. validate task options
        task = task_queue._validate_task_options(task_data, task_queue._resource, task_options)

        # 2. Service url does not get resolved in the emulator so we hardcode it
        service_url = f"http://localhost:9499/projects/{project_id}/locations/{config.region.value}/queues/{task_name}/tasks"

        # 3. Update task payload. Python client creates a http_request field and the emulator expects a httpRequest field
        #    You can see it here https://github.com/firebase/firebase-tools/blob/master/src/emulator/tasksEmulator.ts#L265
        task_payload = task_queue._update_task_payload(task, task_queue._resource, task_queue._extension_id)
        task_payload_dict = task_payload.__dict__
        task_payload_dict["httpRequest"] = task_payload_dict["http_request"]
        del task_payload_dict["http_request"]

        # 4. Post the request
        resp = task_queue._http_client.body(
            'post',
            url=service_url,
            json={'task': task_payload.__dict__}
        )

        # 5. Return the task name
        resp_task = resp["task"]
        resp_task_name = resp_task["name"]
        print("resp_task_name", resp_task_name)
        return resp_task_name

        # Example payload to tasks emulator if you want to try it using postman.
        # http://localhost:9499/projects/demo-local-development/locations/europe-west4/queues/taskbatchevaluation/tasks
        # POST
        # {
        #   "task": {
        #     "httpRequest": {
        #         "url": "http://127.0.0.1:5001/demo-local-development/europe-west4/taskbatchevaluation",
        #         "body": "eyJkYXRhIjogeyJibGEiOiAiYmxhIn19"
        #     }
        #   }
        # }
        # HEADERS:
        # X-GOOG-API-FORMAT-VERSION: 2
        # X-FIREBASE-CLIENT: fire-admin-python/6.5.0
        # BODY:
        # {"data": {"bla": "bla"}}
        # b64encode(json.dumps(data).encode()).decode()
        # eyJkYXRhIjogeyJibGEiOiAiYmxhIn19

    return task_queue.enqueue(task_data, task_options)

Hope it helps