From 008f89987d54e8c699d5470a17b45c920cce2e53 Mon Sep 17 00:00:00 2001 From: Daniel McNally Date: Wed, 2 Dec 2020 01:15:40 -0500 Subject: [PATCH] feat: lock file 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. --- lib/Xud.ts | 31 +++++++++++++++++++++++++++---- lib/service/Service.ts | 2 +- test/integration/Service.spec.ts | 2 +- test/p2p/networks.spec.ts | 10 +++++----- test/p2p/sanity.spec.ts | 14 +++++++------- 5 files changed, 41 insertions(+), 18 deletions(-) diff --git a/lib/Xud.ts b/lib/Xud.ts index 490e252c0..d83880e0a 100644 --- a/lib/Xud.ts +++ b/lib/Xud.ts @@ -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; @@ -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. @@ -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 @@ -296,6 +315,10 @@ class Xud extends EventEmitter { // TODO: ensure we are not in the middle of executing any trades const closePromises: Promise[] = []; + if (this.lockFilePath) { + closePromises.push(fs.unlink(this.lockFilePath).catch(this.logger.error)); + } + this.simnetChannels$?.unsubscribe(); if (this.swapClientManager) { @@ -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'); diff --git a/lib/service/Service.ts b/lib/service/Service.ts index f9ded99a7..8e3898a0d 100644 --- a/lib/service/Service.ts +++ b/lib/service/Service.ts @@ -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'; @@ -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 = { diff --git a/test/integration/Service.spec.ts b/test/integration/Service.spec.ts index 128511dbc..f9b4ed349 100644 --- a/test/integration/Service.spec.ts +++ b/test/integration/Service.spec.ts @@ -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', diff --git a/test/p2p/networks.spec.ts b/test/p2p/networks.spec.ts index 81f5dacbe..7e67ec7c2 100644 --- a/test/p2p/networks.spec.ts +++ b/test/p2p/networks.spec.ts @@ -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})`; @@ -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; diff --git a/test/p2p/sanity.spec.ts b/test/p2p/sanity.spec.ts index 161181ec7..19dac5df0 100644 --- a/test/p2p/sanity.spec.ts +++ b/test/p2p/sanity.spec.ts @@ -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(); @@ -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, }); @@ -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;