Page MenuHomePhabricator

No OneTemporary

diff --git a/server/src/cron/backups.js b/server/src/cron/backups.js
index e971e3f47..9a0ff8372 100644
--- a/server/src/cron/backups.js
+++ b/server/src/cron/backups.js
@@ -1,124 +1,172 @@
// @flow
import childProcess from 'child_process';
import dateFormat from 'dateformat';
import fs from 'fs';
import invariant from 'invariant';
import StreamCache from 'stream-cache';
import { promisify } from 'util';
import zlib from 'zlib';
import dbConfig from '../../secrets/db_config';
const readdir = promisify(fs.readdir);
const lstat = promisify(fs.lstat);
const unlink = promisify(fs.unlink);
let importedBackupConfig = undefined;
async function importBackupConfig() {
if (importedBackupConfig !== undefined) {
return importedBackupConfig;
}
try {
// $FlowFixMe
const backupExports = await import('../../facts/backups');
if (importedBackupConfig === undefined) {
importedBackupConfig = backupExports.default;
}
} catch {
if (importedBackupConfig === undefined) {
importedBackupConfig = null;
}
}
return importedBackupConfig;
}
async function backupDB() {
const backupConfig = await importBackupConfig();
if (!backupConfig || !backupConfig.enabled) {
return;
}
+ const dateString = dateFormat('yyyy-mm-dd-HH:MM');
+ const filename = `squadcal.${dateString}.sql.gz`;
+ const filePath = `${backupConfig.directory}/${filename}`;
+
const mysqlDump = childProcess.spawn(
'mysqldump',
[
'-u',
dbConfig.user,
`-p${dbConfig.password}`,
'--single-transaction',
dbConfig.database,
],
{
stdio: ['ignore', 'pipe', 'ignore'],
},
);
const cache = new StreamCache();
- mysqlDump.stdout.pipe(zlib.createGzip()).pipe(cache);
-
- const dateString = dateFormat('yyyy-mm-dd-HH:MM');
- const filename = `${backupConfig.directory}/squadcal.${dateString}.sql.gz`;
+ mysqlDump.on('error', (e: Error) => {
+ console.warn(`error trying to spawn mysqldump for ${filename}`, e);
+ });
+ mysqlDump.on('exit', (code: number | null, signal: string | null) => {
+ if (signal !== null && signal !== undefined) {
+ console.warn(`mysqldump received signal ${signal} for ${filename}`);
+ } else if (code !== null && code !== 0) {
+ console.warn(`mysqldump exited with code ${code} for ${filename}`);
+ }
+ });
+ mysqlDump.stdout
+ .on('error', (e: Error) => {
+ console.warn(`mysqldump stdout stream emitted error for ${filename}`, e);
+ })
+ .pipe(zlib.createGzip())
+ .on('error', (e: Error) => {
+ console.warn(`gzip transform stream emitted error for ${filename}`, e);
+ })
+ .pipe(cache);
try {
- await saveBackup(filename, cache);
+ await saveBackup(filename, filePath, cache);
} catch (e) {
- await unlink(filename);
+ console.warn(`saveBackup threw for ${filename}`, e);
+ await unlink(filePath);
}
}
async function saveBackup(
+ filename: string,
filePath: string,
cache: StreamCache,
retries: number = 2,
): Promise<void> {
try {
- await trySaveBackup(filePath, cache);
+ await trySaveBackup(filename, filePath, cache);
} catch (e) {
if (e.code !== 'ENOSPC') {
throw e;
}
if (!retries) {
throw e;
}
await deleteOldestBackup();
- await saveBackup(filePath, cache, retries - 1);
+ await saveBackup(filename, filePath, cache, retries - 1);
}
}
-function trySaveBackup(filePath: string, cache: StreamCache): Promise<void> {
+const backupWatchFrequency = 60 * 1000;
+function trySaveBackup(
+ filename: string,
+ filePath: string,
+ cache: StreamCache,
+): Promise<void> {
+ const timeoutObject: {| timeout: ?TimeoutID |} = { timeout: null };
+ const setBackupTimeout = (alreadyWaited: number) => {
+ timeoutObject.timeout = setTimeout(() => {
+ const nowWaited = alreadyWaited + backupWatchFrequency;
+ console.log(
+ `writing backup for ${filename} has taken ${nowWaited}ms so far`,
+ );
+ setBackupTimeout(nowWaited);
+ }, backupWatchFrequency);
+ };
+ setBackupTimeout(0);
+
const writeStream = fs.createWriteStream(filePath);
return new Promise((resolve, reject) => {
- cache.pipe(writeStream).on('finish', resolve).on('error', reject);
+ cache
+ .pipe(writeStream)
+ .on('finish', () => {
+ clearTimeout(timeoutObject.timeout);
+ resolve();
+ })
+ .on('error', (e: Error) => {
+ clearTimeout(timeoutObject.timeout);
+ console.warn(`write stream emitted error for ${filename}`, e);
+ reject(e);
+ });
});
}
async function deleteOldestBackup() {
const backupConfig = await importBackupConfig();
invariant(backupConfig, 'backupConfig should be non-null');
const files = await readdir(backupConfig.directory);
let oldestFile;
for (let file of files) {
if (!file.endsWith('.sql.gz') || !file.startsWith('squadcal.')) {
continue;
}
const stat = await lstat(`${backupConfig.directory}/${file}`);
if (stat.isDirectory()) {
continue;
}
if (!oldestFile || stat.mtime < oldestFile.mtime) {
oldestFile = { file, mtime: stat.mtime };
}
}
if (!oldestFile) {
return;
}
try {
await unlink(`${backupConfig.directory}/${oldestFile.file}`);
} catch (e) {
// Check if it's already been deleted
if (e.code !== 'ENOENT') {
throw e;
}
}
}
export { backupDB };

File Metadata

Mime Type
text/x-diff
Expires
Mon, Dec 23, 10:38 AM (18 h, 1 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
2690781
Default Alt Text
(5 KB)

Event Timeline