diff --git a/packages/xen-api/package.json b/packages/xen-api/package.json index 501c164da..a67f2d2b2 100644 --- a/packages/xen-api/package.json +++ b/packages/xen-api/package.json @@ -35,6 +35,7 @@ "bind-property-descriptor": "^2.0.0", "blocked": "^1.2.1", "debug": "^4.0.1", + "decorator-synchronized": "^0.6.0", "http-request-plus": "^0.14.0", "jest-diff": "^27.3.1", "json-rpc-protocol": "^0.13.1", diff --git a/packages/xen-api/src/index.js b/packages/xen-api/src/index.js index 990b28b5a..b30777a08 100644 --- a/packages/xen-api/src/index.js +++ b/packages/xen-api/src/index.js @@ -761,7 +761,53 @@ export class Xapi extends EventEmitter { return error } - async _call(method, args, timeout = this._callTimeout(method, args)) { + _call(method, args, timeout = this._callTimeout(method, args)) { + return this._guardedCall([method, args, timeout]) + } + + _callGuard = undefined + + _cleanCallGuard = () => { + this._callGuard = undefined + } + + async _acquireCallGuard() { + const guard = this._callGuard + if (guard !== undefined) { + debug('waiting guard') + await guard + return + } + debug('guard acquired') + + const { promise, resolve, reject } = defer() + promise.then(this._cleanCallGuard, this._cleanCallGuard) + this._callGuard = promise + return { resolve, reject } + } + + async _guardedCall(args, ignoredErrors = new Set()) { + await this._callGuard + + try { + return await this._unsafeCall(args) + } catch (error) { + const { code } = error + if (code === 'HOST_IS_SLAVE') { + const guard = await this._acquireCallGuard() + if (guard !== undefined) { + this._setUrl({ ...this._url, hostname: error.params[0] }) + guard.resolve() + } + + return this._guardedCall(args) + } + + throw error + } + } + + async _unsafeCall([method, args, timeout]) { const startTime = Date.now() try { const result = await pTimeout.call(this._addSyncStackTrace(this._transport(method, args)), timeout) @@ -850,13 +896,6 @@ export class Xapi extends EventEmitter { // 3. the session is renewed // 4. the second call fails with SESSION_INVALID which leads to a new // unnecessary renewal - _sessionOpenRetryOptions = { - tries: 2, - when: [{ code: 'HOST_IS_SLAVE' }], - onRetry: async error => { - this._setUrl({ ...this._url, hostname: error.params[0] }) - }, - } _sessionOpen = coalesceCalls(this._sessionOpen) // eslint-disable-next-line no-dupe-class-members async _sessionOpen() { @@ -866,20 +905,14 @@ export class Xapi extends EventEmitter { if (sessionId === undefined) { const params = [user, password] - this._sessionId = await pRetry( - () => this._interruptOnDisconnect(this._call('session.login_with_password', params)), - this._sessionOpenRetryOptions - ) + this._sessionId = await this._interruptOnDisconnect(this._call('session.login_with_password', params)) } const oldPoolRef = this._pool?.$ref // Similar to `(await this.getAllRecords('pool'))[0]` but prevents a // deadlock in case of error due to a pRetry calling _sessionOpen again - const pools = await pRetry( - () => this._call('pool.get_all_records', [this._sessionId]), - this._sessionOpenRetryOptions - ) + const pools = await this._call('pool.get_all_records', [this._sessionId]) const poolRef = Object.keys(pools)[0] this._pool = this._wrapRecord('pool', poolRef, pools[poolRef])