Schedule Cloud Functions with Cloud Tasks (Event Scheduling) Part 2

Muhammad Adnan
7 min readJun 13, 2023

--

Hey, welcome to Part 2 of Scheduling Cloud Function with Cloud Tasks. I hope you have read my first article. If not, it will be hard for you to figure out code and other stuff here. I would recommend reading the first one where we step by step go through each and every little thing related to Cloud Tasks, from initial understanding and setting up, to final handling security issues we have covered everything, so make sure you check that out.

Link to first article.

In this article I will assume you know everything that we learned in Part 1. So let’s get started.

The problem

Build an event scheduling system that will respond in UI to show an event box in different areas of the app, when the status of the event changes at the scheduled times.

Event model

In event model we’re going to have some fields that will be “startEventTime”, “endEventTime”, “eventStatus”, and ofcourse an “eventId”.

Event Status Changes Scenario

create string constants “inProgress”, “started”, “almostStarted”, “ended”, “canceled” in your app, that will be the statuses of the event. By utilizing statuses write some get methods of the database you’re using it could be Firestore, and show them up on the specified status of the event.

Event status will be updated to “almostStarted” when there’s 30 minutes remaining from starting the event and same for others when “started” and “ended”. And if no Cloud Task has been dispatched yet the status will be “inProgress”. And ofcourse, you are right, you can cancel the event 😀.

You can show up events on different areas of your app by running a “where” query in firestore and say where the “eventStatus” isEqualTo “started” show all the started events here, and also same for other event status.

Code example in Flutter:

