Page Menu
Home
Phabricator
Search
Configure Global Search
Log In
Files
F3510024
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
5 KB
Referenced Files
None
Subscribers
None
View Options
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
Details
Attached
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)
Attached To
Mode
rCOMM Comm
Attached
Detach File
Event Timeline
Log In to Comment