node.jspostgresqlexpresslargeobject

how to download larger binary object from postgres using nodejs express?


I have node js rest api script to query from Postgres tables. It's working fine, but When I try to fetch a large objects using "lo_get" of Postgres, I get

JavaScript heap out of memory

Below is the toy example.

index.js

const express = require('express')
const bodyParser = require('body-parser')
const app = express()
const port = 8000
var controller= require('./src/controller');

app.use(bodyParser.json())
app.use(
  bodyParser.urlencoded({
    extended: true,
  })
)

app.use('/cont', controller);

app.listen(port, () => {
  console.log(`App running on port ${port}.`)
})

controller.js

var express = require('express');
var router = express.Router();

router.get('/fileID/:lo_oid/', function( req, res) {
    const lo_oid =  parseInt(req.params.lo_oid)
    model.exportLoSaint(lo_oid, function (err, object) {
        if (err) return res.send(err);
   #  Fixed the typo
   #     response.status(200).file(file);
        res.status(200).objects;
    });
});

module.exports = router;

Model.js

const Pool = require('pg').Pool
var exportLoSaintQuery= "SELECT lo_get($1)"

const exportLoSaint = (lo_oid, callback) => {
    pool.query( exportLoSaintQuery ,[lo_oid], (error, results) => {
                if (error) {
                    callback(error);
                }
                else {
                    callback(results.rows)
                }
            })
        }

on sending request .. http://ipaddress:8000/cont/fileID/<fileID>/ getting below error.


<--- JS stacktrace --->

==== JS stack trace =========================================

    0: ExitFrame [pc: 0x1b2cbd65be1d]