Stream<List<EventEntity>> getStartedEvents() {
final eventCollection = fireStore.collection(events);
return eventCollection.where(“eventStatus”, isEqualTo: “started”).snapshots().map((querySnapshot) => querySnapshot.docs.map((e) => EventModel.fromSnapshot(e).toList());
}

When event is created

When event is created in the events collection, with the specified startEventTime and endEventTime. We will schedule 3 tasks in the queue for a single event. First task in the queue will be dispatched 30 minutes before starting the event, and will update the eventStatus to almostStarted. Second task in the queue will be dispatched when the event is finally started, and will update the eventStatus to started. And the same for endEventTime when end time arrives.

Back to Cloud Functions

I’m assuming again you have everything set up, if not, you might definitely have missed Part 1 so I’d recommend reading this then it will be easy for you to figure out what is going on here.

Event Scheduling OnCreate Trigger

Import these:

import * as functions from 'firebase-functions'
import * as admin from 'firebase-admin'
const { CloudTasksClient } = require('@google-cloud/tasks')
import { OAuth2Client } from “google-auth-library”;

Initialize Firebase admin:

admin.initializeApp()

We will again start from the onCreateEvent trigger.

export const onCreateEvent = functions.firestore.document('/events/{eventId}').onCreate(async snapshot => {

// Access event data
const event = snapshot.data();
const eventId = snapshot.id;

// Get start and end time in milliseconds
const start = event.startEventTime.toMillis();
const end = event.endEventTime.toMillis();

// Configure queue
const project = JSON.parse(process.env.FIREBASE_CONFIG!).projectId;
const location = `us-central1`;
const queue = ‘my-scheduler’;
const tasksClient = new CloudTasksClient();

await createTasks(tasksClient, project, location, queue, eventId, start, end);

});

// function for scheduling tasks
async function createTasks(tasksClient: any, project: any, location: any, queue: any, eventId: any, start: any, end: any) {

const thirtyMinutesBeforeStart = start - 30 * 60 * 1000;
const payload = { eventId };
const taskName = “projects/${project}/locations/${location}/queues/{queue}/tasks”;
const serviceAccountEmail = “SERVICE_ACCOUNT_EMAIL_HERE”;

// Define Tasks to Create

const almostStartedTask {
name: `${taskName}/almostStartedTask-${eventId}-${start}`,
httpRequest: {
httpMethod: “POST”,
url: `https://${location}/${project}.cloudfunctions.net/updateEventCallback?eventId=${eventId}&status=almostStarted`,
body: Buffer.from(JSON.stringify(payload)).toString(base64),
headers: { ‘Content-Type’: application/json },
oidcToken: {
serviceAccountEmail,
audience: `https://{location}/${project}.cloudfunctions.net/updateEventCallback`,
},
},
scheduleTime: { seconds: Math.floor(thirtyMinutesBeforeStart / 1000) },
};


const startedTask {
name: `${taskName}/startedTask-${eventId}-${start}`,
httpRequest: {
httpMethod: “POST”,
url: `https://${location}/${project}.cloudfunctions.net/updateEventCallback?eventId=${eventId}&status=started`,
body: Buffer.from(JSON.stringify(payload)).toString(base64),
headers: { ‘Content-Type’: application/json },
oidcToken: {
serviceAccountEmail,
audience: `https://{location}/${project}.cloudfunctions.net/updateEventCallback`,
},
},
scheduleTime: { seconds: Math.floor(start / 1000) },
};


const endedTask {
name: `${taskName}/endedTask-${eventId}-${start}`,
httpRequest: {
httpMethod: “POST”,
url: `https://${location}/${project}.cloudfunctions.net/updateEventCallback?eventId=${eventId}&status=ended`,
body: Buffer.from(JSON.stringify(payload)).toString(base64),
headers: { ‘Content-Type’: application/json },
oidcToken: {
serviceAccountEmail,
audience: `https://{location}/${project}.cloudfunctions.net/updateEventCallback`,
},
},
scheduleTime: { seconds: Math.floor(end / 1000) },
};


// Create Tasks

await tasksClient.createTasks({ parent: tasksClient.queuePath(project, location, queue), task: almostStartedTask });

await tasksClient.createTasks({ parent: tasksClient.queuePath(project, location, queue), task: startedTask });

await tasksClient.createTasks({ parent: tasksClient.queuePath(project, location, queue), task: endedTask });
}

That was it for the onCreate trigger, having this done when the event is created this onCreate trigger will be triggered and will schedule 3 events which will execute on their specified time.

Next, we need an updateEventCallback HTTP trigger to implement, let’s also go through it.

Note: You must have a service account created, with certain roles that are in the Part 1 of this article.

export const updateEventCallback = functions.https.onRequest(async (req, res) => {

const project = json.parse(process.env.FIREBASE_CONFIG!).projectId;
const location = ‘us-central1’;

// access the headers > authorization of the request
const authorizationHeader = req.headers.authorization;

// check if authorization header is not null
if(!authorizationHeader) {
res.status(401).send(“unauthorized token”);
return;
}

// if authorizationHeader is not null access the token
const token = authorizationHeader.split(‘ ’)[1];

// verify ID token
try {
await verifyIdToken(token, location, project);
} catch (error) {
console.log(error);
res.status(401).send(“Unauthorized token”);
return;
}

// access query params
const eventId = req.query.eventId as string;
const status = req.query.status as string;
// update event status
await admin.firestore().collection(“events”).doc(eventId).update({ eventStatus: status });

});

Also if you want to generate notifications on status changes you can do likewise, after updating the status in the updateEventCallback you can do something like this:

   ...
// update event status
await admin.firestore().collection(“events”).doc(eventId).update({ eventStatus: status });

// get event
const eventDocRef = await admin.firestore().collection(“events”).doc(eventId).get();
const eventData = eventDocRef.data();

if(eventData) {
// here access event necessary fields of events
// such as eventName, the participants of event
// also the eventStatus

// and loop through the event participants list
// and inside the loop body check if the current
// updated status was “started” or “ended”, and
// generate notification accordingly.

}

});

Make sure you have this method to verify token:

async function verifyIdToken(token: string, location: any, project: any) {
const client = new OAuth2Client();
const ticket = await client.verifyIdToken({
idToken: token,
audience: “https://${location}-${project}.cloudfunctions.net/updateEventCallback”,
});

return ticket.getPayload();
}

