Skip to content

Instantly share code, notes, and snippets.

@CodingDoug
Created December 5, 2019 02:24
Show Gist options
  • Save CodingDoug/83bdd8c00dc64b22472f618e26fe5ca3 to your computer and use it in GitHub Desktop.
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)
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()
})
}
})
@josselinPello
Copy link

josselinPello commented Mar 30, 2021

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 under admin.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! 😅

Screenshot 2021-03-30 at 22 34 57

(For info: PubSub seems to have a similar problem googleapis/nodejs-pubsub#1069 (comment))

@justkawal
Copy link

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