skip to Main Content

I have created a function that takes in an array of URL strings for images stored in firebase storage, the function successfully works for instances where the total download amount is less than 1gb.

However with larger amounts the last console log i see is:

"All images streamed to zip"

and then nothing happens for a good few seconds and i get:

Function execution took 141568 ms, finished with status: ‘crash’

then:

**Error: read ECONNRESET
at TLSWrap.onStreamRead (node:internal/stream_base_commons:217:20)
at TLSWrap.callbackTrampoline (node:internal/async_hooks:130:17) **

I have looked around the internet for solutions however I could only find solutions from 2017 where people had advised that the google-cloud-node library’s handling of sockets, and the default socket timeout in the Cloud Functions environment have been fixed.

Here is my cloud function:

const functions = require('firebase-functions');
const fetch = require('node-fetch');
const archiver = require('archiver');
const fs = require('fs');
const path = require('path');
const os = require('os');
const admin = require('firebase-admin');
const { Storage } = require('@google-cloud/storage');

admin.initializeApp({
 credential: admin.credential.applicationDefault(),
 storageBucket: process.env.FIREBASE_STORAGE_BUCKET
});

const runtimeOpts = {
 timeoutSeconds: 300,
 memory: '8GB'
};

exports.batchDownload = functions
 .runWith(runtimeOpts)
 .https.onRequest(async (req, res) => {
    console.log('Function started');
    res.set('Access-Control-Allow-Origin', '*');
    res.set('Access-Control-Allow-Methods', 'POST');
    res.set('Access-Control-Allow-Headers', 'Content-Type');

    if (req.method === 'OPTIONS') {
      console.log('OPTIONS request received');
      res.status(204).send('');
      return;
    }

    const imageUrls = req.body.imageUrls;
    const inspectionId = req.body.id;

    if (!Array.isArray(imageUrls)) {
      console.log('Invalid request format');
      res.status(400).send('Invalid request: incorrect data format');
      return;
    }

    const tempDir = path.join(os.tmpdir(), 'images');
    const zipPath = path.join(os.tmpdir(), 'images.zip');

    if (!fs.existsSync(tempDir)) {
      console.log('Creating temporary directory');
      fs.mkdirSync(tempDir);
    }

    try {
      // Create zip archive
      const output = fs.createWriteStream(zipPath);
      const archive = archiver('zip', {
        zlib: { level: 9 },
      });

      archive.pipe(output);

      // Stream images directly to the zip file
      await Promise.all(imageUrls.map(async (url, index) => {
        console.log(`Streaming image from ${url}`);
        const response = await fetch(url);
        if (!response.ok) {
          throw new Error(`Failed to fetch image from ${url}`);
        }
        const stream = response.body;
        const filePath = `image${index}.jpg`;
        archive.append(stream, { name: filePath });
      }));

      console.log('All images streamed to zip');

      // Finalize the archive
      await archive.finalize();

      // Upload zip file to Cloud Storage
      const storage = new Storage();
      const bucket = storage.bucket(process.env.FIREBASE_STORAGE_BUCKET);
      const file = bucket.file(`inspections/${inspectionId}/images.zip`);
      const stream = file.createWriteStream({
        metadata: {
          contentType: 'application/zip',
        },
      });

      await new Promise((resolve, reject) => {
        stream.on('error', reject);
        stream.on('finish', resolve);
        fs.createReadStream(zipPath).pipe(stream);
      });

      console.log('Zip file uploaded to Cloud Storage');

      // Generate download URL
      const config = {
        action: 'read',
        expires: Date.now() + 60 * 60 * 1000, // Set the expiration to 1 hour from now
      };
      const [url] = await file.getSignedUrl(config);
      console.log('Generated download URL:', url);

      // Send response with download URL
      res.status(200).send({ downloadUrl: url });
    } catch (error) {
      console.error('Error during batch download:', error);
      res.status(500).send('Error during batch download');
    } finally {
      // Cleanup temporary files
      fs.rmdirSync(tempDir, { recursive: true });
      fs.unlinkSync(zipPath);
    }
 });

and here is my front end async function:

 const downloadAllImages = async () => {
   if (imagesToDownload.length < 1) {
      return;
   }
  
   const imageUrls = imagesToDownload.map(image => image.downloadURL);
   const zipFilePath = `inspections/${inspection.id}/images.zip`;
  
   try {
      const fileExists = await checkIfFileExists(zipFilePath);
      if (!fileExists) {
        // If the file does not exist, call the Cloud Function to generate it
        setIsDownloading(true);
        const response = await fetch(CLOUD_FUNCTION_URL, {
          method: 'POST',
          headers: {
            'Content-Type': 'application/json',
          },
          body: JSON.stringify({ imageUrls, id: inspection.id }),
        });
  
        if (!response.ok) {
          throw new Error(`Failed to initiate image download: ${response.statusText}`);
        }
  
        const data = await response.json();
        console.log("reponse", data)
        const downloadUrl = data.downloadUrl;
  
        const a = document.createElement('a');
        a.href = downloadUrl;
        a.download = 'images.zip';
        a.click();
  
        setShowDownloadAllImagesModal(false);
        showNotification("Images Downloaded!");
        setIsDownloading(false);
      } else {
        const storage = getStorage();
        const zipRef = ref(storage, zipFilePath);
  
        getDownloadURL(zipRef)
        .then((url) => {
          const a = document.createElement('a');
          a.href = url;
          a.download = 'images.zip';
          a.click();
        })
        .catch((error) => {
          console.error('Error downloading file:', error);
          showNotification(`Error downloading file: ${error}`);
        });
        setShowDownloadAllImagesModal(false);
        showNotification("Images Downloaded!");
        setIsDownloading(false);
      }
   } catch (error) {
      console.error(`Error checking file existence or initiating image download: ${error}`);
      setShowDownloadAllImagesModal(false);
      showNotification(`${error}`);
      setIsDownloading(false);
   }
  };

I have set this up as a 1st Gen Cloud function and it works in instances totalling less than 1GB, but if i try 3GB – 9GB worth of image files I run into the ECONNRESET problem.

Is anyone able to give any insight or advise further troubleshooting please?

Thanks for taking the time to read this.

2

Answers


  1. Google cloud functions used to advertise having a hard limit of 256mb temporary storage, though I cannot find any specific reference to this now. I imagine you are crashing the function as the /tmp directory is surpassing the maximum permitted size. For context, when you download a file in your code, its stored in the tmp directory on the function. You must then move it to somewhere where it can persist.

    Consider changing your approach, you could stream the download direct to google cloud storage with google-cloud/storage package. Or just chunk your downloads into smaller more manageable batches, by chunking your array of urls.

    Login or Signup to reply.
  2. Following below possible solution could help you to solve the issue:

    1. Verify if the function finished without terminating all operations. The operations that still work in the background don’t have enough CPU to maintain network connections and fail. You can check this document about do_not_start_background_activities

    2. Verify that you are returning the Promises properly. Note: Remember that in a non-HTTP trigger Function, must return either a Promise, an Object or a null explicitly.

    3. If you are sending requests (like the requests library) make sure that the Keep-alive connection is disabled since this might cause the Function to reuse a socket that is in a bad state. If you don’t have access to this configuration on whichever library you are using (like google-cloud-node) then you will have to handle retries manually.

    4. If it seems that the code is properly returning the Promise and the Promise that is being returned is not related to the Firebase SDK wrap the return of the Promise with a new Promise.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search