Skip to content

Commit

Permalink
feat: lock file
Browse files Browse the repository at this point in the history
This introduces a lock file for xud that is created in the xud data
directory any time xud starts up. Anytime a lock file already exists,
an error message is logged and xud exits. This prevents multiple
xud processes from trying to use the same node key or directory at the
same time.

The lock files are unique to each network, so running multiple processes
with different networks (e.g. simnet and mainnet) is allowed. They are
deleted when xud shuts down.

Closes #1989.
  • Loading branch information
sangaman committed Dec 2, 2020
1 parent 7ecdd7e commit 008f899
Show file tree
Hide file tree
Showing 5 changed files with 41 additions and 18 deletions.
31 changes: 27 additions & 4 deletions lib/Xud.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,9 @@ class Xud extends EventEmitter {
public service!: Service;
private logger!: Logger;
private config: Config;
private db!: DB;
private pool!: Pool;
private orderBook!: OrderBook;
private db?: DB;
private pool?: Pool;
private orderBook?: OrderBook;
private rpcServer?: GrpcServer;
private httpServer?: HttpServer;
private grpcAPIProxy?: GrpcWebProxyServer;
Expand All @@ -46,6 +46,7 @@ class Xud extends EventEmitter {
private swapClientManager?: SwapClientManager;
private unitConverter?: UnitConverter;
private simnetChannels$?: Subscription;
private lockFilePath?: string;

/**
* Create an Exchange Union daemon.
Expand Down Expand Up @@ -88,6 +89,24 @@ class Xud extends EventEmitter {
}

try {
if (this.config.instanceid === 0) {
// if we're not running with multiple instances as indicated by an instance id of 0
// create a lock file to prevent multiple xud instances from trying to run
// with the same node key and/or database
const lockFilePath = path.join(this.config.xudir, `${this.config.network}.lock`);
try {
await (await fs.open(lockFilePath, 'wx')).close();
this.lockFilePath = lockFilePath;
} catch (err) {
if (err.code === 'EEXIST') {
this.logger.error(`A lock file exists at ${lockFilePath}. Another xud process is running or a previous process exited ungracefully.`);
return;
} else {
throw err;
}
}
}

if (!this.config.rpc.disable) {
// start rpc server first, it will respond with UNAVAILABLE error
// indicating xud is starting until xud finishes initializing
Expand Down Expand Up @@ -296,6 +315,10 @@ class Xud extends EventEmitter {
// TODO: ensure we are not in the middle of executing any trades
const closePromises: Promise<void>[] = [];

if (this.lockFilePath) {
closePromises.push(fs.unlink(this.lockFilePath).catch(this.logger.error));
}

this.simnetChannels$?.unsubscribe();

if (this.swapClientManager) {
Expand All @@ -318,7 +341,7 @@ class Xud extends EventEmitter {
}
await Promise.all(closePromises);

await this.db.close();
await this.db?.close();
this.logger.info('XUD shutdown gracefully');

this.emit('shutdown');
Expand Down
2 changes: 1 addition & 1 deletion lib/service/Service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { ProvidePreimageEvent, TransferReceivedEvent } from '../connextclient/ty
import { OrderSide, Owner, SwapClientType, SwapRole } from '../constants/enums';
import { OrderAttributes, TradeInstance } from '../db/types';
import Logger, { Level, LevelPriority } from '../Logger';
import NodeKey from '../nodekey/NodeKey';
import OrderBook from '../orderbook/OrderBook';
import { Currency, isOwnOrder, Order, OrderPortion, OwnLimitOrder, OwnMarketOrder, OwnOrder, PeerOrder, PlaceOrderEvent } from '../orderbook/types';
import Pool from '../p2p/Pool';
Expand All @@ -19,7 +20,6 @@ import { checkDecimalPlaces, sortOrders, toEip55Address } from '../utils/utils';
import commitHash from '../Version';
import errors from './errors';
import { NodeIdentifier, ServiceComponents, ServiceOrder, ServiceOrderSidesArrays, ServicePlaceOrderEvent, ServiceTrade, XudInfo } from './types';
import NodeKey from 'lib/nodekey/NodeKey';

/** Functions to check argument validity and throw [[INVALID_ARGUMENT]] when invalid. */
const argChecks = {
Expand Down
2 changes: 1 addition & 1 deletion test/integration/Service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ describe('API Service', () => {
});

it('should remove an order', () => {
const tp = xud['orderBook'].tradingPairs.get('LTC/BTC')!;
const tp = xud['orderBook']!.tradingPairs.get('LTC/BTC')!;
expect(tp.ownOrders.buyMap.has(orderId!)).to.be.true;
const args = {
orderId: '1',
Expand Down
10 changes: 5 additions & 5 deletions test/p2p/networks.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ describe('P2P Networks Tests', () => {
await Promise.all([srcNode.start(srcNodeConfig), destNode.start(destNodeConfig)]);

const host = 'localhost';
const port = destNode['pool']['listenPort']!;
const nodePubKey = destNode['pool'].nodePubKey;
const port = destNode['pool']!['listenPort']!;
const nodePubKey = destNode['pool']!.nodePubKey;
const nodeTwoUri = toUri({ host, port, nodePubKey });

const rejectionMsg = `Peer ${nodePubKey}@${host}:${port} closed due to WireProtocolErr framer: incompatible msg origin network (expected: ${srcNodeNetwork}, found: ${destNodeNetwork})`;
Expand All @@ -39,11 +39,11 @@ describe('P2P Networks Tests', () => {
const srcNode = new Xud();
const destNode = new Xud();
await Promise.all([srcNode.start(srcNodeConfig), destNode.start(destNodeConfig)]);
const srcNodePubKey = srcNode['pool'].nodePubKey;
const destNodePubKey = destNode['pool'].nodePubKey;
const srcNodePubKey = srcNode['pool']!.nodePubKey;
const destNodePubKey = destNode['pool']!.nodePubKey;

const host = 'localhost';
const port = destNode['pool']['listenPort']!;
const port = destNode['pool']!['listenPort']!;
const nodeTwoUri = toUri({ host, port, nodePubKey: destNodePubKey });

await expect(srcNode.service.connect({ nodeUri: nodeTwoUri, retryConnecting: false })).to.be.fulfilled;
Expand Down
14 changes: 7 additions & 7 deletions test/p2p/sanity.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,11 +61,11 @@ describe('P2P Sanity Tests', () => {

await Promise.all([nodeOne.start(nodeOneConfig), nodeTwo.start(nodeTwoConfig)]);

nodeOnePubKey = nodeOne['pool'].nodePubKey;
nodeTwoPubKey = nodeTwo['pool'].nodePubKey;
nodeOnePubKey = nodeOne['pool']!.nodePubKey;
nodeTwoPubKey = nodeTwo['pool']!.nodePubKey;

nodeTwoPort = nodeTwo['pool']['listenPort']!;
nodeOneUri = toUri({ nodePubKey: nodeOnePubKey, host: 'localhost', port: nodeOne['pool']['listenPort']! });
nodeTwoPort = nodeTwo['pool']!['listenPort']!;
nodeOneUri = toUri({ nodePubKey: nodeOnePubKey, host: 'localhost', port: nodeOne['pool']!['listenPort']! });
nodeTwoUri = toUri({ nodePubKey: nodeTwoPubKey, host: 'localhost', port: nodeTwoPort });

unusedPort = await getUnusedPort();
Expand All @@ -82,13 +82,13 @@ describe('P2P Sanity Tests', () => {

it('should update the node state', (done) => {
const btcPubKey = '0395033b252c6f40e3756984162d68174e2bd8060a129c0d3462a9370471c6d28f';
const nodeTwoPeer = nodeOne['pool'].getPeer(nodeTwoPubKey);
const nodeTwoPeer = nodeOne['pool']!.getPeer(nodeTwoPubKey);
nodeTwoPeer.on('nodeStateUpdate', () => {
expect(nodeTwoPeer['nodeState']!.lndPubKeys['BTC']).to.equal(btcPubKey);
done();
});

nodeTwo['pool'].updateLndState({
nodeTwo['pool']!.updateLndState({
currency: 'BTC',
pubKey: btcPubKey,
});
Expand All @@ -100,7 +100,7 @@ describe('P2P Sanity Tests', () => {
});

it('should disconnect successfully', async () => {
await nodeOne['pool']['closePeer'](nodeTwoPubKey, DisconnectionReason.NotAcceptingConnections);
await nodeOne['pool']!['closePeer'](nodeTwoPubKey, DisconnectionReason.NotAcceptingConnections);

const listPeersResult = nodeOne.service.listPeers();
expect(listPeersResult).to.be.empty;
Expand Down

0 comments on commit 008f899

Please sign in to comment.