首页 > 解决方案 > Restify:复制文件并使用承诺链查询数据库时出现套接字挂断错误

问题描述

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

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

主恢复文件

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(用于快速查询restify API)

'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'
});

如果向 localhost 发出请求,则会引发“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”错误。

标签: mysqlnode.jspromisefsrestify

解决方案


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

正确答案

TL;博士

PM2 使用 API 提交并保存的每个新文件重新启动 NodeJS 服务。解决方法:告诉 PM2 忽略存储 API 文件的目录。看到这个答案

长答案

虽然 OP 没有提及,但我的设置使用 PM2 作为应用程序的 NodeJS 服务管理器,并且我打开了“监视和重新加载”功能,该功能会在每次文件更改时重新启动服务。不幸的是,我忘记指示 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);
        });
});

推荐阅读