Skip to content

Commit f0ee657

Browse files
authored
Merge pull request #614 from brendandburns/stop
Add a CONNECT event to informer.
2 parents 965da9b + 7d88ed6 commit f0ee657

File tree

3 files changed

+168
-46
lines changed

3 files changed

+168
-46
lines changed

src/cache.ts

Lines changed: 28 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,15 @@
1-
import { ADD, CHANGE, DELETE, ERROR, Informer, ListPromise, ObjectCallback, UPDATE } from './informer';
1+
import {
2+
ADD,
3+
CHANGE,
4+
CONNECT,
5+
DELETE,
6+
ERROR,
7+
ErrorCallback,
8+
Informer,
9+
ListPromise,
10+
ObjectCallback,
11+
UPDATE,
12+
} from './informer';
213
import { KubernetesObject } from './types';
314
import { RequestResult, Watch } from './watch';
415

@@ -11,7 +22,7 @@ export class ListWatch<T extends KubernetesObject> implements ObjectCache<T>, In
1122
private objects: T[] = [];
1223
private resourceVersion: string;
1324
private readonly indexCache: { [key: string]: T[] } = {};
14-
private readonly callbackCache: { [key: string]: Array<ObjectCallback<T>> } = {};
25+
private readonly callbackCache: { [key: string]: Array<ObjectCallback<T> | ErrorCallback> } = {};
1526
private request: RequestResult | undefined;
1627
private stopped: boolean = false;
1728

@@ -25,6 +36,7 @@ export class ListWatch<T extends KubernetesObject> implements ObjectCache<T>, In
2536
this.callbackCache[UPDATE] = [];
2637
this.callbackCache[DELETE] = [];
2738
this.callbackCache[ERROR] = [];
39+
this.callbackCache[CONNECT] = [];
2840
this.resourceVersion = '';
2941
if (autoStart) {
3042
this.doneHandler(null);
@@ -41,11 +53,13 @@ export class ListWatch<T extends KubernetesObject> implements ObjectCache<T>, In
4153
this._stop();
4254
}
4355

