最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

node.js - Firebase Functions excessive latency - Stack Overflow

programmeradmin2浏览0评论

There is a significant performance degradation in a Firebase function I developed. This function handles payment reception via Stripe, generates a QR Code, and records data in multiple Firestore collections for ticket creation during a reservation.

Ideally, the execution of this function should conclude within a few seconds at most. However, since its deployment, we have been observing abnormally long response times: on average between 5 and 15 seconds, and in some cases, between 2 and 4 minutes per ticket, even though several tickets may be processed concurrently.

To try and mitigate this issue, we have already implemented several optimizations, including:

  • Managing cold starts and increasing the minimum number of instances.

  • Optimizing the function’s code.

  • Reducing the size of the generated QR Codes.

  • Decreasing the volume of data transmitted to Firestore.

Despite these actions, the problem persists and significantly impacts our ticket creation process.

I would greatly appreciate any advice or insights from your experiences to help identify the causes of these delays and implement the necessary fixes.

Thank you in advance for your assistance and expertise.

const { setGlobalOptions } = require('firebase-functions/v2');
setGlobalOptions({ 
  region: "europe-west3",
});

const functions = require('firebase-functions');
const admin = require('firebase-admin');
const stripe = require('stripe')('---');
const axios = require('axios');
const QRCode = require('qrcode');
const { Storage } = require('@google-cloud/storage');
const storage = new Storage();
const { firestore } = require('firebase-admin');
const cors = require('cors')({ origin: true });

admin.initializeApp();

exports.stripeWebhook = functions.https.onRequest((req, res) => {
    cors(req, res, async () => {
      let event;
      try {
        event = stripe.webhooks.constructEvent(
          req.rawBody,
          req.headers['stripe-signature'],
          '---' 
        );
      } catch (err) {

      }

      if (event.type === 'checkout.sessionpleted') {
        const session = event.data.object;
        const data = extractDataFromSession(session);

        // Parsing du champ tickets (JSON)
        let ticketsArray = [];
        try {
          ticketsArray = JSON.parse(data.tickets);
          console.log("Tickets extraits:", ticketsArray);
        } catch (err) {
          console.error("Erreur lors du parsing des tickets:", err.message);
          return res.status(400).send("Tickets invalides dans les metadata.");
        }
        if (!Array.isArray(ticketsArray) || ticketsArray.length === 0) {
          console.error("Aucun ticket trouvé.");
          return res.status(400).send("Aucun ticket trouvé.");
        }

        // Réponse immédiate à Stripe (code 200)
        res.status(200).send({ received: true });

        // Traitement complet en arrière-plan (ne bloque pas la réponse)
        (async () => {
          try {
            for (const ticket of ticketsArray) {
              for (let i = 0; i < ticket.quantity; i++) {
                const ticketData = {
                  locaImageUrl: data.localisationImageUrl,
                  garantis_: data.garantis_,
                  email: data.email,
                  sexe: data.sexe,
                  total: data.totalA,
                  uid: data.uid,
                  name: data.name,
                  namePro: data.namePro,
                  etc... etc
    
                };


                const qr = await uniqueCodeCreate(ticketData);
                const publicUrl = await createAndUploadQRCode(ticketData, qr, 256, 'M');

                await reservationAndProDetailsToFirestore(ticketData, publicUrl, qr);
                await sendQrCodeEmail(ticketData, publicUrl, qr);
          
               
              }
            }
          } catch (error) {
            console.error("Erreur lors du traitement asynchrone:", error);
          }
        })();
      } else {
        res.status(400).send("Type d’événement non géré");
      }
    });
});

