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 671e049
Show file tree
Hide file tree
Showing 6 changed files with 56 additions and 33 deletions.
31 changes: 27 additions & 4 deletions lib/Xud.ts
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
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
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
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
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
30 changes: 15 additions & 15 deletions test/simulation/custom-xud.patch
@@ -1,8 +1,8 @@
diff --git a/lib/Xud.ts b/lib/Xud.ts
index 489a50a4..f9391527 100644
index d83880e0..1b95a77a 100644
--- a/lib/Xud.ts
+++ b/lib/Xud.ts
@@ -87,6 +87,11 @@ class Xud extends EventEmitter {
@@ -88,6 +88,11 @@ class Xud extends EventEmitter {
this.logger.info('config file loaded');
}

Expand All @@ -12,8 +12,8 @@ index 489a50a4..f9391527 100644
+ }
+
try {
if (!this.config.rpc.disable) {
// start rpc server first, it will respond with UNAVAILABLE error
if (this.config.instanceid === 0) {
// if we're not running with multiple instances as indicated by an instance id of 0
diff --git a/lib/swaps/SwapRecovery.ts b/lib/swaps/SwapRecovery.ts
index 3759f6a3..4089dc94 100644
--- a/lib/swaps/SwapRecovery.ts
Expand All @@ -39,10 +39,10 @@ index 3759f6a3..4089dc94 100644
}

diff --git a/lib/swaps/Swaps.ts b/lib/swaps/Swaps.ts
index 9648e02b..0e850119 100644
index c6946347..730cf781 100644
--- a/lib/swaps/Swaps.ts
+++ b/lib/swaps/Swaps.ts
@@ -730,6 +730,24 @@ class Swaps extends EventEmitter {
@@ -699,6 +699,24 @@ class Swaps extends EventEmitter {
} else if (deal.state === SwapState.Active) {
// we check that the deal is still active before we try to settle the invoice
try {
Expand All @@ -67,7 +67,7 @@ index 9648e02b..0e850119 100644
await swapClient.settleInvoice(rHash, rPreimage, currency);
} catch (err) {
this.logger.error(`could not settle invoice for deal ${rHash}`, err);
@@ -750,7 +768,9 @@ class Swaps extends EventEmitter {
@@ -719,7 +737,9 @@ class Swaps extends EventEmitter {
} catch (err) {
this.logger.error(`could not settle invoice for deal ${rHash}`, err);
}
Expand All @@ -78,7 +78,7 @@ index 9648e02b..0e850119 100644
});
await settleRetryPromise;
} else {
@@ -774,6 +794,16 @@ class Swaps extends EventEmitter {
@@ -743,6 +763,16 @@ class Swaps extends EventEmitter {
* accepted, initiates the swap.
*/
private handleSwapAccepted = async (responsePacket: packets.SwapAcceptedPacket, peer: Peer) => {
Expand All @@ -95,7 +95,7 @@ index 9648e02b..0e850119 100644
assert(responsePacket.body, 'SwapAcceptedPacket does not contain a body');
const { quantity, rHash, makerCltvDelta } = responsePacket.body;
const deal = this.getDeal(rHash);
@@ -861,6 +891,11 @@ class Swaps extends EventEmitter {
@@ -830,6 +860,11 @@ class Swaps extends EventEmitter {

try {
await makerSwapClient.sendPayment(deal);
Expand All @@ -107,7 +107,7 @@ index 9648e02b..0e850119 100644
} catch (err) {
// first we must handle the edge case where the maker has paid us but failed to claim our payment
// in this case, we've already marked the swap as having been paid and completed
@@ -1042,6 +1077,18 @@ class Swaps extends EventEmitter {
@@ -1011,6 +1046,18 @@ class Swaps extends EventEmitter {

this.logger.debug('Executing maker code to resolve hash');

Expand All @@ -126,7 +126,7 @@ index 9648e02b..0e850119 100644
const swapClient = this.swapClientManager.get(deal.takerCurrency)!;

// we update the phase persist the deal to the database before we attempt to send payment
@@ -1052,6 +1099,13 @@ class Swaps extends EventEmitter {
@@ -1021,6 +1068,13 @@ class Swaps extends EventEmitter {
assert(deal.state !== SwapState.Error, `cannot send payment for failed swap ${deal.rHash}`);

try {
Expand All @@ -140,7 +140,7 @@ index 9648e02b..0e850119 100644
deal.rPreimage = await swapClient.sendPayment(deal);
} catch (err) {
this.logger.debug(`sendPayment in resolveHash for swap ${deal.rHash} failed due to ${err.message}`);
@@ -1129,10 +1183,22 @@ class Swaps extends EventEmitter {
@@ -1098,10 +1152,22 @@ class Swaps extends EventEmitter {
}
}

Expand All @@ -164,7 +164,7 @@ index 9648e02b..0e850119 100644
return deal.rPreimage;
} else {
// If we are here we are the taker
@@ -1140,6 +1206,16 @@ class Swaps extends EventEmitter {
@@ -1109,6 +1175,16 @@ class Swaps extends EventEmitter {
assert(htlcCurrency === undefined || htlcCurrency === deal.takerCurrency, 'incoming htlc does not match expected deal currency');
this.logger.debug('Executing taker code to resolve hash');

Expand All @@ -181,7 +181,7 @@ index 9648e02b..0e850119 100644
return deal.rPreimage;
}
}
@@ -1308,8 +1384,11 @@ class Swaps extends EventEmitter {
@@ -1277,8 +1353,11 @@ class Swaps extends EventEmitter {
swapClient.removeInvoice(deal.rHash).catch(this.logger.error); // we don't need to await the remove invoice call
}
} else if (deal.phase === SwapPhase.SendingPayment) {
Expand All @@ -195,7 +195,7 @@ index 9648e02b..0e850119 100644
}

this.logger.trace(`emitting swap.failed event for ${deal.rHash}`);
@@ -1373,9 +1452,14 @@ class Swaps extends EventEmitter {
@@ -1343,9 +1422,14 @@ class Swaps extends EventEmitter {

if (deal.role === SwapRole.Maker) {
// the maker begins execution of the swap upon accepting the deal
Expand Down

0 comments on commit 671e049

Please sign in to comment.