Skip to content

Commit a883a94

Browse files
committed
Address feedback on createPointInTimeFinder.
1 parent 99b64df commit a883a94

File tree

4 files changed

+159
-58
lines changed

4 files changed

+159
-58
lines changed

src/core/server/saved_objects/export/find_with_point_in_time.test.ts renamed to src/core/server/saved_objects/export/point_in_time_finder.test.ts

Lines changed: 98 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,7 @@ import { loggerMock, MockedLogger } from '../../logging/logger.mock';
1111
import { SavedObjectsFindOptions } from '../types';
1212
import { SavedObjectsFindResult } from '../service';
1313

14-
import type { FindWithPointInTime } from './find_with_point_in_time';
15-
import { findWithPointInTime } from './find_with_point_in_time';
14+
import { createPointInTimeFinder } from './point_in_time_finder';
1615

1716
const mockHits = [
1817
{
@@ -39,18 +38,55 @@ const mockHits = [
3938
},
4039
];
4140

42-
describe('findWithPointInTime()', () => {
41+
describe('createPointInTimeFinder()', () => {
4342
let logger: MockedLogger;
4443
let savedObjectsClient: ReturnType<typeof savedObjectsClientMock.create>;
45-
let finder: FindWithPointInTime;
4644

4745
beforeEach(() => {
4846
logger = loggerMock.create();
4947
savedObjectsClient = savedObjectsClientMock.create();
50-
finder = findWithPointInTime({ savedObjectsClient, logger });
5148
});
5249

5350
describe('#find', () => {
51+
test('throws if a PIT is already open', async () => {
52+
savedObjectsClient.openPointInTimeForType.mockResolvedValueOnce({
53+
id: 'abc123',
54+
});
55+
savedObjectsClient.find.mockResolvedValueOnce({
56+
total: 2,
57+
saved_objects: mockHits,
58+
pit_id: 'abc123',
59+
per_page: 1,
60+
page: 0,
61+
});
62+
savedObjectsClient.find.mockResolvedValueOnce({
63+
total: 2,
64+
saved_objects: mockHits,
65+
pit_id: 'abc123',
66+
per_page: 1,
67+
page: 1,
68+
});
69+
70+
const findOptions: SavedObjectsFindOptions = {
71+
type: ['visualization'],
72+
search: 'foo*',
73+
perPage: 1,
74+
};
75+
76+
const finder = createPointInTimeFinder({ findOptions, logger, savedObjectsClient });
77+
await finder.find().next();
78+
79+
expect(savedObjectsClient.find).toHaveBeenCalledTimes(1);
80+
savedObjectsClient.find.mockClear();
81+
82+
expect(async () => {
83+
await finder.find().next();
84+
}).rejects.toThrowErrorMatchingInlineSnapshot(
85+
`"Point In Time has already been opened for this finder instance. Please call \`close()\` before calling \`find()\` again."`
86+
);
87+
expect(savedObjectsClient.find).toHaveBeenCalledTimes(0);
88+
});
89+
5490
test('works with a single page of results', async () => {
5591
savedObjectsClient.openPointInTimeForType.mockResolvedValueOnce({
5692
id: 'abc123',
@@ -63,13 +99,14 @@ describe('findWithPointInTime()', () => {
6399
page: 0,
64100
});
65101

66-
const options: SavedObjectsFindOptions = {
102+
const findOptions: SavedObjectsFindOptions = {
67103
type: ['visualization'],
68104
search: 'foo*',
69105
};
70106

107+
const finder = createPointInTimeFinder({ findOptions, logger, savedObjectsClient });
71108
const hits: SavedObjectsFindResult[] = [];
72-
for await (const result of finder.find(options)) {
109+
for await (const result of finder.find()) {
73110
hits.push(...result.saved_objects);
74111
}
75112

@@ -113,14 +150,15 @@ describe('findWithPointInTime()', () => {
113150
page: 0,
114151
});
115152

116-
const options: SavedObjectsFindOptions = {
153+
const findOptions: SavedObjectsFindOptions = {
117154
type: ['visualization'],
118155
search: 'foo*',
119156
perPage: 1,
120157
};
121158

159+
const finder = createPointInTimeFinder({ findOptions, logger, savedObjectsClient });
122160
const hits: SavedObjectsFindResult[] = [];
123-
for await (const result of finder.find(options)) {
161+
for await (const result of finder.find()) {
124162
hits.push(...result.saved_objects);
125163
}
126164

@@ -154,14 +192,15 @@ describe('findWithPointInTime()', () => {
154192
page: 0,
155193
});
156194

157-
const options: SavedObjectsFindOptions = {
195+
const findOptions: SavedObjectsFindOptions = {
158196
type: ['visualization'],
159197
search: 'foo*',
160198
perPage: 2,
161199
};
162200

201+
const finder = createPointInTimeFinder({ findOptions, logger, savedObjectsClient });
163202
const hits: SavedObjectsFindResult[] = [];
164-
for await (const result of finder.find(options)) {
203+
for await (const result of finder.find()) {
165204
hits.push(...result.saved_objects);
166205
await finder.close();
167206
}
@@ -195,14 +234,15 @@ describe('findWithPointInTime()', () => {
195234
page: 0,
196235
});
197236

198-
const options: SavedObjectsFindOptions = {
237+
const findOptions: SavedObjectsFindOptions = {
199238
type: ['visualization'],
200239
search: 'foo*',
201240
perPage: 1,
202241
};
203242

243+
const finder = createPointInTimeFinder({ findOptions, logger, savedObjectsClient });
204244
const hits: SavedObjectsFindResult[] = [];
205-
for await (const result of finder.find(options)) {
245+
for await (const result of finder.find()) {
206246
hits.push(...result.saved_objects);
207247
await finder.close();
208248
}
@@ -217,15 +257,16 @@ describe('findWithPointInTime()', () => {
217257
});
218258
savedObjectsClient.find.mockRejectedValueOnce(new Error('oops'));
219259

220-
const options: SavedObjectsFindOptions = {
260+
const findOptions: SavedObjectsFindOptions = {
221261
type: ['visualization'],
222262
search: 'foo*',
223263
perPage: 2,
224264
};
225265

266+
const finder = createPointInTimeFinder({ findOptions, logger, savedObjectsClient });
226267
const hits: SavedObjectsFindResult[] = [];
227268
try {
228-
for await (const result of finder.find(options)) {
269+
for await (const result of finder.find()) {
229270
hits.push(...result.saved_objects);
230271
}
231272
} catch (e) {
@@ -234,5 +275,47 @@ describe('findWithPointInTime()', () => {
234275

235276
expect(savedObjectsClient.closePointInTime).toHaveBeenCalledWith('test');
236277
});
278+
279+
test('finder can be reused after closing', async () => {
280+
savedObjectsClient.openPointInTimeForType.mockResolvedValueOnce({
281+
id: 'abc123',
282+
});
283+
savedObjectsClient.find.mockResolvedValueOnce({
284+
total: 2,
285+
saved_objects: mockHits,
286+
pit_id: 'abc123',
287+
per_page: 1,
288+
page: 0,
289+
});
290+
savedObjectsClient.find.mockResolvedValueOnce({
291+
total: 2,
292+
saved_objects: mockHits,
293+
pit_id: 'abc123',
294+
per_page: 1,
295+
page: 1,
296+
});
297+
298+
const findOptions: SavedObjectsFindOptions = {
299+
type: ['visualization'],
300+
search: 'foo*',
301+
perPage: 1,
302+
};
303+
304+
const finder = createPointInTimeFinder({ findOptions, logger, savedObjectsClient });
305+
306+
const findA = finder.find();
307+
await findA.next();
308+
await finder.close();
309+
310+
const findB = finder.find();
311+
await findB.next();
312+
await finder.close();
313+
314+
expect((await findA.next()).done).toBe(true);
315+
expect((await findB.next()).done).toBe(true);
316+
expect(savedObjectsClient.openPointInTimeForType).toHaveBeenCalledTimes(2);
317+
expect(savedObjectsClient.find).toHaveBeenCalledTimes(2);
318+
expect(savedObjectsClient.closePointInTime).toHaveBeenCalledTimes(2);
319+
});
237320
});
238321
});

src/core/server/saved_objects/export/find_with_point_in_time.ts renamed to src/core/server/saved_objects/export/point_in_time_finder.ts

Lines changed: 40 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -25,70 +25,75 @@ import { SavedObjectsFindResponse } from '../service';
2525
*
2626
* @example
2727
* ```ts
28-
* const finder = findWithPointInTime({
29-
* logger,
30-
* savedObjectsClient,
31-
* });
32-
*
33-
* const options: SavedObjectsFindOptions = {
28+
* const findOptions: SavedObjectsFindOptions = {
3429
* type: 'visualization',
3530
* search: 'foo*',
3631
* perPage: 100,
3732
* };
3833
*
34+
* const finder = createPointInTimeFinder({
35+
* logger,
36+
* savedObjectsClient,
37+
* findOptions,
38+
* });
39+
*
3940
* const responses: SavedObjectFindResponse[] = [];
40-
* for await (const response of finder.find(options)) {
41+
* for await (const response of finder.find()) {
4142
* responses.push(...response);
4243
* if (doneSearching) {
4344
* await finder.close();
4445
* }
4546
* }
4647
* ```
4748
*/
48-
export function findWithPointInTime({
49+
export function createPointInTimeFinder({
50+
findOptions,
4951
logger,
5052
savedObjectsClient,
5153
}: {
54+
findOptions: SavedObjectsFindOptions;
5255
logger: Logger;
5356
savedObjectsClient: SavedObjectsClientContract;
5457
}) {
55-
return new FindWithPointInTime({ logger, savedObjectsClient });
58+
return new PointInTimeFinder({ findOptions, logger, savedObjectsClient });
5659
}
5760

5861
/**
5962
* @internal
6063
*/
61-
export class FindWithPointInTime {
64+
export class PointInTimeFinder {
6265
readonly #log: Logger;
6366
readonly #savedObjectsClient: SavedObjectsClientContract;
64-
#open?: boolean;
65-
#perPage?: number;
67+
readonly #findOptions: SavedObjectsFindOptions;
68+
#open: boolean = false;
6669
#pitId?: string;
67-
#type?: string | string[];
6870

6971
constructor({
70-
savedObjectsClient,
72+
findOptions,
7173
logger,
74+
savedObjectsClient,
7275
}: {
73-
savedObjectsClient: SavedObjectsClientContract;
76+
findOptions: SavedObjectsFindOptions;
7477
logger: Logger;
78+
savedObjectsClient: SavedObjectsClientContract;
7579
}) {
7680
this.#log = logger;
7781
this.#savedObjectsClient = savedObjectsClient;
82+
this.#findOptions = {
83+
// Default to 1000 items per page as a tradeoff between
84+
// speed and memory consumption.
85+
perPage: 1000,
86+
...findOptions,
87+
};
7888
}
7989

80-
async *find(options: SavedObjectsFindOptions) {
81-
this.#open = true;
82-
this.#type = options.type;
83-
// Default to 1000 items per page as a tradeoff between
84-
// speed and memory consumption.
85-
this.#perPage = options.perPage ?? 1000;
86-
87-
const findOptions: SavedObjectsFindOptions = {
88-
...options,
89-
perPage: this.#perPage,
90-
type: this.#type,
91-
};
90+
async *find() {
91+
if (this.#open) {
92+
throw new Error(
93+
'Point In Time has already been opened for this finder instance. ' +
94+
'Please call `close()` before calling `find()` again.'
95+
);
96+
}
9297

9398
// Open PIT and request our first page of hits
9499
await this.open();
@@ -97,7 +102,7 @@ export class FindWithPointInTime {
97102
let lastHitSortValue: unknown[] | undefined;
98103
do {
99104
const results = await this.findNext({
100-
findOptions,
105+
findOptions: this.#findOptions,
101106
id: this.#pitId,
102107
...(lastHitSortValue ? { searchAfter: lastHitSortValue } : {}),
103108
});
@@ -108,44 +113,44 @@ export class FindWithPointInTime {
108113
this.#log.debug(`Collected [${lastResultsCount}] saved objects for export.`);
109114

110115
// Close PIT if this was our last page
111-
if (this.#pitId && lastResultsCount < this.#perPage) {
116+
if (this.#pitId && lastResultsCount < this.#findOptions.perPage!) {
112117
await this.close();
113118
}
114119

115120
yield results;
116121
// We've reached the end when there are fewer hits than our perPage size,
117122
// or when `close()` has been called.
118-
} while (this.#open && lastHitSortValue && lastResultsCount >= this.#perPage);
123+
} while (this.#open && lastHitSortValue && lastResultsCount >= this.#findOptions.perPage!);
119124

120125
return;
121126
}
122127

123128
async close() {
124129
try {
125130
if (this.#pitId) {
126-
this.#log.debug(`Closing PIT for types [${this.#type}]`);
131+
this.#log.debug(`Closing PIT for types [${this.#findOptions.type}]`);
127132
await this.#savedObjectsClient.closePointInTime(this.#pitId);
128133
this.#pitId = undefined;
129134
}
130-
this.#type = undefined;
131135
this.#open = false;
132136
} catch (e) {
133-
this.#log.error(`Failed to close PIT for types [${this.#type}]`);
137+
this.#log.error(`Failed to close PIT for types [${this.#findOptions.type}]`);
134138
throw e;
135139
}
136140
}
137141

138142
private async open() {
139143
try {
140-
const { id } = await this.#savedObjectsClient.openPointInTimeForType(this.#type!);
144+
const { id } = await this.#savedObjectsClient.openPointInTimeForType(this.#findOptions.type);
141145
this.#pitId = id;
146+
this.#open = true;
142147
} catch (e) {
143148
// Since `find` swallows 404s, it is expected that exporter will do the same,
144149
// so we only rethrow non-404 errors here.
145150
if (e.output.statusCode !== 404) {
146151
throw e;
147152
}
148-
this.#log.debug(`Unable to open PIT for types [${this.#type}]: 404 ${e}`);
153+
this.#log.debug(`Unable to open PIT for types [${this.#findOptions.type}]: 404 ${e}`);
149154
}
150155
}
151156

0 commit comments

Comments
 (0)