зеркало из https://github.com/microsoft/etcd3.git
fix: mark leases as lost if the watch connection is alive but etcd is unresponsive
Fixes #110
This commit is contained in:
Родитель
d50fb8a270
Коммит
e6eaff250a
|
@ -8,8 +8,9 @@
|
||||||
Thank you to [@pauliusuza](https://github.com/pauliusuza) for his help updating everything
|
Thank you to [@pauliusuza](https://github.com/pauliusuza) for his help updating everything
|
||||||
|
|
||||||
- **feat**: add `SingleRangeBuilder.exists()` that returns if the given key exists
|
- **feat**: add `SingleRangeBuilder.exists()` that returns if the given key exists
|
||||||
- **fix**: errors when creating watchers not being handled correctly (see [#114](https://github.com/microsoft/etcd3/issues/114))
|
|
||||||
- **feat**: allow apply call options to authentication token exchange (see [#111](https://github.com/microsoft/etcd3/issues/111))
|
- **feat**: allow apply call options to authentication token exchange (see [#111](https://github.com/microsoft/etcd3/issues/111))
|
||||||
|
- **fix**: errors when creating watchers not being handled correctly (see [#114](https://github.com/microsoft/etcd3/issues/114))
|
||||||
|
- **fix**: mark leases as lost if the watch connection is alive but etcd is unresponsive (see [#110](https://github.com/microsoft/etcd3/issues/110))
|
||||||
|
|
||||||
## 0.2.13 2019-07-03
|
## 0.2.13 2019-07-03
|
||||||
|
|
||||||
|
|
17
src/lease.ts
17
src/lease.ts
|
@ -8,7 +8,7 @@ import { PutBuilder } from './builder';
|
||||||
import { ConnectionPool, Host } from './connection-pool';
|
import { ConnectionPool, Host } from './connection-pool';
|
||||||
import { castGrpcError, EtcdError, EtcdLeaseInvalidError, GRPCConnectFailedError } from './errors';
|
import { castGrpcError, EtcdError, EtcdLeaseInvalidError, GRPCConnectFailedError } from './errors';
|
||||||
import * as RPC from './rpc';
|
import * as RPC from './rpc';
|
||||||
import { NSApplicator } from './util';
|
import { NSApplicator, debounce } from './util';
|
||||||
|
|
||||||
function throwIfError<T>(value: T | Error): T {
|
function throwIfError<T>(value: T | Error): T {
|
||||||
if (value instanceof Error) {
|
if (value instanceof Error) {
|
||||||
|
@ -293,14 +293,22 @@ export class Lease extends EventEmitter {
|
||||||
return stream.end();
|
return stream.end();
|
||||||
}
|
}
|
||||||
|
|
||||||
const keepaliveTimer = setInterval(() => this.fireKeepAlive(stream), (1000 * this.ttl) / 3);
|
// this is what the official Go client uses, good enough:
|
||||||
|
const keepAliveInterval = (1000 * this.ttl) / 3;
|
||||||
|
const keepaliveTimer = setInterval(() => this.fireKeepAlive(stream), keepAliveInterval);
|
||||||
|
const keepAliveTimeout = debounce(1000 * this.ttl, () =>
|
||||||
|
this.handleKeepaliveError(new GRPCConnectFailedError('GRPC watch stream has timed out.')),
|
||||||
|
);
|
||||||
|
|
||||||
this.teardown = () => {
|
this.teardown = () => {
|
||||||
this.teardown = () => undefined;
|
this.teardown = () => undefined;
|
||||||
|
keepAliveTimeout.cancel();
|
||||||
clearInterval(keepaliveTimer);
|
clearInterval(keepaliveTimer);
|
||||||
stream.end();
|
stream.end();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
keepAliveTimeout(); // start the debounce
|
||||||
|
|
||||||
stream
|
stream
|
||||||
.on('error', err => this.handleKeepaliveError(err))
|
.on('error', err => this.handleKeepaliveError(err))
|
||||||
.on('data', res => {
|
.on('data', res => {
|
||||||
|
@ -309,6 +317,7 @@ export class Lease extends EventEmitter {
|
||||||
}
|
}
|
||||||
|
|
||||||
this.lastKeepAlive = Date.now();
|
this.lastKeepAlive = Date.now();
|
||||||
|
keepAliveTimeout();
|
||||||
this.emit('keepaliveSucceeded', res);
|
this.emit('keepaliveSucceeded', res);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -326,6 +335,10 @@ export class Lease extends EventEmitter {
|
||||||
}
|
}
|
||||||
|
|
||||||
private handleKeepaliveError(err: Error) {
|
private handleKeepaliveError(err: Error) {
|
||||||
|
if (this.state === State.Revoked) {
|
||||||
|
return; // often write-after-end, or something along those lines
|
||||||
|
}
|
||||||
|
|
||||||
this.emit('keepaliveFailed', castGrpcError(err));
|
this.emit('keepaliveFailed', castGrpcError(err));
|
||||||
this.teardown();
|
this.teardown();
|
||||||
|
|
||||||
|
|
|
@ -5,8 +5,14 @@ import { expect } from 'chai';
|
||||||
import * as sinon from 'sinon';
|
import * as sinon from 'sinon';
|
||||||
|
|
||||||
import { Etcd3, EtcdLeaseInvalidError, GRPCConnectFailedError, Lease } from '..';
|
import { Etcd3, EtcdLeaseInvalidError, GRPCConnectFailedError, Lease } from '..';
|
||||||
import { onceEvent } from '../util';
|
import { onceEvent, delay } from '../util';
|
||||||
import { createTestClientAndKeys, getOptions, proxy, tearDownTestClient } from './util';
|
import {
|
||||||
|
createTestClientAndKeys,
|
||||||
|
getOptions,
|
||||||
|
proxy,
|
||||||
|
tearDownTestClient,
|
||||||
|
TrafficDirection,
|
||||||
|
} from './util';
|
||||||
|
|
||||||
describe('lease()', () => {
|
describe('lease()', () => {
|
||||||
let client: Etcd3;
|
let client: Etcd3;
|
||||||
|
@ -87,9 +93,9 @@ describe('lease()', () => {
|
||||||
|
|
||||||
lease = proxiedClient.lease(100);
|
lease = proxiedClient.lease(100);
|
||||||
await lease.grant();
|
await lease.grant();
|
||||||
proxy.pause();
|
proxy.suspend();
|
||||||
await onceEvent(lease, 'keepaliveFailed');
|
await onceEvent(lease, 'keepaliveFailed');
|
||||||
proxy.resume();
|
proxy.unsuspend();
|
||||||
await onceEvent(lease, 'keepaliveSucceeded');
|
await onceEvent(lease, 'keepaliveSucceeded');
|
||||||
await lease.revoke();
|
await lease.revoke();
|
||||||
|
|
||||||
|
@ -103,7 +109,7 @@ describe('lease()', () => {
|
||||||
|
|
||||||
lease = proxiedClient.lease(1);
|
lease = proxiedClient.lease(1);
|
||||||
await lease.grant();
|
await lease.grant();
|
||||||
proxy.pause();
|
proxy.suspend();
|
||||||
(lease as any).lastKeepAlive = Date.now() - 2000; // speed things up a little
|
(lease as any).lastKeepAlive = Date.now() - 2000; // speed things up a little
|
||||||
const err = await onceEvent(lease, 'lost');
|
const err = await onceEvent(lease, 'lost');
|
||||||
expect(err.message).to.match(/our lease has expired/);
|
expect(err.message).to.match(/our lease has expired/);
|
||||||
|
@ -178,6 +184,29 @@ describe('lease()', () => {
|
||||||
expect(kaFired.fired).to.be.false;
|
expect(kaFired.fired).to.be.false;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('marks leases as failed if etcd does not respond to keepalives in time (#110)', async () => {
|
||||||
|
await lease.revoke();
|
||||||
|
|
||||||
|
await proxy.activate();
|
||||||
|
const proxiedClient = new Etcd3(getOptions());
|
||||||
|
lease = proxiedClient.lease(1);
|
||||||
|
await lease.grant();
|
||||||
|
proxy.pause(TrafficDirection.FromEtcd);
|
||||||
|
|
||||||
|
const failedEvent = watchEmission('keepaliveFailed');
|
||||||
|
clock.tick(50000);
|
||||||
|
await Promise.resolve(); // drain task queues
|
||||||
|
|
||||||
|
expect(failedEvent.fired).to.be.false;
|
||||||
|
clock.tick(10000);
|
||||||
|
await Promise.resolve(); // drain task queues
|
||||||
|
|
||||||
|
expect(failedEvent.fired).to.be.true;
|
||||||
|
lease.release();
|
||||||
|
proxiedClient.close();
|
||||||
|
await proxy.deactivate();
|
||||||
|
});
|
||||||
|
|
||||||
it('tears down if the lease gets revoked', async () => {
|
it('tears down if the lease gets revoked', async () => {
|
||||||
await client.leaseClient.leaseRevoke({ ID: await lease.grant() });
|
await client.leaseClient.leaseRevoke({ ID: await lease.grant() });
|
||||||
clock.tick(20000);
|
clock.tick(20000);
|
||||||
|
|
|
@ -16,6 +16,11 @@ const tlsKey = fs.readFileSync(`${rootPath}/src/test/certs/private/etcd0.localho
|
||||||
const etcdSourceAddress = process.env.ETCD_ADDR || '127.0.0.1:2379';
|
const etcdSourceAddress = process.env.ETCD_ADDR || '127.0.0.1:2379';
|
||||||
const [etcdSourceHost, etcdSourcePort] = etcdSourceAddress.split(':');
|
const [etcdSourceHost, etcdSourcePort] = etcdSourceAddress.split(':');
|
||||||
|
|
||||||
|
export const enum TrafficDirection {
|
||||||
|
ToEtcd,
|
||||||
|
FromEtcd,
|
||||||
|
}
|
||||||
|
|
||||||
export const etcdVersion = process.env.ETCD_VERSION || '3.3.9';
|
export const etcdVersion = process.env.ETCD_VERSION || '3.3.9';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -29,6 +34,7 @@ export class Proxy {
|
||||||
private server: tls.Server;
|
private server: tls.Server;
|
||||||
private host: string;
|
private host: string;
|
||||||
private port: number;
|
private port: number;
|
||||||
|
private enabledDataFlows = new Set([TrafficDirection.FromEtcd, TrafficDirection.ToEtcd]);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* activate creates the proxy server.
|
* activate creates the proxy server.
|
||||||
|
@ -54,11 +60,11 @@ export class Proxy {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* pause temporarily shuts down the server, but does not 'deactivate' the
|
* suspend temporarily shuts down the server, but does not 'deactivate' the
|
||||||
* proxy; new connections will still try to hit it. Can be restored with
|
* proxy; new connections will still try to hit it. Can be restored with
|
||||||
* resume().
|
* resume().
|
||||||
*/
|
*/
|
||||||
public pause() {
|
public suspend() {
|
||||||
this.server.close();
|
this.server.close();
|
||||||
this.connections.forEach(cnx => cnx.end());
|
this.connections.forEach(cnx => cnx.end());
|
||||||
this.connections = [];
|
this.connections = [];
|
||||||
|
@ -67,10 +73,24 @@ export class Proxy {
|
||||||
/**
|
/**
|
||||||
* Starts up a previously stopped server.
|
* Starts up a previously stopped server.
|
||||||
*/
|
*/
|
||||||
public resume() {
|
public unsuspend() {
|
||||||
this.server.listen(this.port, this.host);
|
this.server.listen(this.port, this.host);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disables data flowing in one direction on the connection.
|
||||||
|
*/
|
||||||
|
public pause(direction: TrafficDirection) {
|
||||||
|
this.enabledDataFlows.delete(direction);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reenables data flow on the connection.
|
||||||
|
*/
|
||||||
|
public resume(direction: TrafficDirection) {
|
||||||
|
this.enabledDataFlows.add(direction);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Destroys a previously-active proxy server.
|
* Destroys a previously-active proxy server.
|
||||||
*/
|
*/
|
||||||
|
@ -118,17 +138,23 @@ export class Proxy {
|
||||||
};
|
};
|
||||||
|
|
||||||
serverCnx.on('data', (data: Buffer) => {
|
serverCnx.on('data', (data: Buffer) => {
|
||||||
if (!ended) {
|
if (ended || !this.enabledDataFlows.has(TrafficDirection.FromEtcd)) {
|
||||||
clientCnx.write(data);
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
clientCnx.write(data);
|
||||||
});
|
});
|
||||||
serverCnx.on('end', end);
|
serverCnx.on('end', end);
|
||||||
serverCnx.on('error', end);
|
serverCnx.on('error', end);
|
||||||
|
|
||||||
clientCnx.on('data', (data: Buffer) => {
|
clientCnx.on('data', (data: Buffer) => {
|
||||||
if (serverConnected && !ended) {
|
if (ended || !this.enabledDataFlows.has(TrafficDirection.ToEtcd)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (serverConnected) {
|
||||||
serverCnx.write(data);
|
serverCnx.write(data);
|
||||||
} else if (!ended) {
|
} else {
|
||||||
serverBuffer.push(data);
|
serverBuffer.push(data);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -71,9 +71,9 @@ describe('watch()', () => {
|
||||||
|
|
||||||
const watcher = await proxiedClient.watch().key('foo1').create();
|
const watcher = await proxiedClient.watch().key('foo1').create();
|
||||||
|
|
||||||
proxy.pause();
|
proxy.suspend();
|
||||||
await onceEvent(watcher, 'disconnected');
|
await onceEvent(watcher, 'disconnected');
|
||||||
proxy.resume();
|
proxy.unsuspend();
|
||||||
await onceEvent(watcher, 'connected');
|
await onceEvent(watcher, 'connected');
|
||||||
await expectWatching(watcher, 'foo1');
|
await expectWatching(watcher, 'foo1');
|
||||||
|
|
||||||
|
@ -98,9 +98,9 @@ describe('watch()', () => {
|
||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
proxy.pause();
|
proxy.suspend();
|
||||||
await onceEvent(watcher, 'disconnected');
|
await onceEvent(watcher, 'disconnected');
|
||||||
proxy.resume();
|
proxy.unsuspend();
|
||||||
await onceEvent(watcher, 'put').then((res: IKeyValue) => {
|
await onceEvent(watcher, 'put').then((res: IKeyValue) => {
|
||||||
expect(res.key.toString()).to.equal('foo1');
|
expect(res.key.toString()).to.equal('foo1');
|
||||||
expect(res.value.toString()).to.equal('update 2');
|
expect(res.value.toString()).to.equal('update 2');
|
||||||
|
@ -115,11 +115,11 @@ describe('watch()', () => {
|
||||||
const proxiedClient = await createTestClientAndKeys();
|
const proxiedClient = await createTestClientAndKeys();
|
||||||
|
|
||||||
const watcher = await proxiedClient.watch().key('foo1').create();
|
const watcher = await proxiedClient.watch().key('foo1').create();
|
||||||
proxy.pause();
|
proxy.suspend();
|
||||||
await onceEvent(watcher, 'disconnected');
|
await onceEvent(watcher, 'disconnected');
|
||||||
const actualRevision = Number(watcher.request.start_revision);
|
const actualRevision = Number(watcher.request.start_revision);
|
||||||
watcher.request.start_revision = 999999;
|
watcher.request.start_revision = 999999;
|
||||||
proxy.resume();
|
proxy.unsuspend();
|
||||||
await onceEvent(watcher, 'connected');
|
await onceEvent(watcher, 'connected');
|
||||||
expect(Number(watcher.request.start_revision)).to.equal(actualRevision);
|
expect(Number(watcher.request.start_revision)).to.equal(actualRevision);
|
||||||
});
|
});
|
||||||
|
@ -213,10 +213,10 @@ describe('watch()', () => {
|
||||||
const proxiedClient = await createTestClientAndKeys();
|
const proxiedClient = await createTestClientAndKeys();
|
||||||
|
|
||||||
const watcher = await proxiedClient.watch().key('foo1').create();
|
const watcher = await proxiedClient.watch().key('foo1').create();
|
||||||
proxy.pause();
|
proxy.suspend();
|
||||||
await watcher.cancel();
|
await watcher.cancel();
|
||||||
|
|
||||||
proxy.resume();
|
proxy.unsuspend();
|
||||||
expect(getWatchers()).to.deep.equal([]);
|
expect(getWatchers()).to.deep.equal([]);
|
||||||
|
|
||||||
proxiedClient.close();
|
proxiedClient.close();
|
||||||
|
|
21
src/util.ts
21
src/util.ts
|
@ -189,6 +189,27 @@ export function onceEvent(emitter: EventEmitter, ...events: string[]): Promise<a
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A trailing-edge debounce function.
|
||||||
|
*/
|
||||||
|
export function debounce(duration: number, fn: () => void) {
|
||||||
|
let timeout: NodeJS.Timeout | undefined;
|
||||||
|
|
||||||
|
const wrapper = () => {
|
||||||
|
wrapper.cancel();
|
||||||
|
timeout = setTimeout(fn, duration);
|
||||||
|
};
|
||||||
|
|
||||||
|
wrapper.cancel = () => {
|
||||||
|
if (timeout) {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
timeout = undefined;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return wrapper;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* PromiseWrap provides promise-like functions that auto-invoke an exec
|
* PromiseWrap provides promise-like functions that auto-invoke an exec
|
||||||
* method when called.
|
* method when called.
|
||||||
|
|
Загрузка…
Ссылка в новой задаче