With this done, we have successfully scheduled an event, which will notify users according to status changes when the scheduled time arrives of the task in the Cloud Tasks queue.

What if we want to update event time?

Remember? From Part 1, I said there’s no built-in method to update the event time but you have to delete the tasks from the queue and recreate them. So let’s do it.

export const eventUpdateTime = functions.firestore.document(“/events/${eventId}”).onUpdate(async (change, context) => {

const eventBeforeUpdate = change.before.data();
const eventAfterUpdate = change.after.data();

const startTimeBefore = eventBeforeUpdate.startEventTime.toMillis();
const endTimeBefore = eventBeforeUpdate.endEventTime.toMillis();
const startTimeAfter = eventAfterUpdate.startEventTime.toMillis();
const endTimeAfter = eventAfterUpdate.endEventTime.toMillis();

if(startTimeBefore !== startTimeAfter || endTimeBefore !== endTimeAfter) {
// access eventId
const eventId = eventAfterUpdate.eventId;

// access queue info
const project = JSON.parse(process.env.FIREBASE_CONFIG!).projectId;
const location = ‘us-central1’;
const queue = “my-scheduler”;
const tasksClient = new CloudTasksClient();

// actual queue path
const parent = tasksClient.queuePath(project, location, queue);
// list tasks
const tasks = await tasksClient.listTasks({ parent });

// Iterate over tasks to find and delete the ones associated with updated event

for(const task of tasks[0]) {
if(task.name.includes(eventId)) {
await tasksClient.deleteTask({ name: task.name });
console.log(`Task ${task.name} was deleted.`);
}
}

// call the createTasks method to reschedule the tasks according to the startEventTime and endEventTime fields updated.

const start = eventAfterUpdate.startEventTime.toMillis();
const end = eventAfterUpdate.endEventTime.toMillis();

await createTasks(tasksClient, project, location, queue, eventId, start, end);
}

});

What if we want to cancel event?

It’s pretty simple, check the condition on status changes to “canceled” and delete all the tasks from the queue containing that specific eventId as well as event from the collection. Cancelation is described already in the previous article Part 1.

What about security?

What?? Security is already done, what kind of security now you’re talking about? Calm down, I’m just joking 😀.

Remember this

Security should never be an afterthought when orchestrating your tasks schedules. It’s crucial to create robust security rules that cap the understanding duration according to your specific needs. But why, you ask?

Consider a scenario: a rogue user schedules events far into the future. These events, then, languish in your tasks queue indefinitely. This is not merely an irritating inconvenience, but a genuine issue to tackle.

Cloud Tasks has an inbuilt safety mechanism; tasks cannot be scheduled for more than 30 days. Yet if we, the developers, unwarily allow users in our app to schedule tasks beyond this timeframe, we’re inviting trouble. Without strict security rules, the events will attempt to register beyond the 30 days limit, causing an error or even potentially disrupting the entire task queue system.

So, my fellow coders, the power lies in your hands. With right security rules, you can ensure smooth sailing — or, in our case, smooth scheduling. Remember, the longevity of a task in a queue is a lot like a perfectly steeped cup of tea — it needs to be just right.

Conclusion

I provided everything along with a clear explanation of how each method is going to execute and what job it is going to complete. But I’m not sure what are the requirements of your app, so feel free to modify the code according to your app’s requirement. If you found this article useful, share it with other learners and let me know your experience in the comment section. Don’t forget to clap.

Follow me on:

LinkedIn: https://www.linkedin.com/in/muhammad-adnan-23bb8821b/

GitHub: https://github.com/AdnanKhan45

Instagram: https://www.instagram.com/dev.adnankhan/

--

--

Muhammad Adnan

Flutter Software Engineer | Helping founders successfully land their Android & iOS apps | Empowering people on YouTube | Firebase & Node.js Expert