Restify:使用承诺链复制文件和查询数据库时发生套接字挂起错误

Restify: socket hangup error when copying a file and querying a database using a promise chain

我正在使用 restify 框架构建一个小型应用程序,它将上传的文件从其临时位置复制到永久位置,然后将该新位置插入到 MySQL 数据库中。但是,当尝试复制文件然后 运行 promisified 查询时,系统会抛出一个未被 promise 链捕获的静默错误,从而导致 Web 服务器端出现 502 错误。下面是一个最小的工作示例。这个例子已经过测试,确实失败了。

如果过程中的某个步骤被删除(复制文件或将字符串存储在数据库中),则静默错误消失并发送 API 响应。但是,这两个步骤都需要用于以后的文件检索。

主 Restify 文件

const restify = require('restify');
const corsMiddleware = require('restify-cors-middleware');
const cookieParser = require('restify-cookies');

const DataBugsDbCredentials = require('./config/config').appdb;
const fs = require('fs');
const { host, port, name, user, pass } = DataBugsDbCredentials;
const database = new (require('./lib/database'))(host, port, name, user, pass);

const server = restify.createServer({
    name: 'insect app'
});

// enable options response in restify (anger) -- this is so stupid!! (anger)
const cors = corsMiddleware({});
server.pre(cors.preflight);
server.use(cors.actual);

// set query and body parsing for access to this information on requests
server.use(restify.plugins.acceptParser(server.acceptable));
server.use(restify.plugins.queryParser({ mapParams: true }));
server.use(restify.plugins.bodyParser({ mapParams: true }));
server.use(cookieParser.parse);


server.post('/test', (req, res, next) => {
    const { files } = req;

    let temporaryFile = files['file'].path;
    let permanentLocation = '/srv/www/domain.com/permanent_location';

    // copy file 
    return fs.promises.copyFile(temporaryFile, permanentLocation)

        // insert into database
        .then(() => database.query(
            `insert into Specimen (
                CollectorId,
                HumanReadableId,
                FileLocation
            ) values (
                1,
                'AAA004',
                ${permanentLocation}
            )`
        ))
        .then(() => {
            console.log('success!!!')
            return res.send('success!')
        })
        .catch(error => {
            console.error(error)
            return res.send(error);
        });
});

./lib/database.js

'use strict';

const mysql = require('mysql2');

class Database {
    constructor(host, port, name, user, pass) {
        this.connection = this.connect(host, port, name, user, pass);
        this.query = this.query.bind(this);
    }

    /**
     * Connects to a MySQL-compatible database, returning the connection object for later use
     * @param {String} host The host of the database connection
     * @param {Number} port The port for connecting to the database
     * @param {String} name The name of the database to connect to
     * @param {String} user The user name for the database
     * @param {String} pass The password for the database user
     * @return {Object} The database connection object
     */
    connect(host, port, name, user, pass) {
        let connection = mysql.createPool({
            connectionLimit : 20,
            host            : host,
            port            : port,
            user            : user,
            password        : pass,
            database        : name,
            // debug           : true
        });

        connection.on('error', err => console.error(err));
        return connection;
    }

    /**
     * Promisifies database queries for easier handling
     * @param {String} queryString String representing a database query
     * @return {Promise} The results of the query
     */
    query(queryString) {
        // console.log('querying database');
        return new Promise((resolve, reject) => {
            // console.log('query promise before query, resolve', resolve);
            // console.log('query promise before query, reject', reject);
            // console.log('query string:', queryString)
            this.connection.query(queryString, (error, results, fields) => {
                console.log('query callback', queryString);
                console.error('query error', error, queryString);
                if (error) {
                    // console.error('query error', error);
                    reject(error);
                } else {
                    // console.log('query results', results);
                    resolve(results);
                }
            });
        });
    }
}

module.exports = Database;

./testfile.js(用于快速查询restifyAPI)

'use strict';

const fs = require('fs');
const request = require('request');

let req = request.post({
    url: 'https://api.databugs.net/test',
}, (error, res, addInsectBody) => {
    if (error) {
        console.error(error);
    } else {
        console.log('addInsectBody:', addInsectBody);
    }
});
let form = req.form();
form.append('file', fs.createReadStream('butterfly.jpg'), {
    filename: 'butterfly.jpg',
    contentType: 'multipart/form-data'
});

如果向本地主机发出请求,则会抛出 'ECONNRESET' 错误,如下所示:

Error: socket hang up
    at connResetException (internal/errors.js:570:14)
    at Socket.socketOnEnd (_http_client.js:440:23)
    at Socket.emit (events.js:215:7)
    at endReadableNT (_stream_readable.js:1183:12)
    at processTicksAndRejections (internal/process/task_queues.js:80:21) {
  code: 'ECONNRESET'
}

仅当数据库和文件 I/O 都存在于承诺链中时才会抛出此错误。此外,如果首先发出数据库请求,然后文件 I/O 发生,则不会发生该错误;但是,对服务器的另一个快速请求将立即导致 'ECONNRESET' 错误。