function extractDataFromSession(session) {
  return {
    localisationImageUrl: session.metadata.localisationImageUrl,
    garantis_: session.metadata.garantis_,
    email: session.metadata.email,
    sexe: session.metadata.sexe,
    total: session.metadata.total,
    uid: session.metadata.uid,
    etc..etc..etc
  };
}

  
  async function uniqueCodeCreate(data) {
      const prenom = data.name;
  
      const rng = Math.floor(Math.random() * ____);
      const qr = ____;

      return qr;
  }

  
  async function sendQrCodeEmail(data, publicUrl, qr) {
    try {
        
        const fraix = isNaN(parseFloat(data.ticketPriceFraix)) ? 0.0 : parseFloat(data.ticketPriceFraix);
        const amount = parseFloat(data.ticketPrice);
        const dateDebutTimestamp = admin.firestore.Timestamp.fromDate(new Date(data.dateDebut));
        const url = '---';

        // Envoi de la requête POST à l'API
        const response = await axios.post(url, {
          localisationImageUrl: "",
          resaID: qr,
          dateDebut: dateDebutTimestamp,
         etc...etc...
        });
  
        // Vérification de la réponse
        if (response.status !== 200) {
           
        
        }
        return { success: true, message: "Email sent successfully." };
  
    } catch (error) {

  
        let errorMessage = "Error sending QR code email";
        if (error.response) {
            console.error("Erreur dans la réponse:", error.response.status);
            console.error("Détails de l'erreur:", error.response.data);
            errorMessage += : Server responded with status: ${error.response.status};
        } else if (error.request) {
            console.error("Erreur dans la requête:", error.request);
            errorMessage += ": No response received";
        } else {
            console.error('Erreur lors de la configuration de la requête:', error.message);
            errorMessage += : Request setup failed: ${error.message};
        }
  
        return { success: false, message: errorMessage };
    }
  }
  

  
  
async function createAndUploadQRCode(data, qr, width = 256, errorCorrectionLevel = 'M') {
  try {
    const options = {
      color: { dark: "#000000", light: "#0000" },
      width: width,
      errorCorrectionLevel: errorCorrectionLevel,
    };
    const qrCodeBuffer = await QRCode.toBuffer(qr, options);

    
    const bucket = admin.storage().bucket();
    const filePath = EventsFile/${---}/${data.---}/---/${---}.png;
    const file = bucket.file(filePath);
    await file.save(qrCodeBuffer, { contentType: 'image/png', public: true });
    console.log("QR code uploadé.");

    const publicUrl = /${bucket.name}/${file.name};
    return publicUrl;
  } catch (error) {
    throw error;
  }
}

  async function reservationAndProDetailsToFirestore(data, publicUrl, qr) {
  try {
    // Initialisation locale de Firestore
    const firebaseFirestore = firestore();
   
    const fraix = isNaN(parseFloat(data.ticketPriceFraix)) ? 0.0 : parseFloat(data.ticketPriceFraix);
    const amount = parseFloat(data.ticketPrice);
    const dateDebutTimestamp = admin.firestore.Timestamp.fromDate(new Date(data.dateDebut));
    const dateFinTimestamp = admin.firestore.Timestamp.fromDate(new Date(data.dateFin));
    const now = new Date();

    const resaModel = {
     ...//...//...
    };
 
    const resaDetails = {
  ...//...//...
    };
  
    const historiqueDetails = {
  ...//...//...
    };
    const clientInfo = {
    ...//...//...
    };

    const historiqueClientDetails = {
      ...//...//...
    };
   
    const postRef = firebaseFirestore
      .collection("--")
      .doc(--)
      .collection("--")
      .doc(--);

    const postUpdateData = {
      '--': admin.firestore.FieldValue.increment(amount),
      [--]: firestore.FieldValue.increment(-1),
      [--]: firestore.FieldValue.increment(1)
    };
    if (data.sexe === '--') {
      postUpdateData['--'] = firestore.FieldValue.increment(1);
    } else if (data.-- === '--') {
      postUpdateData['--'] = firestore.FieldValue.increment(1);
    }

    const batch = firebaseFirestore.batch();

    // Ajout des écritures dans le batch :
    batch.set(
      firebaseFirestore.collection("--").doc(--).collection("-- --").doc(--),
      
    );
    batch.set(
      firebaseFirestore.collection("--").doc(--).collection("Reservation").doc(--),
      resaDetails
    );
    batch.set(
      firebaseFirestore.collection("--").doc(--).collection("-- --").doc(--),
      historiqueDetails
    );
    const clientDocRef = firebaseFirestore.collection("--").doc(--).collection("--").doc(--);
    batch.set(clientDocRef, clientInfo, { merge: true });
    batch.set(clientDocRef.collection("--").doc(--), historiqueClientDetails);
    
    batch.update(postRef, { ...postUpdateData, Like: admin.firestore.FieldValue.increment(1) });

    // Modification : retourner la promesse du commit
    return batchmit().then(() => {
      console.timeEnd("batchCommit");
      console.log("=== Fin de combinedReservationAndHistoryToFirestore ===");
      return { success: true, message: "" };
    });
  } catch (error) {
    console.error("", error);
    return { success: false, message: "" };
  }
}