44-
public on(verb: string, cb: ObjectCallback<T>): void {
56+
public on(verb: 'add' | 'update' | 'delete' | 'change', cb: ObjectCallback<T>): void;
57+
public on(verb: 'error' | 'connect', cb: ErrorCallback): void;
58+
public on(verb: string, cb: any): void {
4559
if (verb === CHANGE) {
46-
this.on(ADD, cb);
47-
this.on(UPDATE, cb);
48-
this.on(DELETE, cb);
60+
this.on('add', cb);
61+
this.on('update', cb);
62+
this.on('delete', cb);
4963
return;
5064
}
5165
if (this.callbackCache[verb] === undefined) {
@@ -54,11 +68,13 @@ export class ListWatch<T extends KubernetesObject> implements ObjectCache<T>, In
5468
this.callbackCache[verb].push(cb);
5569
}
5670

57-
public off(verb: string, cb: ObjectCallback<T>): void {
71+
public off(verb: 'add' | 'update' | 'delete' | 'change', cb: ObjectCallback<T>): void;
72+
public off(verb: 'error' | 'connect', cb: ErrorCallback): void;
73+
public off(verb: string, cb: any): void {
5874
if (verb === CHANGE) {
59-
this.off(ADD, cb);
60-
this.off(UPDATE, cb);
61-
this.off(DELETE, cb);
75+
this.off('add', cb);
76+
this.off('update', cb);
77+
this.off('delete', cb);
6278
return;
6379
}
6480
if (this.callbackCache[verb] === undefined) {
@@ -102,13 +118,14 @@ export class ListWatch<T extends KubernetesObject> implements ObjectCache<T>, In
102118
private async doneHandler(err: any): Promise<any> {
103119
this._stop();
104120
if (err) {
105-
this.callbackCache[ERROR].forEach((elt: ObjectCallback<T>) => elt(err));
121+
this.callbackCache[ERROR].forEach((elt: ErrorCallback) => elt(err));
106122
return;
107123
}
108124
if (this.stopped) {
109125
// do not auto-restart
110126
return;
111127
}
128+
this.callbackCache[CONNECT].forEach((elt: ErrorCallback) => elt(undefined));
112129
// TODO: Don't always list here for efficiency
113130
// try to restart the watch from resourceVersion, but detect 410 GONE and relist in that case.
114131
// Or if resourceVersion is empty.

src/cache_test.ts

Lines changed: 134 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { EventEmitter } from 'ws';
99

1010
import { V1Namespace, V1NamespaceList, V1ObjectMeta, V1Pod, V1ListMeta } from './api';
1111
import { deleteObject, ListWatch, deleteItems } from './cache';
12-
import { ADD, UPDATE, DELETE, ERROR, ListPromise, CHANGE } from './informer';
12+
import { ListPromise } from './informer';
1313

1414
use(chaiAsPromised);
1515

@@ -44,7 +44,9 @@ describe('ListWatchCache', () => {
4444
};
4545
const lw = new ListWatch('/some/path', fake, listFn);
4646
const verb = 'FOOBAR';
47-
expect(() => lw.on(verb, (obj: V1Namespace) => {})).to.throw(`Unknown verb: ${verb}`);
47+
// The 'as any' is a hack to get around Typescript which prevents an unknown verb from being
48+
// passed. We want to test for Javascript clients also, where this is possible
49+
expect(() => (lw as any).on(verb, (obj?: V1Namespace) => {})).to.throw(`Unknown verb: ${verb}`);
4850
});
4951

5052
it('should perform basic caching', async () => {
@@ -216,19 +218,19 @@ describe('ListWatchCache', () => {
216218
expect(pathOut).to.equal('/some/path');
217219

218220
const addPromise = new Promise<V1Namespace>((resolve: (V1Namespace) => void) => {
219-
informer.on(ADD, (obj: V1Namespace) => {
221+
informer.on('add', (obj?: V1Namespace) => {
220222
resolve(obj);
221223
});
222224
});
223225

224226
const updatePromise = new Promise<V1Namespace>((resolve: (V1Namespace) => void) => {
225-
informer.on(UPDATE, (obj: V1Namespace) => {
227+
informer.on('update', (obj?: V1Namespace) => {
226228
resolve(obj);
227229
});
228230
});
229231

230232
const deletePromise = new Promise<V1Namespace>((resolve: (V1Namespace) => void) => {
231-
informer.on(DELETE, (obj: V1Namespace) => {
233+
informer.on('delete', (obj?: V1Namespace) => {
232234
resolve(obj);
233235
});
234236
});
@@ -310,7 +312,7 @@ describe('ListWatchCache', () => {
310312

311313
let count = 0;
312314
const changePromise = new Promise<boolean>((resolve: (V1Namespace) => void) => {
313-
informer.on(CHANGE, (obj: V1Namespace) => {
315+
informer.on('change', (obj?: V1Namespace) => {
314316
count++;
315317
if (count == 3) {
316318
resolve(true);
@@ -372,13 +374,13 @@ describe('ListWatchCache', () => {
372374
expect(pathOut).to.equal('/some/path');
373375

374376
const addPromise = new Promise<V1Namespace>((resolve: (V1Namespace) => void) => {
375-
informer.on(ADD, (obj: V1Namespace) => {
377+
informer.on('add', (obj?: V1Namespace) => {
376378
resolve(obj);
377379
});
378380
});
379381

380382
const addPromise2 = new Promise<V1Namespace>((resolve: (V1Namespace) => void) => {
381-
informer.on(ADD, (obj: V1Namespace) => {
383+
informer.on('add', (obj?: V1Namespace) => {
382384
resolve(obj);
383385
});
384386
});
@@ -442,9 +444,9 @@ describe('ListWatchCache', () => {
442444
const informer = new ListWatch('/some/path', mock.instance(fakeWatch), listFn, false);
443445

444446
const addObjects: V1Namespace[] = [];
445-
informer.on(ADD, (obj: V1Namespace) => addObjects.push(obj));
447+
informer.on('add', (obj?: V1Namespace) => addObjects.push(obj!));
446448
const updateObjects: V1Namespace[] = [];
447-
informer.on(UPDATE, (obj: V1Namespace) => updateObjects.push(obj));
449+
informer.on('update', (obj?: V1Namespace) => updateObjects.push(obj!));
448450

449451
informer.start();
450452
await promise;
@@ -518,11 +520,11 @@ describe('ListWatchCache', () => {
518520
const informer = new ListWatch('/some/path', mock.instance(fakeWatch), listFn, false);
519521

520522
const addObjects: V1Namespace[] = [];
521-
informer.on(ADD, (obj: V1Namespace) => addObjects.push(obj));
523+
informer.on('add', (obj?: V1Namespace) => addObjects.push(obj!));
522524
const updateObjects: V1Namespace[] = [];
523-
informer.on(UPDATE, (obj: V1Namespace) => updateObjects.push(obj));
525+
informer.on('update', (obj?: V1Namespace) => updateObjects.push(obj!));
524526
const deleteObjects: V1Namespace[] = [];
525-
informer.on(DELETE, (obj: V1Namespace) => deleteObjects.push(obj));
527+
informer.on('delete', (obj?: V1Namespace) => deleteObjects.push(obj!));
526528
informer.start();
527529
await promise;
528530
const [pathOut, , , doneHandler] = mock.capture(fakeWatch.watch).last();
@@ -716,29 +718,29 @@ describe('ListWatchCache', () => {
716718
const informer = new ListWatch('/some/path', mock.instance(fakeWatch), listFn);
717719

718720
const addedList1: V1Namespace[] = [];
719-
const addToList1Fn = function(obj: V1Namespace) {
720-
addedList1.push(obj);
721+
const addToList1Fn = function(obj?: V1Namespace) {
722+
addedList1.push(obj!);
721723
};
722724
const addedList2: V1Namespace[] = [];
723-
const addToList2Fn = function(obj: V1Namespace) {
724-
addedList2.push(obj);
725+
const addToList2Fn = function(obj?: V1Namespace) {
726+
addedList2.push(obj!);
725727
};
726728

727729
informer.start();
728730

729731
await watchCalled;
730732
const [, , watchHandler] = mock.capture(fakeWatch.watch).last();
731733

732-
informer.on(ADD, addToList1Fn);
733-
informer.on(ADD, addToList2Fn);
734+
informer.on('add', addToList1Fn);
735+
informer.on('add', addToList2Fn);
734736

735737
watchHandler('ADDED', {
736738
metadata: {
737739
name: 'name1',
738740
} as V1ObjectMeta,
739741
} as V1Namespace);
740742

741-
informer.off(ADD, addToList2Fn);
743+
informer.off('add', addToList2Fn);
742744

743745
watchHandler('ADDED', {
744746
metadata: {
@@ -775,20 +777,20 @@ describe('ListWatchCache', () => {
775777
const informer = new ListWatch('/some/path', mock.instance(fakeWatch), listFn);
776778

777779
const addedList: V1Namespace[] = [];
778-
const addToListFn = function(obj: V1Namespace) {
779-
addedList.push(obj);
780+
const addToListFn = function(obj?: V1Namespace) {
781+
addedList.push(obj!);
780782
};
781783
const removeSelf = function() {
782-
informer.off(ADD, removeSelf);
784+
informer.off('add', removeSelf);
783785
};
784786

785787
informer.start();
786788

787789
await watchCalled;
788790
const [, , watchHandler] = mock.capture(fakeWatch.watch).last();
789791

790-
informer.on(ADD, removeSelf);
791-
informer.on(ADD, addToListFn);
792+
informer.on('add', removeSelf);
793+
informer.on('add', addToListFn);
792794

793795
watchHandler('ADDED', {
794796
metadata: {
@@ -863,12 +865,12 @@ describe('ListWatchCache', () => {
863865
const informer = new ListWatch('/some/path', mock.instance(fakeWatch), listFn);
864866

865867
const addedList1: V1Namespace[] = [];
866-
const addToList1Fn = function(obj: V1Namespace) {
867-
addedList1.push(obj);
868+
const addToList1Fn = function(obj?: V1Namespace) {
869+
addedList1.push(obj!);
868870
};
869871
const addedList2: V1Namespace[] = [];
870-
const addToList2Fn = function(obj: V1Namespace) {
871-
addedList2.push(obj);
872+
const addToList2Fn = function(obj?: V1Namespace) {
873+
addedList2.push(obj!);
872874
};
873875

874876
informer.start();
@@ -877,9 +879,9 @@ describe('ListWatchCache', () => {
877879
const [, , watchHandler] = mock.capture(fakeWatch.watch).last();
878880

879881
let adds = 0;
880-
informer.on(ADD, () => adds++);
881-
informer.on(ADD, addToList1Fn);
882-
informer.on(ADD, addToList2Fn);
882+
informer.on('add', () => adds++);
883+
informer.on('add', addToList1Fn);
884+
informer.on('add', addToList2Fn);
883885

884886
watchHandler('ADDED', {
885887
metadata: {
@@ -888,7 +890,7 @@ describe('ListWatchCache', () => {
888890
} as V1ObjectMeta,
889891
} as V1Namespace);
890892

891-
informer.off(ADD, addToList2Fn);
893+
informer.off('add', addToList2Fn);
892894

893895
watchHandler('ADDED', {
894896
metadata: {
@@ -1010,7 +1012,7 @@ describe('ListWatchCache', () => {
10101012
await promise;
10111013

10121014
let errorEmitted = false;
1013-
cache.on(ERROR, () => (errorEmitted = true));
1015+
cache.on('error', () => (errorEmitted = true));
10141016

10151017
const [, , , doneHandler] = mock.capture(fakeWatch.watch).last();
10161018

@@ -1106,7 +1108,104 @@ describe('delete items', () => {
11061108
];
11071109
const pods: V1Pod[] = [];
11081110

1109-
deleteItems(listA, listB, [(obj: V1Pod) => pods.push(obj)]);
1111+
deleteItems(listA, listB, [(obj?: V1Pod) => pods.push(obj!)]);
11101112
expect(pods).to.deep.equal(expected);
11111113
});
1114+
1115+
it('should call the connect handler', async () => {
1116+
const fakeWatch = mock.mock(Watch);
1117+
const listObj = {
1118+
metadata: {
1119+
resourceVersion: '12345',
1120+
} as V1ListMeta,
1121+
items: [],
1122+
} as V1NamespaceList;
1123+
1124+
const listFn: ListPromise<V1Namespace> = function(): Promise<{
1125+
response: http.IncomingMessage;
1126+
body: V1NamespaceList;
1127+
}> {
1128+
return new Promise<{ response: http.IncomingMessage; body: V1NamespaceList }>(
1129+
(resolve, reject) => {
1130+
resolve({ response: {} as http.IncomingMessage, body: listObj });
1131+
},
1132+
);
1133+
};
1134+
const informer = new ListWatch('/some/path', mock.instance(fakeWatch), listFn, false);
1135+
const connectPromise = new Promise<boolean>((resolve: (boolean) => void) => {
1136+
informer.on('connect', (obj?: V1Namespace) => {
1137+
resolve(true);
1138+
});
1139+
});
1140+
informer.start();
1141+
1142+
expect(connectPromise).to.eventually.be.true;
1143+
});
1144+
1145+
it('does calls connect after a restart after an error', async () => {
1146+
const fakeWatch = mock.mock(Watch);
1147+
const list: V1Pod[] = [
1148+
{
1149+
metadata: {
1150+
name: 'name1',
1151+
namespace: 'ns1',
1152+
} as V1ObjectMeta,
1153+
} as V1Pod,
1154+
{
1155+
metadata: {
1156+
name: 'name2',
1157+
namespace: 'ns2',
1158+
} as V1ObjectMeta,
1159+
} as V1Pod,
1160+
];
1161+
const listObj = {
1162+
metadata: {
1163+
resourceVersion: '12345',
1164+
} as V1ListMeta,
1165+
items: list,
1166+
} as V1NamespaceList;
1167+
1168+
const listFn: ListPromise<V1Namespace> = function(): Promise<{
1169+
response: http.IncomingMessage;
1170+
body: V1NamespaceList;
1171+
}> {
1172+
return new Promise<{ response: http.IncomingMessage; body: V1NamespaceList }>(
1173+
(resolve, reject) => {
1174+
resolve({ response: {} as http.IncomingMessage, body: listObj });
1175+
},
1176+
);
1177+
};
1178+
let promise = new Promise((resolve) => {
1179+
mock.when(
1180+
fakeWatch.watch(mock.anything(), mock.anything(), mock.anything(), mock.anything()),
1181+
).thenCall(() => {
1182+
resolve(new FakeRequest());
1183+
});
1184+
});
1185+
1186+
const cache = new ListWatch('/some/path', mock.instance(fakeWatch), listFn);
1187+
await promise;
1188+
1189+
let errorEmitted = false;
1190+
cache.on('error', () => (errorEmitted = true));
1191+
1192+
const [, , , doneHandler] = mock.capture(fakeWatch.watch).last();
1193+
1194+
const error = new Error('testing');
1195+
await doneHandler(error);
1196+
1197+
mock.verify(
1198+
fakeWatch.watch(mock.anything(), mock.anything(), mock.anything(), mock.anything()),
1199+
).once();
1200+
expect(errorEmitted).to.equal(true);
1201+
1202+
const connectPromise = new Promise<boolean>((resolve: (boolean) => void) => {
1203+
cache.on('connect', (obj?: V1Namespace) => {
1204+
resolve(true);
1205+
});
1206+
});
1207+
cache.start();
1208+
1209+
expect(connectPromise).to.eventually.be.true;
1210+
});
11121211
});

0 commit comments

Comments
 (0)