您当时没有处理数据库承诺并赶上 -

主 Restify 文件


const restify = require('restify');
const corsMiddleware = require('restify-cors-middleware');
const cookieParser = require('restify-cookies');

const DataBugsDbCredentials = require('./config/config').appdb;
const fs = require('fs');
const { host, port, name, user, pass } = DataBugsDbCredentials;
const database = new (require('./lib/database'))(host, port, name, user, pass);

const server = restify.createServer({
    name: 'insect app'
});

// enable options response in restify (anger) -- this is so stupid!! (anger)
const cors = corsMiddleware({});
server.pre(cors.preflight);
server.use(cors.actual);

// set query and body parsing for access to this information on requests
server.use(restify.plugins.acceptParser(server.acceptable));
server.use(restify.plugins.queryParser({ mapParams: true }));
server.use(restify.plugins.bodyParser({ mapParams: true }));
server.use(cookieParser.parse);


server.post('/test', (req, res, next) => {
    const { files } = req;

    let temporaryFile = files['file'].path;
    let permanentLocation = '/srv/www/domain.com/permanent_location';

    // copy file 
    return fs.promises.copyFile(temporaryFile, permanentLocation)

        // insert into database
        .then(() =>{ 
                // Your database class instance query method returns promise 
                database.query(
                `insert into Specimen (
                    CollectorId,
                    HumanReadableId,
                    FileLocation
                ) values (
                    1,
                    'AAA004',
                    ${permanentLocation}
                )`
                ).then(() => {
                    console.log('success!!!')
                    return res.send('success!')
                })
                .catch(error => {
                    console.error('Inner database promise error', error)
                    return res.send(error);
                });
            }).catch(error => {
                console.error('Outer fs.copyfile promise error', error)
                return res.send(error);
            })

});

我觉得我应该编辑这个答案,尽管解决方案揭示了一个菜鸟错误,希望它可以帮助其他人。为了完全透明,我将保留之前的答案,但请注意它不正确。

正确答案

TL;DR

PM2 重新启动 NodeJS 服务,每个新文件提交并由 API 保存。修复:告诉 PM2 忽略存储 API 文件的目录。看到这个

长答案

虽然 OP 没有提到它,但我的设置使用 PM2 作为应用程序的 NodeJS 服务管理器,并且我打开了 'watch & reload' 功能,该功能在每次文件更改时重新启动服务。不幸的是,我忘记指示 PM2 忽略存储通过 API 提交的新文件的子目录中的文件更改。因此,每个提交到 API 的新文件都会导致服务重新加载。如果在存储文件后还有更多指令需要执行,它们将在 PM2 重新启动服务时终止。 502 网关错误是 NodeJS 服务在此期间暂时不可用的简单结果。

将数据库事务更改为首先发生(如下面错误描述的解决方案)只是确保服务重启发生在最后没有其他指令挂起时。

上一个不正确答案

到目前为止,我找到的唯一解决方案是切换文件 I/O 和数据库查询,以便文件 I/O 操作排在最后。此外,将文件 I/O 操作更改为重命名而不是复制文件可以防止快速连续的 API 查询抛出相同的错误(在任何文件 I/O 操作之后快速进行数据库查询不是重命名似乎是问题)。可悲的是,我对 OP 中的套接字挂起没有合理的解释,但下面是 OP 中的代码修改后使其正常运行。

const restify = require('restify');
const corsMiddleware = require('restify-cors-middleware');
const cookieParser = require('restify-cookies');

const DataBugsDbCredentials = require('./config/config').appdb;
const fs = require('fs');
const { host, port, name, user, pass } = DataBugsDbCredentials;
const database = new (require('./lib/database'))(host, port, name, user, pass);

const server = restify.createServer({
    name: 'insect app'
});

// enable options response in restify (anger) -- this is so stupid!! (anger)
const cors = corsMiddleware({});
server.pre(cors.preflight);
server.use(cors.actual);

// set query and body parsing for access to this information on requests
server.use(restify.plugins.acceptParser(server.acceptable));
server.use(restify.plugins.queryParser({ mapParams: true }));
server.use(restify.plugins.bodyParser({ mapParams: true }));
server.use(cookieParser.parse);


server.post('/test', (req, res, next) => {
    const { files } = req;

    let temporaryFile = files['file'].path;
    let permanentLocation = '/srv/www/domain.com/permanent_location';

    // copy file 
    // insert into database
    return database.query(
            `insert into Specimen (
                CollectorId,
                HumanReadableId,
                FileLocation
            ) values (
                1,
                'AAA004',
                ${permanentLocation}
            )`
        )
        .then(() => fs.promises.rename(temporaryFile, permanentLocation))
        .then(() => {
            console.log('success!!!')
            return res.send('success!')
        })
        .catch(error => {
            console.error(error)
            return res.send(error);
        });
});