This is the full Cloud Functions code explained in my blog post: How to schedule a Cloud Function to run in the future (in order to build a Firestore document TTL)
Created
December 5, 2019 02:24
-
-
Save CodingDoug/83bdd8c00dc64b22472f618e26fe5ca3 to your computer and use it in GitHub Desktop.
How to schedule a Cloud Function to run in the future (in order to build a Firestore document TTL)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import * as functions from 'firebase-functions' | |
import * as admin from 'firebase-admin' | |
const { CloudTasksClient } = require('@google-cloud/tasks') | |
admin.initializeApp() | |
// Payload of JSON data to send to Cloud Tasks, will be received by the HTTP callback | |
interface ExpirationTaskPayload { | |
docPath: string | |
} | |
// Description of document data that contains optional fields for expiration | |
interface ExpiringDocumentData extends admin.firestore.DocumentData { | |
expiresIn?: number | |
expiresAt?: admin.firestore.Timestamp | |
expirationTask?: string | |
} | |
export const onCreatePost = | |
functions.firestore.document('/posts/{id}').onCreate(async snapshot => { | |
const data = snapshot.data()! as ExpiringDocumentData | |
const { expiresIn, expiresAt } = data | |
let expirationAtSeconds: number | undefined | |
if (expiresIn && expiresIn > 0) { | |
expirationAtSeconds = Date.now() / 1000 + expiresIn | |
} | |
else if (expiresAt) { | |
expirationAtSeconds = expiresAt.seconds | |
} | |
if (!expirationAtSeconds) { | |
// No expiration set on this document, nothing to do | |
return | |
} | |
// Get the project ID from the FIREBASE_CONFIG env var | |
const project = JSON.parse(process.env.FIREBASE_CONFIG!).projectId | |
const location = 'us-central1' | |
const queue = 'firestore-ttl' | |
const tasksClient = new CloudTasksClient() | |
const queuePath: string = tasksClient.queuePath(project, location, queue) | |
const url = `https://${location}-${project}.cloudfunctions.net/firestoreTtlCallback` | |
const docPath = snapshot.ref.path | |
const payload: ExpirationTaskPayload = { docPath } | |
const task = { | |
httpRequest: { | |
httpMethod: 'POST', | |
url, | |
body: Buffer.from(JSON.stringify(payload)).toString('base64'), | |
headers: { | |
'Content-Type': 'application/json', | |
}, | |
}, | |
scheduleTime: { | |
seconds: expirationAtSeconds | |
} | |
} | |
const [ response ] = await tasksClient.createTask({ parent: queuePath, task }) | |
const expirationTask = response.name | |
const update: ExpiringDocumentData = { expirationTask } | |
await snapshot.ref.update(update) | |
}) | |
export const firestoreTtlCallback = functions.https.onRequest(async (req, res) => { | |
const payload = req.body as ExpirationTaskPayload | |
try { | |
await admin.firestore().doc(payload.docPath).delete() | |
res.send(200) | |
} | |
catch (error) { | |
console.error(error) | |
res.status(500).send(error) | |
} | |
}) | |
export const onUpdatePostCancelExpirationTask = | |
functions.firestore.document('/posts/{id}').onUpdate(async change => { | |
const before = change.before.data() as ExpiringDocumentData | |
const after = change.after.data() as ExpiringDocumentData | |
// Did the document lose its expiration? | |
const expirationTask = after.expirationTask | |
const removedExpiresAt = before.expiresAt && !after.expiresAt | |
const removedExpiresIn = before.expiresIn && !after.expiresIn | |
if (expirationTask && (removedExpiresAt || removedExpiresIn)) { | |
const tasksClient = new CloudTasksClient() | |
await tasksClient.deleteTask({ name: expirationTask }) | |
await change.after.ref.update({ | |
expirationTask: admin.firestore.FieldValue.delete() | |
}) | |
} | |
}) |
I'm having an issue: Error: 7 PERMISSION_DENIED: Permission denied on resource project cloud task: #3133276751295.........
i.e. Cloud Function is unable to delete the cloud tasks.
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Awesome piece of code demonstrating the power of Cloud Tasks in combination with Firebase Cloud functions!
However, @CodingDoug I strongly suggest you move the client creation (
const tasksClient = new CloudTasksClient()
) out of the Firestore document listener and make it global, for example just underadmin.initializeApp()
Otherwise, people re-using this code can create a nasty memory leak that makes their cloud function's memory utilization increase linearly with time (and probably exceed the limit) unless they re-deploy it! 😅
(For info: PubSub seems to have a similar problem googleapis/nodejs-pubsub#1069 (comment))