Skip to content

🐛 [firestore-bigquery-export] handleFailedTransactions crashes on warm containers, masking real BigQuery errors #2801

@jakehockey10

Description

@jakehockey10

Describe your configuration

  • Extension name: firestore-bigquery-export
  • Extension version: 0.3.1

Describe the problem

handleFailedTransactions in firestore-bigquery-change-tracker calls db.settings({ ignoreUndefinedProperties: true }) inside the exported async function body, then performs batch operations on the same Firestore instance. The Firestore Node SDK's settings() contract requires it be called at most once and before any other method is invoked on the same instance.

Because getFirestore(config.firestoreInstanceId!) returns the per-process singleton, the second invocation on any warm Cloud Functions container satisfies both blocking conditions (already-called AND other-methods-ran). The result is a Firestore has already been initialized error that masks the original BigQuery insert failure that triggered the dead-letter path in the first place.

Steps to reproduce

  1. Install firestore-bigquery-export@0.3.1 with BACKUP_COLLECTION set.
  2. Trigger an intentional BigQuery insert failure (e.g. schema mismatch, missing dataset, quota exhaustion) so insertData exhausts its retries and falls through to handleFailedTransactions.
  3. First invocation: the dead-letter path succeeds and writes rows to the configured backup collection.
  4. Second invocation on the same warm container: throws with the stack trace below.

Expected result

The dead-letter path writes to the backup collection on every failure, without conflicting with the Firestore instance's initialization.

Actual result

Error: Firestore has already been initialized. You can only call settings() once, and only before calling any other methods on a Firestore object.
    at Firestore.settings (/workspace/node_modules/@google-cloud/firestore/build/src/index.js:554:19)
    at exports.default (/workspace/node_modules/@firebaseextensions/firestore-bigquery-change-tracker/lib/bigquery/handleFailedTransactions.js:11:8)
    at FirestoreBigQueryEventHistoryTracker.insertData (/workspace/node_modules/@firebaseextensions/firestore-bigquery-change-tracker/lib/bigquery/index.js:192:62)
    at process.processTicksAndRejections (node:internal/process/task_queues:105:5)

Root cause

The 2022 version of handleFailedTransactions.ts called settings() once inside an if (!firebase.apps.length) guard at module scope — correct usage.

PR #2364 ("support non-default db", April 2025) refactored the module to use getFirestore(config.firestoreInstanceId!) but moved the settings() call out of the init guard and into the exported function body. Every invocation now retries settings() on the singleton Firestore instance, which throws after the first call.

Current code at commit b899c58e:

export default async (
  rows: any[],
  config: ChangeTrackerConfig,
  e: Error,
): Promise<void> => {
  const db = getFirestore(config.firestoreInstanceId!);
  db.settings({
    ignoreUndefinedProperties: true,
  });
  const batchArray = [db.batch()];
  // ... batch.set(), batch.commit() on the same db
};

Suggested fix

Either:

  1. Init-once at module scope. Call settings() in a module-level guard (keyed by firestoreInstanceId if non-default DBs need distinct settings), restoring the pattern from the 2022 version.
  2. Use a distinct Firestore instance. Scope the settings() call to a new Firestore object rather than the process singleton.

Impact

  • The dead-letter path silently crashes on every failure after the first on a warm container, instead of recording failed rows to the backup collection.
  • The original BigQuery error — which is typically what operators need to see — is masked by the secondary settings() crash.
  • Operators troubleshooting insert failures waste cycles chasing the Firestore-initialization error before realizing it's a wrapper around something else entirely.

Additional context

Filed after hitting this while upgrading from 0.2.6 to 0.3.1 in a production Cloud Functions environment. No matching upstream reports found on a search of "Firestore has already been initialized" / "can only call settings" / handleFailedTransactions — this appears to be the first public report.

Happy to open a PR implementing option (1) if that's the preferred direction.

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions