Functions emulator can enter an infinite loop after a Firestore trigger
See original GitHub issue[REQUIRED] Environment info
firebase-tools: 9.1.2
Platform: macOS 11.1 x86
[REQUIRED] Test case
This is a rather unusual bug that appears to be the result of some kind of internal race condition in the functions emulator when it comes to deleting documents. This is triggered from using an admin client of the @firebase/rules-unit-testing
library triggering an emulated function from firebase-functions
, but I believe the issue resides in the emulator itself, hence this ticket. I have been able to reproduce this issue on Node 12 and Node 14.
Firstly, a Firestore function trigger is needed. This is what will enter the infinite loop. The trigger itself is an onCreate
trigger that should only be called when a document is initially created. It will immediately delete anything written to some collection in the database. I have been able to reproduce the issue for both root collections and sub-collections. We will use a root collection for simplicity.
// Firebase functions project
// index.js
const functions = require("firebase-functions");
exports.bugTestFunction = functions.firestore
.document("testing-bug/{mydoc}")
.onCreate(async (snapshot, context) => {
console.log("data", JSON.stringify(snapshot.data()));
await snapshot.ref.delete();
});
Now we set some data and delete it ourselves. I have created a script that appears to trigger the issue every time, using the @firebase/rules-unit-testing
library. Ensure that the project ID matches the project ID of the firebase functions project. Also note that we need to call the same code twice to trigger 2 invocations of the target function. The bug does not occur with just 1 call.
// Separate script for triggering the bug
// bug-script.js
const firebase = require("@firebase/rules-unit-testing");
const admin = firebase.initializeAdminApp({
// project ID MUST match that of your firebase functions project
projectId: "my-project",
});
async function run() {
const ref = admin.firestore().collection("testing-bug");
console.log("setting value...");
const newRef = await ref.add({ test: 123 });
await new Promise((r) => setTimeout(r, 3000));
console.log("deleting value...");
await newRef.delete();
}
// run twice for the bug
run().then(() => run());
[REQUIRED] Steps to reproduce
After using the steps described above to setup the test environment, we can reproduce the bug.
- Open a terminal and start the Firebase Emulator Suite. Functions and Firestore should be enabled.
- Leaving the emulator running, open another terminal and run the test code above using
node bug-script.js
. - Look back at the emulator. We see it has entered an infinite loop with a repeating log as shown below. You can kill the emulator by sending
Ctrl
+C
.
i functions: Beginning execution of "bugTestFunction"
i functions: Finished "bugTestFunction" in ~1s
i functions: Beginning execution of "bugTestFunction"
i functions: Finished "bugTestFunction" in ~1s
i functions: Beginning execution of "bugTestFunction"
i functions: Finished "bugTestFunction" in ~1s
i functions: Beginning execution of "bugTestFunction"
i functions: Finished "bugTestFunction" in ~1s
i functions: Beginning execution of "bugTestFunction"
i functions: Finished "bugTestFunction" in ~1s
i functions: Beginning execution of "bugTestFunction"
i functions: Finished "bugTestFunction" in ~1s
i functions: Beginning execution of "bugTestFunction"
i functions: Finished "bugTestFunction" in ~1s
i functions: Beginning execution of "bugTestFunction"
i functions: Finished "bugTestFunction" in ~1s
i functions: Beginning execution of "bugTestFunction"
i functions: Finished "bugTestFunction" in ~1s
i functions: Beginning execution of "bugTestFunction"
i functions: Finished "bugTestFunction" in ~1s
i functions: Beginning execution of "bugTestFunction"
i functions: Finished "bugTestFunction" in ~1s
[REQUIRED] Expected behavior
The emulator should not enter an infinite loop. The document at the reference should be deleted after 1 function invocation. Even if the local client ‘beats’ the function trigger to deleting the document, the trigger should still only be called once, it will now just point to a deleted document.
[REQUIRED] Actual behavior
The document is deleted, but the emulator appears to call the trigger function in a loop. The snapshot.data()
will contain undefined
for each of the additional triggers, implying that the trigger is repeatedly called on the already deleted document.
There is a possibility that this issue also occurs in the live Functions and Firestore environment, but I have not attempted this for fear of exhausting my billing budget! If anyone can also confirm that this only occurs in the emulator that would help to put me at ease!
Issue Analytics
- State:
- Created 3 years ago
- Comments:7 (5 by maintainers)
Asigning to @rosalyntan who is on call for issue this week.
@bradleymackey thank you for the details! I was able to reproduce this, it does seem like a bug to me. Let me investigate some more.