mysql - 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”错误。
解决方案
尽管解决方案揭示了一个新手错误,但我觉得我应该编辑这个答案,希望它可以帮助其他人。为了完全透明,我将保留下面的先前答案,但请不要认为它不正确。
正确答案
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);
});
});
推荐阅读
- php - 将多维php数组转换为json数组
- .net - 无法使 .NET Core POST 请求正常工作
- android - 是否可以使用 java 1.8 构建 AOSP 棒棒糖?
- keras - keras 中这两个卷积层之间的区别在哪里?
- nginx - kubernetes nginx 入口并发连接
- android - 如何获取未使用的消息数
- python - Turtle.onkeypress 让我感到困惑。为什么它不起作用?
- angular - Angular 6 - 将事件处理程序添加到动态创建的 html 元素
- c++ - 用于在 PowerPC 中执行系统调用的通用 C/C++ 函数
- javascript - 有谁知道为什么这个错误在几分钟后再次发生并且很糟糕