Security context: 0x2bf3bbc08bd9 <JSObject>
    1: toJSON [0x259ded584f29] [buffer.js:~979] [pc=0x1b2cbd665145](this=0x306d2be17731 <Uint8Array map = 0x9dd44699a69>)
    2: arguments adaptor frame: 1->0
    3: InternalFrame [pc: 0x1b2cbd60ee75]
    4: EntryFrame [pc: 0x1b2cbd6092c1]
    5: builtin exit frame: stringify(this=0x0a6855c58f61 <Object map = 0x428c84042a9>,0x2160e26026f1 <undefined>,0x2160e...

FATAL ERROR: CALL_AND_RETRY_LAST Allocation failed - JavaScript heap out of memory
 1: 0x56228da912e4 node::Abort() [node]
 2: 0x56228da91332  [node]
 3: 0x56228dc86d4a v8::Utils::ReportOOMFailure(v8::internal::Isolate*, char const*, bool) [node]
 4: 0x56228dc86fc5 v8::internal::V8::FatalProcessOutOfMemory(v8::internal::Isolate*, char const*, bool) [node]
 5: 0x56228e0387d6  [node]
 6: 0x56228e04e018 v8::internal::Heap::AllocateRawWithRetryOrFail(int, v8::internal::AllocationSpace, v8::internal::AllocationAlignment) [node]
 7: 0x56228e01a63b v8::internal::Factory::AllocateRawArray(int, v8::internal::PretenureFlag) [node]
 8: 0x56228e01aad8 v8::internal::Factory::NewFixedArrayWithFiller(v8::internal::Heap::RootListIndex, int, v8::internal::Object*, v8::internal::PretenureFlag) [node]
 9: 0x56228dfe48fe  [node]
10: 0x56228dfe4ad3  [node]
11: 0x56228e143566 v8::internal::JSObject::AddDataElement(v8::internal::Handle<v8::internal::JSObject>, unsigned int, v8::internal::Handle<v8::internal::Object>, v8::internal::PropertyAttributes, v8::internal::ShouldThrow) [node]
12: 0x56228e17897e v8::internal::Object::SetProperty(v8::internal::LookupIterator*, v8::internal::Handle<v8::internal::Object>, v8::internal::LanguageMode, v8::internal::Object::StoreFromKeyed) [node]
13: 0x56228e2c1299 v8::internal::Runtime::SetObjectProperty(v8::internal::Isolate*, v8::internal::Handle<v8::internal::Object>, v8::internal::Handle<v8::internal::Object>, v8::internal::Handle<v8::internal::Object>, v8::internal::LanguageMode) [node]
14: 0x56228e0a9b45 v8::internal::Runtime_KeyedStoreIC_Slow(int, v8::internal::Object**, v8::internal::Isolate*) [node]
15: 0x1b2cbd65be1d 

The size of binary object in postgres is only 128 mb. So, I increased the size of node js to 1gb, it did not help. Other solution I found was not using express to download binary from postgres.

******************* update 1 *********** ************

Model.js

const exportLoSaintForMaster = (oid, fileName, callback) => {
    var fileStream = require('fs').createWriteStream(fileName);
    var man = new LargeObjectManager({ pg: pool });
    pool.query('BEGIN', function (err, result) {
        if (err) {
            callback(err);
            pool.emit('error', err);
        }
        console.log("\nthe oid : %d\n", oid)
        var bufferSize = 16384;
        man.openAndReadableStream(oid, bufferSize, function (err, size, stream) {
            if (err) {
                callback(err);
                return console.error('Unable to read the given large object', err);
            }

            console.log('Streaming a large object with a total size of', size);
            stream.on('end', function () {
                pool.query('COMMIT', callback);
            });

            // Store it as an image

            stream.pipe(fileStream);
        });
    });
    callback(fileStream)
};

The scripts waits for a long time and then error outs with the below message ..


        response.status(200).objects;
        ^

ReferenceError: response is not defined



******************* Update 2 *********************

After fixing the typo above error is no more. yet I see some problems.. as the client-side is not able to download it. it shows below error on client side ..

i tried to do get . I get below message

{"_writableState":{"objectMode":false,"highWaterMark":16384,"finalCalled":false,"needDrain":false,"ending":false,"ended":false,"finished":false,"destroyed":false,"decodeStrings":true,"defaultEncoding":"utf8","length":0,"writing":false,"corked":0,"sync":true,"bufferProcessing":false,"writecb":null,"writelen":0,"bufferedRequest":null,"lastBufferedRequest":null,"pendingcb":0,"prefinished":false,"errorEmitted":false,"emitClose":false,"autoDestroy":false,"bufferedRequestCount":0,"corkedRequestsFree":{"next":null,"entry":null}},"writable":true,"_events":{},"_eventsCount":0,"path":"myfile1","fd":null,"flags":"w","mode":438,"autoClose":true,"bytesWritten":0,"closed":false}

I am using a postman to download it. I think the issue may be because of the way controller.js is invoking the script in model.js .

Please suggest me

******************* Update 3 ****************

controller.js


router.get('/fileID/:lo_oid/fileName/:fileName', function (req, res) {
    const oid = parseInt(req.params.lo_oid)
    const fileName = req.params.fileName
    model.exportLoSaintForMaster(oid,fileName, (err, stream) => stream.pipe(res));
});

model.js

const exportLoSaintForMaster = (oid, fileName, callback) => {
    var fileStream = require('fs').createWriteStream(fileName);
    var man = new LargeObjectManager({ pg: pool });
    pool.query('BEGIN', function (err, result) {
        if (err) {
            callback(err);
            pool.emit('error', err);
        }
        console.log("\nthe oid : %d\n", oid)
        var bufferSize = 16384;
        man.openAndReadableStream(oid, bufferSize, function (err, size, stream) {
            if (err) {
                callback(err);
                return console.error('Unable to read the given large object', err);
            }

            console.log('Streaming a large object with a total size of', size);
            stream.on('end', function () {
                pool.query('COMMIT', callback);
            });

            // Store it as an image

            stream.pipe(fileStream);
        });
    });
    callback(null, fileStream)
}; 

module.exports = {

    exportLoSaintForMaster
}

getting below error

Error [ERR_STREAM_CANNOT_PIPE]: Cannot pipe, not readable
    at WriteStream.Writable.pipe (_stream_writable.js:243:24)
    ...
    ...
    

Solution

  • As you've mentioned you could increase NodeJS heap size witht the flag -max-old-space-size, for example set it to 4GB :

     node --max-old-space-size=4096 index.js
    

    But it's not a proper solution. The right thing to do would be to handle it with streams as Aviv Lo mentioned it in the comment. It will allow you to reduce the memory footprint of your script by handling data chunk by chunk.

    For handling PostgreSQL Large Objects, you can use node-pg-large-objects (npm i -s pg-large-object) :

    const { LargeObjectManager } = require('pg-large-object');
    const { createWriteStream } = require('fs');
    
    router.get('/fileID/:lo_oid/', (req, res, next) => {
    
        // assuming db is your Database connection
        // When working with Large Objects, always use a transaction                                                                                                                                              
        db.tx(tx => {
            const man = new LargeObjectManager({pgPromise: tx});
            const lo_oid = parseInt(req.params.lo_oid);
    
            const bufferSize = 16384; // the size of each chunk, customize it                                                                                                                                                                     
    
            return man.openAndReadableStreamAsync(oid, bufferSize)
                .then(([size, stream]) => {
                    stream.pipe(res); // It will take care of binding to data and end events.                                                                                                                                                     
                });
        }).catch(error => {
            next(error); // pass to express error handler                                                                                                                                                                                         
        });
    
    }
    

    This way you will handle your large object as a stream and pipe it to your client for download.

    With this solution, the maximum heap size occupied by this part of your script would be the bufferSize and not the whole file size as previously.


    Update 3 edit

    In your controller :

    router.get('/fileID/:lo_oid/fileName/:fileName', function (req, res, next) {
        const oid = parseInt(req.params.lo_oid);
        const fileName = req.params.fileName;
        res.header('Content-Disposition', `attachment; filename="${fileName}"`);
    
        model.exportLoSaintForMaster(oid, (err, stream) => { 
            if (err) return next(err);
            stream.pipe(res);
        });
    });
    

    In your model :

    const exportLoSaintForMaster = (oid, callback) => {
        const man = new LargeObjectManager({ pg: pool });
        pool.query('BEGIN', function (err, result) {
            if (err) {
                callback(err);
                pool.emit('error', err);
            }
            console.log("\nthe oid : %d\n", oid)
            const bufferSize = 16384;
            man.openAndReadableStream(oid, bufferSize, function (err, size, stream) {
                if (err) {
                    return callback(err);
                }
    
                console.log(`Streaming a large object with a total size of ${size}`);
                stream.on('end', _ => pool.query('COMMIT'));
    
                callback(null, stream);
            });
        });
    }; 
    

    Warning : I don't know how your pool object works, so this code may not work exactly as expected.