There is a significant performance degradation in a Firebase function I developed. This function handles payment reception via Stripe, generates a QR Code, and records data in multiple Firestore collections for ticket creation during a reservation.

Ideally, the execution of this function should conclude within a few seconds at most. However, since its deployment, we have been observing abnormally long response times: on average between 5 and 15 seconds, and in some cases, between 2 and 4 minutes per ticket, even though several tickets may be processed concurrently.

To try and mitigate this issue, we have already implemented several optimizations, including:

  • Managing cold starts and increasing the minimum number of instances.

  • Optimizing the function’s code.

  • Reducing the size of the generated QR Codes.

  • Decreasing the volume of data transmitted to Firestore.

Despite these actions, the problem persists and significantly impacts our ticket creation process.

I would greatly appreciate any advice or insights from your experiences to help identify the causes of these delays and implement the necessary fixes.

Thank you in advance for your assistance and expertise.

const { setGlobalOptions } = require('firebase-functions/v2');
setGlobalOptions({ 
  region: "europe-west3",
});

const functions = require('firebase-functions');
const admin = require('firebase-admin');
const stripe = require('stripe')('---');
const axios = require('axios');
const QRCode = require('qrcode');
const { Storage } = require('@google-cloud/storage');
const storage = new Storage();
const { firestore } = require('firebase-admin');
const cors = require('cors')({ origin: true });

admin.initializeApp();

exports.stripeWebhook = functions.https.onRequest((req, res) => {
    cors(req, res, async () => {
      let event;
      try {
        event = stripe.webhooks.constructEvent(
          req.rawBody,
          req.headers['stripe-signature'],
          '---' 
        );
      } catch (err) {

      }

      if (event.type === 'checkout.sessionpleted') {
        const session = event.data.object;
        const data = extractDataFromSession(session);

        // Parsing du champ tickets (JSON)
        let ticketsArray = [];
        try {
          ticketsArray = JSON.parse(data.tickets);
          console.log("Tickets extraits:", ticketsArray);
        } catch (err) {
          console.error("Erreur lors du parsing des tickets:", err.message);
          return res.status(400).send("Tickets invalides dans les metadata.");
        }
        if (!Array.isArray(ticketsArray) || ticketsArray.length === 0) {
          console.error("Aucun ticket trouvé.");
          return res.status(400).send("Aucun ticket trouvé.");
        }

        // Réponse immédiate à Stripe (code 200)
        res.status(200).send({ received: true });

        // Traitement complet en arrière-plan (ne bloque pas la réponse)
        (async () => {
          try {
            for (const ticket of ticketsArray) {
              for (let i = 0; i < ticket.quantity; i++) {
                const ticketData = {
                  locaImageUrl: data.localisationImageUrl,
                  garantis_: data.garantis_,
                  email: data.email,
                  sexe: data.sexe,
                  total: data.totalA,
                  uid: data.uid,
                  name: data.name,
                  namePro: data.namePro,
                  etc... etc
    
                };


                const qr = await uniqueCodeCreate(ticketData);
                const publicUrl = await createAndUploadQRCode(ticketData, qr, 256, 'M');

                await reservationAndProDetailsToFirestore(ticketData, publicUrl, qr);
                await sendQrCodeEmail(ticketData, publicUrl, qr);
          
               
              }
            }
          } catch (error) {
            console.error("Erreur lors du traitement asynchrone:", error);
          }
        })();
      } else {
        res.status(400).send("Type d’événement non géré");
      }
    });
});

