Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(backup): write xud db every 3 minutes max #1655

Merged
merged 2 commits into from
Jun 16, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 23 additions & 26 deletions lib/backup/Backup.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { createHash } from 'crypto';
import { EventEmitter } from 'events';
import fs from 'fs';
import path from 'path';
Expand All @@ -9,7 +8,9 @@ import { getDefaultBackupDir } from '../utils/utils';

interface Backup {
on(event: 'newBackup', listener: (path: string) => void): this;
on(event: 'changeDetected', listener: (client: string) => void): this;
emit(event: 'newBackup', path: string): boolean;
emit(event: 'changeDetected', client: string): boolean;
}

class Backup extends EventEmitter {
Expand All @@ -22,6 +23,18 @@ class Backup extends EventEmitter {
private lndClients: LndClient[] = [];
private checkLndTimer: ReturnType<typeof setInterval> | undefined;

/** A map of client names to a boolean indicating whether they have changed since the last backup. */
private databaseChangedMap = new Map<string, boolean>();

private xudBackupTimer = setInterval(() => {
if (this.databaseChangedMap.get('xud') === true) {
const backupPath = this.getBackupPath('xud');
const content = this.readDatabase(this.config.dbpath);
this.writeBackup(backupPath, content);
this.databaseChangedMap.set('xud', false);
}
}, 180000);

public start = async (args: { [argName: string]: any }) => {
await this.config.load(args);

Expand All @@ -48,13 +61,6 @@ class Backup extends EventEmitter {
this.logger.error(`Could not connect to LNDs: ${err}`);
});

// Start the Raiden database filewatcher
if (args.raiden) {
this.startFilewatcher('raiden', args.raiden.dbpath).catch(this.logger.error);
} else {
this.logger.warn('Raiden database file not specified');
}

// Start the XUD database filewatcher
this.startFilewatcher('xud', this.config.dbpath).catch(this.logger.error);

Expand All @@ -72,6 +78,8 @@ class Backup extends EventEmitter {
for (const lndClient of this.lndClients) {
lndClient.close();
}

clearInterval(this.xudBackupTimer);
}

private waitForLndConnected = (lndClient: LndClient) => {
Expand Down Expand Up @@ -138,15 +146,13 @@ class Backup extends EventEmitter {
}

private startFilewatcher = async (client: string, dbPath: string) => {
let previousDatabaseHash: string | undefined;
const backupPath = this.getBackupPath(client);

if (fs.existsSync(dbPath)) {
this.logger.verbose(`Writing initial ${client} database backup to: ${backupPath}`);
const { content, hash } = this.readDatabase(dbPath);
const content = this.readDatabase(dbPath);

this.writeBackup(backupPath, content);
previousDatabaseHash = hash;
} else {
this.logger.warn(`Could not find database file of ${client} at ${dbPath}, waiting for it to be created...`);
const dbDir = path.dirname(dbPath);
Expand All @@ -164,29 +170,19 @@ class Backup extends EventEmitter {

this.fileWatchers.push(fs.watch(dbPath, { persistent: true, recursive: false }, (event: string) => {
if (event === 'change') {
const { content, hash } = this.readDatabase(dbPath);

// Compare the MD5 hash of the current content of the file with hash of the content when
// it was backed up the last time to ensure that the content of the file has changed
if (hash !== previousDatabaseHash) {
this.logger.trace(`${client} database changed`);

previousDatabaseHash = hash;
this.writeBackup(backupPath, content);
}
this.logger.trace(`${client} database changed`);
this.emit('changeDetected', client);
this.databaseChangedMap.set(client, true);
}
}));

this.logger.verbose(`Listening for changes to the ${client} database`);
}

private readDatabase = (path: string): { content: Buffer, hash: string } => {
private readDatabase = (path: string) => {
const content = fs.readFileSync(path);

return {
content,
hash: createHash('md5').update(content).digest('base64'),
};
return content;
}

private writeBackup = (backupPath: string, data: Uint8Array) => {
Expand All @@ -195,6 +191,7 @@ class Backup extends EventEmitter {
backupPath,
data,
);
this.logger.trace(`new backup written to ${backupPath}`);
this.emit('newBackup', backupPath);
} catch (error) {
this.logger.error(`Could not write backup file: ${error}`);
Expand Down
78 changes: 10 additions & 68 deletions test/jest/Backup.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,18 +14,13 @@ const removeDir = (dir: string) => {

const backupdir = 'backup-test';

const raidenDatabasePath = 'raiden';
const xudDatabasePath = 'xud';

const backups = {
lnd: {
event: 'lnd event',
startup: 'lnd startup',
},
raiden: {
event: 'raiden event',
startup: 'raiden startup',
},
xud: {
event: 'xud event',
startup: 'xud startup',
Expand All @@ -35,7 +30,6 @@ const backups = {
let channelBackupCallback: any;

const onListenerMock = jest.fn((event, callback) => {

if (event === 'channelBackup') {
channelBackupCallback = callback;
} else {
Expand All @@ -61,29 +55,20 @@ describe('Backup', () => {
const backup = new Backup();

beforeAll(async () => {
await Promise.all([
fs.promises.writeFile(
raidenDatabasePath,
backups.raiden.startup,
),
fs.promises.writeFile(
xudDatabasePath,
backups.xud.startup,
),
]);
await fs.promises.writeFile(
xudDatabasePath,
backups.xud.startup,
);

await backup.start({
backupdir,
loglevel: 'error',
dbpath: xudDatabasePath,
raiden: {
dbpath: raidenDatabasePath,
},
});
});

afterAll(async () => {
await backup.stop();
afterAll(() => {
backup.stop();
});

test('should write LND backups on startup', () => {
Expand All @@ -106,39 +91,6 @@ describe('Backup', () => {
).toEqual(backups.lnd.event);
});

test('should write Raiden backups on startup', () => {
expect(
fs.readFileSync(
path.join(backupdir, 'raiden'),
'utf8',
),
).toEqual(backups.raiden.startup);
});

test('should write Raiden backups on new event', async () => {
fs.writeFileSync(
raidenDatabasePath,
backups.raiden.event,
);

// Wait to make sure the file watcher handled the new file
await new Promise((resolve, reject) => {
setTimeout(reject, 3000);
backup.on('newBackup', (path) => {
if (path.endsWith(raidenDatabasePath)) {
resolve();
}
});
});

expect(
fs.readFileSync(
path.join(backupdir, 'raiden'),
'utf8',
),
).toEqual(backups.raiden.event);
});

test('should write XUD database backups on startup', () => {
expect(
fs.readFileSync(
Expand All @@ -148,7 +100,7 @@ describe('Backup', () => {
).toEqual(backups.xud.startup);
});

test('should write XUD database backups on new event', async () => {
test('should detect XUD database backups on new event', async () => {
fs.writeFileSync(
xudDatabasePath,
backups.xud.event,
Expand All @@ -157,29 +109,19 @@ describe('Backup', () => {
// Wait to make sure the file watcher handled the new file
await new Promise((resolve, reject) => {
setTimeout(reject, 3000);
backup.on('newBackup', (path) => {
backup.on('changeDetected', (path) => {
if (path.endsWith(xudDatabasePath)) {
resolve();
}
});
});

expect(
fs.readFileSync(
path.join(backupdir, 'xud'),
'utf8',
),
).toEqual(backups.xud.event);
});

afterAll(async () => {
await backup.stop();
backup.stop();

removeDir(backupdir);

await Promise.all([
fs.promises.unlink(xudDatabasePath),
fs.promises.unlink(raidenDatabasePath),
]);
await fs.promises.unlink(xudDatabasePath);
});
});