Skip to content

Commit 878d186

Browse files
authored
improve client-side SO client get pooling (#82603) (#82690)
1 parent face4ce commit 878d186

2 files changed

Lines changed: 102 additions & 28 deletions

File tree

src/core/public/saved_objects/saved_objects_client.test.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,57 @@ describe('SavedObjectsClient', () => {
9797
`);
9898
});
9999

100+
test('removes duplicates when calling `_bulk_get`', async () => {
101+
// Await #get call to ensure batchQueue is empty and throttle has reset
102+
await savedObjectsClient.get('type2', doc.id);
103+
http.fetch.mockClear();
104+
105+
savedObjectsClient.get(doc.type, doc.id);
106+
savedObjectsClient.get('some-type', 'some-id');
107+
await savedObjectsClient.get(doc.type, doc.id);
108+
109+
expect(http.fetch).toHaveBeenCalledTimes(1);
110+
expect(http.fetch.mock.calls[0]).toMatchInlineSnapshot(`
111+
Array [
112+
"/api/saved_objects/_bulk_get",
113+
Object {
114+
"body": "[{\\"id\\":\\"AVwSwFxtcMV38qjDZoQg\\",\\"type\\":\\"config\\"},{\\"id\\":\\"some-id\\",\\"type\\":\\"some-type\\"}]",
115+
"method": "POST",
116+
"query": undefined,
117+
},
118+
]
119+
`);
120+
});
121+
122+
test('resolves with correct object when there are duplicates present', async () => {
123+
// Await #get call to ensure batchQueue is empty and throttle has reset
124+
await savedObjectsClient.get('type2', doc.id);
125+
http.fetch.mockClear();
126+
127+
const call1 = savedObjectsClient.get(doc.type, doc.id);
128+
const objFromCall2 = await savedObjectsClient.get(doc.type, doc.id);
129+
const objFromCall1 = await call1;
130+
131+
expect(objFromCall1.type).toBe(doc.type);
132+
expect(objFromCall1.id).toBe(doc.id);
133+
134+
expect(objFromCall2.type).toBe(doc.type);
135+
expect(objFromCall2.id).toBe(doc.id);
136+
});
137+
138+
test('do not share instances or references between duplicate callers', async () => {
139+
// Await #get call to ensure batchQueue is empty and throttle has reset
140+
await savedObjectsClient.get('type2', doc.id);
141+
http.fetch.mockClear();
142+
143+
const call1 = savedObjectsClient.get(doc.type, doc.id);
144+
const objFromCall2 = await savedObjectsClient.get(doc.type, doc.id);
145+
const objFromCall1 = await call1;
146+
147+
objFromCall1.set('title', 'new title');
148+
expect(objFromCall2.get('title')).toEqual('Example title');
149+
});
150+
100151
test('resolves with SimpleSavedObject instance', async () => {
101152
const response = savedObjectsClient.get(doc.type, doc.id);
102153
await expect(response).resolves.toBeInstanceOf(SimpleSavedObject);

src/core/public/saved_objects/saved_objects_client.ts

Lines changed: 51 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
* under the License.
1818
*/
1919

20-
import { cloneDeep, pick, throttle } from 'lodash';
20+
import { pick, throttle, cloneDeep } from 'lodash';
2121
import { resolve as resolveUrl } from 'url';
2222
import type { PublicMethodsOf } from '@kbn/utility-types';
2323

@@ -144,6 +144,23 @@ const API_BASE_URL = '/api/saved_objects/';
144144
*/
145145
export type SavedObjectsClientContract = PublicMethodsOf<SavedObjectsClient>;
146146

147+
interface ObjectTypeAndId {
148+
id: string;
149+
type: string;
150+
}
151+
152+
const getObjectsToFetch = (queue: BatchQueueEntry[]): ObjectTypeAndId[] => {
153+
const objects: ObjectTypeAndId[] = [];
154+
const inserted = new Set<string>();
155+
queue.forEach(({ id, type }) => {
156+
if (!inserted.has(`${type}|${id}`)) {
157+
objects.push({ id, type });
158+
inserted.add(`${type}|${id}`);
159+
}
160+
});
161+
return objects;
162+
};
163+
147164
/**
148165
* Saved Objects is Kibana's data persisentence mechanism allowing plugins to
149166
* use Elasticsearch for storing plugin state. The client-side
@@ -160,31 +177,34 @@ export class SavedObjectsClient {
160177
* Throttled processing of get requests into bulk requests at 100ms interval
161178
*/
162179
private processBatchQueue = throttle(
163-
() => {
164-
const queue = cloneDeep(this.batchQueue);
180+
async () => {
181+
const queue = [...this.batchQueue];
165182
this.batchQueue = [];
166183

167-
this.bulkGet(queue)
168-
.then(({ savedObjects }) => {
169-
queue.forEach((queueItem) => {
170-
const foundObject = savedObjects.find((savedObject) => {
171-
return savedObject.id === queueItem.id && savedObject.type === queueItem.type;
172-
});
184+
try {
185+
const objectsToFetch = getObjectsToFetch(queue);
186+
const { saved_objects: savedObjects } = await this.performBulkGet(objectsToFetch);
173187

174-
if (!foundObject) {
175-
return queueItem.resolve(
176-
this.createSavedObject(pick(queueItem, ['id', 'type']) as SavedObject)
177-
);
178-
}
179-
180-
queueItem.resolve(foundObject);
181-
});
182-
})
183-
.catch((err) => {
184-
queue.forEach((queueItem) => {
185-
queueItem.reject(err);
188+
queue.forEach((queueItem) => {
189+
const foundObject = savedObjects.find((savedObject) => {
190+
return savedObject.id === queueItem.id && savedObject.type === queueItem.type;
186191
});
192+
193+
if (foundObject) {
194+
// multiple calls may have been requested the same object.
195+
// we need to clone to avoid sharing references between the instances
196+
queueItem.resolve(this.createSavedObject(cloneDeep(foundObject)));
197+
} else {
198+
queueItem.resolve(
199+
this.createSavedObject(pick(queueItem, ['id', 'type']) as SavedObject)
200+
);
201+
}
187202
});
203+
} catch (err) {
204+
queue.forEach((queueItem) => {
205+
queueItem.reject(err);
206+
});
207+
}
188208
},
189209
BATCH_INTERVAL,
190210
{ leading: false }
@@ -373,14 +393,8 @@ export class SavedObjectsClient {
373393
* ])
374394
*/
375395
public bulkGet = (objects: Array<{ id: string; type: string }> = []) => {
376-
const path = this.getPath(['_bulk_get']);
377396
const filteredObjects = objects.map((obj) => pick(obj, ['id', 'type']));
378-
379-
const request: ReturnType<SavedObjectsApi['bulkGet']> = this.savedObjectsFetch(path, {
380-
method: 'POST',
381-
body: JSON.stringify(filteredObjects),
382-
});
383-
return request.then((resp) => {
397+
return this.performBulkGet(filteredObjects).then((resp) => {
384398
resp.saved_objects = resp.saved_objects.map((d) => this.createSavedObject(d));
385399
return renameKeys<
386400
PromiseType<ReturnType<SavedObjectsApi['bulkGet']>>,
@@ -389,6 +403,15 @@ export class SavedObjectsClient {
389403
});
390404
};
391405

406+
private async performBulkGet(objects: ObjectTypeAndId[]) {
407+
const path = this.getPath(['_bulk_get']);
408+
const request: ReturnType<SavedObjectsApi['bulkGet']> = this.savedObjectsFetch(path, {
409+
method: 'POST',
410+
body: JSON.stringify(objects),
411+
});
412+
return request;
413+
}
414+
392415
/**
393416
* Updates an object
394417
*

0 commit comments

Comments
 (0)