function extractDataFromSession(session) {
  return {
    localisationImageUrl: session.metadata.localisationImageUrl,
    garantis_: session.metadata.garantis_,
    email: session.metadata.email,
    sexe: session.metadata.sexe,
    total: session.metadata.total,
    uid: session.metadata.uid,
    etc..etc..etc
  };
}

  
  async function uniqueCodeCreate(data) {
      const prenom = data.name;
  
      const rng = Math.floor(Math.random() * ____);
      const qr = ____;

      return qr;
  }

  
  async function sendQrCodeEmail(data, publicUrl, qr) {
    try {
        
        const fraix = isNaN(parseFloat(data.ticketPriceFraix)) ? 0.0 : parseFloat(data.ticketPriceFraix);
        const amount = parseFloat(data.ticketPrice);
        const dateDebutTimestamp = admin.firestore.Timestamp.fromDate(new Date(data.dateDebut));
        const url = '---';

        // Envoi de la requête POST à l'API
        const response = await axios.post(url, {
          localisationImageUrl: "",
          resaID: qr,
          dateDebut: dateDebutTimestamp,
         etc...etc...
        });
  
        // Vérification de la réponse
        if (response.status !== 200) {
           
        
        }
        return { success: true, message: "Email sent successfully." };
  
    } catch (error) {

  
        let errorMessage = "Error sending QR code email";
        if (error.response) {
            console.error("Erreur dans la réponse:", error.response.status);
            console.error("Détails de l'erreur:", error.response.data);
            errorMessage += : Server responded with status: ${error.response.status};
        } else if (error.request) {
            console.error("Erreur dans la requête:", error.request);
            errorMessage += ": No response received";
        } else {
            console.error('Erreur lors de la configuration de la requête:', error.message);
            errorMessage += : Request setup failed: ${error.message};
        }
  
        return { success: false, message: errorMessage };
    }
  }
  

  
  
async function createAndUploadQRCode(data, qr, width = 256, errorCorrectionLevel = 'M') {
  try {
    const options = {
      color: { dark: "#000000", light: "#0000" },
      width: width,
      errorCorrectionLevel: errorCorrectionLevel,
    };
    const qrCodeBuffer = await QRCode.toBuffer(qr, options);

    
    const bucket = admin.storage().bucket();
    const filePath = EventsFile/${---}/${data.---}/---/${---}.png;
    const file = bucket.file(filePath);
    await file.save(qrCodeBuffer, { contentType: 'image/png', public: true });
    console.log("QR code uploadé.");

    const publicUrl = https://storage.googleapis/${bucket.name}/${file.name};
    return publicUrl;
  } catch (error) {
    throw error;
  }
}

  async function reservationAndProDetailsToFirestore(data, publicUrl, qr) {
  try {
    // Initialisation locale de Firestore
    const firebaseFirestore = firestore();
   
    const fraix = isNaN(parseFloat(data.ticketPriceFraix)) ? 0.0 : parseFloat(data.ticketPriceFraix);
    const amount = parseFloat(data.ticketPrice);
    const dateDebutTimestamp = admin.firestore.Timestamp.fromDate(new Date(data.dateDebut));
    const dateFinTimestamp = admin.firestore.Timestamp.fromDate(new Date(data.dateFin));
    const now = new Date();

    const resaModel = {
     ...//...//...
    };
 
    const resaDetails = {
  ...//...//...
    };
  
    const historiqueDetails = {
  ...//...//...
    };
    const clientInfo = {
    ...//...//...
    };

    const historiqueClientDetails = {
      ...//...//...
    };
   
    const postRef = firebaseFirestore
      .collection("--")
      .doc(--)
      .collection("--")
      .doc(--);

    const postUpdateData = {
      '--': admin.firestore.FieldValue.increment(amount),
      [--]: firestore.FieldValue.increment(-1),
      [--]: firestore.FieldValue.increment(1)
    };
    if (data.sexe === '--') {
      postUpdateData['--'] = firestore.FieldValue.increment(1);
    } else if (data.-- === '--') {
      postUpdateData['--'] = firestore.FieldValue.increment(1);
    }

    const batch = firebaseFirestore.batch();

    // Ajout des écritures dans le batch :
    batch.set(
      firebaseFirestore.collection("--").doc(--).collection("-- --").doc(--),
      
    );
    batch.set(
      firebaseFirestore.collection("--").doc(--).collection("Reservation").doc(--),
      resaDetails
    );
    batch.set(
      firebaseFirestore.collection("--").doc(--).collection("-- --").doc(--),
      historiqueDetails
    );
    const clientDocRef = firebaseFirestore.collection("--").doc(--).collection("--").doc(--);
    batch.set(clientDocRef, clientInfo, { merge: true });
    batch.set(clientDocRef.collection("--").doc(--), historiqueClientDetails);
    
    batch.update(postRef, { ...postUpdateData, Like: admin.firestore.FieldValue.increment(1) });

    // Modification : retourner la promesse du commit
    return batchmit().then(() => {
      console.timeEnd("batchCommit");
      console.log("=== Fin de combinedReservationAndHistoryToFirestore ===");
      return { success: true, message: "" };
    });
  } catch (error) {
    console.error("", error);
    return { success: false, message: "" };
  }
}
Share Improve this question edited Mar 14 at 15:18 Frank van Puffelen 601k85 gold badges890 silver badges860 bronze badges Recognized by Google Cloud Collective asked Mar 14 at 6:20 Codeur 997Codeur 997 11 silver badge1 bronze badge
Add a comment  | 

1 Answer 1

Reset to default 1

Once a HTTP Cloud Function sends back a response to the caller, it is considered "finished". Any work attempted after sending back a response may never execute, and if it does, it is severely throttled as documented here and some network requests may be blocked causing additional problems.

In your case, you are telling the Cloud Functions runtime that your function is done when calling either of these lines:

res.status(200).send({ received: true });

or

res.status(400).send("Type d’événement non géré");

You should treat these lines as if they had return at the start, where no more code can run after that point.

If sending a response to the caller of your function in a quick and timely manner is important (as recommended by Stripe), accept the request, validate and save the relevant data to the Realtime Database or Cloud Firestore, and then send back the response to the caller. In a separate Cloud Function, configure it to listen to that database addition and then act on it accordingly performing whatever background tasks you need to do.


Importantly, the local Firebase Emulators do not replicate the throttling behaviour of deployed Cloud Functions. Because your local system isn't shutting down immediately after sending a response, you may wrongly assume that your function can perform work after sending a response.


Refactoring your code to prevent the throttling issue is beyond the scope of StackOverflow. However, the steps that you would need to perform include:

HTTP Request Handler

  1. Accept HTTP Request
  2. Validate/authenticate the request
  3. Save the request data to the Realtime Database or Cloud Firestore
  4. Send back the acknowledgement to the caller, ideally with information on where to get further status updates. (see the HTTP 202 Accepted flow for the general form of this)

Background Task

  1. Trigger a RTDB Cloud Function or Cloud Firestore Cloud Function based on the database call in step 3 above.
  2. Read the request data
  3. Perform the background tasks
  4. Update the database with the result (so that the user/admin can check if it has completed)

If you are expecting high volume and don't need task progress tracking, you could invoke a Pub/Sub Event Cloud Function instead as covered in this other thread. However, for something like Stripe billing, I would opt for the database approach for better auditability when you need to chase down things like duplicate payments.

发布评论

评论列表(0)

  1. 暂无评论