Skip to content

Commit f314f6f

Browse files
author
Adrien Maret
authored
Add BatchController for performant document writing (#679)
This PR introduce a new advanced class to process documents per batch without modifying existing code. The `BatchController` extends the `DocumentController` except that the following methods are replaced by their batch equivalent: - create => mCreate - replace => mReplace - createOrReplace => mCreateOrReplace - update => mUpdate - get => mGet - exists => mGet - delete => mDelete You only need to replace the `sdk.document.XXX` call by `batchController.XXX`: ``` await sdk.document.create('test', 'test', { name: 'aschen' }); const batchController = new BatchController(sdk); await batchController.create('test', 'test', { name: 'aschen' }); ``` The actual drawback is that the error management won't be the same because the `BatchController` with throw generic errors if something went wrong and not Kuzzle specifics errors with appropriate error codes.
1 parent 6226063 commit f314f6f

File tree

18 files changed

+1633
-64
lines changed

18 files changed

+1633
-64
lines changed
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
---
2+
code: true
3+
type: page
4+
title: constructor
5+
description: BatchController constructor method
6+
---
7+
8+
# constructor
9+
10+
<SinceBadge version="auto-version" />
11+
12+
Instantiate a new BatchController.
13+
14+
Each instance will start a timer to periodically send batch requests.
15+
16+
## Arguments
17+
18+
```js
19+
const batch = new BatchController(sdk, options);
20+
```
21+
22+
<br/>
23+
24+
| Argument | Type | Description |
25+
| --------- | ----------------- | ------------------ |
26+
| `sdk` | <pre>Kuzzle</pre> | SDK instance |
27+
| `options` | <pre>object</pre> | Additional options |
28+
29+
## options
30+
31+
* `interval`: Timer interval in ms (10). Actions will be executed every {interval} ms
32+
* `maxWriteBufferSize`: Max write buffer size (200). (Should match config "limits.documentsWriteCount")
33+
* `maxReadBufferSize`: Max read buffer size (10000). (Should match config "limits.documentsReadCount")
34+
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
---
2+
code: true
3+
type: page
4+
title: dispose
5+
description: BatchController dispose method
6+
---
7+
8+
# dispose
9+
10+
<SinceBadge version="auto-version" />
11+
12+
Dispose the instance.
13+
14+
This method has to be called to destroy the underlaying timer sending batch requests.
15+
16+
## Arguments
17+
18+
```js
19+
dispose (): Promise<void>
20+
```
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
code: true
3+
type: branch
4+
title: BatchController
5+
description: BatchController class documentation
6+
---
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
---
2+
code: false
3+
type: page
4+
title: Introduction
5+
description: BatchController class
6+
order: 0
7+
---
8+
9+
# BatchController
10+
11+
<SinceBadge version="auto-version" />
12+
13+
This class is an overload of the document controller and can be switched "as-is" in your code.
14+
15+
It allows to group API actions into batches to significantly increase performances.
16+
17+
The following methods will be executed by batch using
18+
m* actions:
19+
- create => mCreate
20+
- replace => mReplace
21+
- createOrReplace => mCreateOrReplace
22+
- update => mUpdate
23+
- get => mGet
24+
- exists => mGet
25+
- delete => mDelete
26+
27+
::: warning
28+
Standard API errors will not be available.
29+
Except for the `services.storage.not_found` error.
30+
:::
31+
32+
By default, the BatchController sends a batch of documents every 10ms. This can be configured when instantiating the BatchController through the `options.interval` constructor parameter.
33+
34+
::: info
35+
Depending on your workload, you may want to increase the timer interval to execute bigger batches.
36+
A bigger interval will also mean more time between two batches and potentially degraded performances.
37+
The default value of 10ms offers a good balance between batch size and maximum delay between two batches and should be suitable for most situations.
38+
:::
39+
40+
**Example:**
41+
42+
```js
43+
import { BatchController, Kuzzle, Http } from 'kuzzle';
44+
45+
const sdk = new Kuzzle(new Http('localhost'));
46+
47+
const batch = new BatchController(sdk);
48+
49+
// Same as sdk.document.exists but executed in a batch
50+
const exists = await batch.exists('city', 'galle', 'dana');
51+
52+
if (exists) {
53+
// Same as sdk.document.update but executed in a batch
54+
await batch.update('city', 'galle', 'dana', { power: 'off' });
55+
}
56+
else {
57+
// Same as sdk.document.create but executed in a batch
58+
await batch.create('city', 'galle', { power: 'off' }, 'dana');
59+
}
60+
61+
// Original sdk.document.search method
62+
const results = await batch.search('city', 'galle', {});
63+
```
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
---
2+
code: false
3+
type: page
4+
title: Batch Processing
5+
description: Process batches of data with the SDK
6+
order: 600
7+
---
8+
9+
# Batch Processing
10+
11+
Most of the methods of the Document controller have a batch alternative:
12+
- create => mCreate
13+
- replace => mReplace
14+
- createOrReplace => mCreateOrReplace
15+
- update => mUpdate
16+
- get => mGet
17+
- exists => mGet
18+
- delete => mDelete
19+
20+
Those methods can be used to process batches of documents at once and increase performances.
21+
22+
## BatchController
23+
24+
Although the m* methods offer very good performances when handling documents, they will need a refactor of the code and architecture.
25+
26+
Instead the [BatchController](/sdk/js/7/core-classes/batch-controller/introduction) provides a consistent way to deal with documents per batch.
27+
28+
It overloads the original DocumentController but methods will be executed in batch at a fixed interval.
29+
30+
The BatchController is usable without modifying the original code, just by replacing the original calls to `document.*` to `batch.*`
31+
32+
**Example:**
33+
34+
```js
35+
import { BatchController, Kuzzle, Http } from 'kuzzle';
36+
37+
const sdk = new Kuzzle(new Http('localhost'));
38+
39+
const batch = new BatchController(sdk);
40+
41+
// Same as sdk.document.exists but executed in a batch
42+
const exists = await batch.exists('city', 'galle', 'dana');
43+
44+
if (exists) {
45+
// Same as sdk.document.update but executed in a batch
46+
await batch.update('city', 'galle', 'dana', { power: 'off' });
47+
}
48+
else {
49+
// Same as sdk.document.create but executed in a batch
50+
await batch.create('city', 'galle', { power: 'off' }, 'dana');
51+
}
52+
53+
// Original sdk.document.search method
54+
const results = await batch.search('city', 'galle', {});
55+
```
56+
57+
::: warning
58+
Standard API errors will not be available.
59+
Except for the `services.storage.not_found` error.
60+
:::
61+
62+
By default, the BatchController send a batch of document every 10ms. This can be configured when instantiating the BatchController through the `options.interval` [constructor](/sdk/js/7/core-classes/batch-controller/constructor) parameter.
63+
64+
::: info
65+
Depending on your load, you may want to increase the timer interval to execute bigger batch.
66+
A bigger interval will also mean more time between two batch and potentialy degraded performances.
67+
The default value of 10ms offer a good balance between batch size and maximum delay between two batch and should be suitable for most situations.
68+
:::
69+
70+

index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ export * from './src/core/searchResult/Specifications';
2121
export * from './src/core/searchResult/User';
2222
export * from './src/core/Observer';
2323
export * from './src/core/RealtimeDocument';
24+
export * from './src/core/batchWriter/BatchController';
2425

2526
export * from './src/types';
2627

src/KuzzleError.ts

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import { hilightUserCode } from './utils/stackTrace';
44
import { RequestPayload } from './types/RequestPayload';
5+
import { JSONObject } from './types';
56

67
/**
78
* Standard Kuzzle error.
@@ -81,7 +82,7 @@ export class KuzzleError extends Error {
8182
* The SDK stack is needed alongside the protocol used.
8283
* Those information will allow to construct a enhanced stacktrace:
8384
*
84-
* BadRequestError: lol
85+
* BadRequestError: Trololol
8586
at new BadRequestError (/home/aschen/projets/kuzzleio/kuzzle/lib/kerror/errors/badRequestError.ts:26:5)
8687
> at BaseController.handler [as sayHello] (/home/aschen/projets/kuzzleio/kuzzle/test.js:9:15)
8788
at doAction (/home/aschen/projets/kuzzleio/kuzzle/lib/api/funnel.js:759:47)
@@ -96,13 +97,18 @@ export class KuzzleError extends Error {
9697
> at /home/aschen/projets/kuzzleio/sdk-javascript/test.js:8:18
9798
at processTicksAndRejections (internal/process/task_queues.js:97:5)
9899
*/
99-
constructor (apiError, sdkStack: string, protocol: string, request?: RequestPayload) {
100+
constructor (
101+
apiError: { message: string, status?: number, id?: string, code?: number, errors?: JSONObject[], count?: number, stack?: string },
102+
sdkStack?: string,
103+
protocol?: string,
104+
request?: RequestPayload,
105+
) {
100106
super(apiError.message);
101-
this.status = apiError.status;
102107

108+
this.status = apiError.status;
103109
this.id = apiError.id;
104110
this.code = apiError.code;
105-
111+
106112
if (request) {
107113
this.controller = request.controller;
108114
this.collection = request.collection;
@@ -135,10 +141,12 @@ export class KuzzleError extends Error {
135141
}
136142

137143
// Append the SDK stacktrace
138-
this.stack += sdkStack
139-
.split('\n')
140-
.map(hilightUserCode)
141-
.slice(1)
142-
.join('\n');
144+
if (sdkStack) {
145+
this.stack += sdkStack
146+
.split('\n')
147+
.map(hilightUserCode)
148+
.slice(1)
149+
.join('\n');
150+
}
143151
}
144152
}

src/controllers/Document.ts

Lines changed: 31 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -66,11 +66,10 @@ export class DocumentController extends BaseController {
6666
* @param collection Collection name
6767
* @param content Document content
6868
* @param _id Optional document ID
69-
* @param options Additional options
70-
* - `queuable` If true, queues the request during downtime, until connected to Kuzzle again
71-
* - `refresh` If set to `wait_for`, Kuzzle will not respond until the API key is indexed
72-
* - `silent` If true, then Kuzzle will not generate notifications
73-
* - `timeout` Request Timeout in ms, after the delay if not resolved the promise will be rejected
69+
* @param options.queuable If true, queues the request during downtime, until connected to Kuzzle again
70+
* @param options.refresh If set to `wait_for`, Kuzzle will not respond until the API key is indexed
71+
* @param options.silent If true, then Kuzzle will not generate notifications
72+
* @param options.timeout Request Timeout in ms, after the delay if not resolved the promise will be rejected
7473
*
7574
* @returns The created document
7675
*/
@@ -104,11 +103,10 @@ export class DocumentController extends BaseController {
104103
* @param collection Collection name
105104
* @param id Document ID
106105
* @param content Document content
107-
* @param options Additional options
108-
* - `queuable` If true, queues the request during downtime, until connected to Kuzzle again
109-
* - `refresh` If set to `wait_for`, Kuzzle will not respond until the API key is indexed
110-
* - `silent` If true, then Kuzzle will not generate notifications
111-
* - `timeout` Request Timeout in ms, after the delay if not resolved the promise will be rejected
106+
* @param options.queuable If true, queues the request during downtime, until connected to Kuzzle again
107+
* @param options.refresh If set to `wait_for`, Kuzzle will not respond until the API key is indexed
108+
* @param options.silent If true, then Kuzzle will not generate notifications
109+
* @param options.timeout Request Timeout in ms, after the delay if not resolved the promise will be rejected
112110
*
113111
* @returns The created or replaced document
114112
*/
@@ -141,10 +139,10 @@ export class DocumentController extends BaseController {
141139
* @param collection Collection name
142140
* @param _id Document ID
143141
* @param options Additional options
144-
* - `queuable` If true, queues the request during downtime, until connected to Kuzzle again
145-
* - `refresh` If set to `wait_for`, Kuzzle will not respond until the API key is indexed
146-
* - `silent` If true, then Kuzzle will not generate notifications
147-
* - `timeout` Request Timeout in ms, after the delay if not resolved the promise will be rejected
142+
* @param options.queuable If true, queues the request during downtime, until connected to Kuzzle again
143+
* @param options.refresh If set to `wait_for`, Kuzzle will not respond until the API key is indexed
144+
* @param options.silent If true, then Kuzzle will not generate notifications
145+
* @param options.timeout Request Timeout in ms, after the delay if not resolved the promise will be rejected
148146
*
149147
* @returns The document ID
150148
*/
@@ -153,7 +151,7 @@ export class DocumentController extends BaseController {
153151
collection: string,
154152
_id: string,
155153
options: ArgsDocumentControllerDelete = {}
156-
): Promise<number> {
154+
): Promise<string> {
157155
const request = {
158156
index,
159157
collection,
@@ -249,10 +247,10 @@ export class DocumentController extends BaseController {
249247
* @param collection Collection name
250248
* @param _id Document ID
251249
* @param options Additional options
252-
* - `queuable` If true, queues the request during downtime, until connected to Kuzzle again
253-
* - `refresh` If set to `wait_for`, Kuzzle will not respond until the API key is indexed
254-
* - `silent` If true, then Kuzzle will not generate notifications
255-
* - `timeout` Request Timeout in ms, after the delay if not resolved the promise will be rejected
250+
* @param options.queuable If true, queues the request during downtime, until connected to Kuzzle again
251+
* @param options.refresh If set to `wait_for`, Kuzzle will not respond until the API key is indexed
252+
* @param options.silent If true, then Kuzzle will not generate notifications
253+
* @param options.timeout Request Timeout in ms, after the delay if not resolved the promise will be rejected
256254
*
257255
* @returns True if the document exists
258256
*/
@@ -282,10 +280,10 @@ export class DocumentController extends BaseController {
282280
* @param collection Collection name
283281
* @param _id Document ID
284282
* @param options Additional options
285-
* - `queuable` If true, queues the request during downtime, until connected to Kuzzle again
286-
* - `refresh` If set to `wait_for`, Kuzzle will not respond until the API key is indexed
287-
* - `silent` If true, then Kuzzle will not generate notifications
288-
* - `timeout` Request Timeout in ms, after the delay if not resolved the promise will be rejected
283+
* @param options.queuable If true, queues the request during downtime, until connected to Kuzzle again
284+
* @param options.refresh If set to `wait_for`, Kuzzle will not respond until the API key is indexed
285+
* @param options.silent If true, then Kuzzle will not generate notifications
286+
* @param options.timeout Request Timeout in ms, after the delay if not resolved the promise will be rejected
289287
*
290288
* @returns The document
291289
*/
@@ -577,11 +575,10 @@ export class DocumentController extends BaseController {
577575
* @param collection Collection name
578576
* @param id Document ID
579577
* @param content Document content
580-
* @param options Additional options
581-
* - `queuable` If true, queues the request during downtime, until connected to Kuzzle again
582-
* - `refresh` If set to `wait_for`, Kuzzle will not respond until the API key is indexed
583-
* - `silent` If true, then Kuzzle will not generate notifications
584-
* - `timeout` Request Timeout in ms, after the delay if not resolved the promise will be rejected
578+
* @param options.queuable If true, queues the request during downtime, until connected to Kuzzle again
579+
* @param options.refresh If set to `wait_for`, Kuzzle will not respond until the API key is indexed
580+
* @param options.silent If true, then Kuzzle will not generate notifications
581+
* @param options.timeout Request Timeout in ms, after the delay if not resolved the promise will be rejected
585582
*
586583
* @returns The replaced document
587584
*/
@@ -674,13 +671,12 @@ export class DocumentController extends BaseController {
674671
* @param collection Collection name
675672
* @param id Document ID
676673
* @param content Document content
677-
* @param options Additional options
678-
* - `queuable` If true, queues the request during downtime, until connected to Kuzzle again
679-
* - `refresh` If set to `wait_for`, Kuzzle will not respond until the API key is indexed
680-
* - `silent` If true, then Kuzzle will not generate notifications
681-
* - `retryOnConflict` Number of times the database layer should retry in case of version conflict
682-
* - `source` If true, returns the updated document inside the response
683-
* - `timeout` Request Timeout in ms, after the delay if not resolved the promise will be rejected
674+
* @param options.queuable If true, queues the request during downtime, until connected to Kuzzle again
675+
* @param options.refresh If set to `wait_for`, Kuzzle will not respond until the API key is indexed
676+
* @param options.silent If true, then Kuzzle will not generate notifications
677+
* @param options.retryOnConflict Number of times the database layer should retry in case of version conflict
678+
* @param options.source If true, returns the updated document inside the response
679+
* @param options.timeout Request Timeout in ms, after the delay if not resolved the promise will be rejected
684680
*
685681
* @returns The replaced document
686682
*/

src/core/InstrumentablePromise.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
export class InstrumentablePromise {
2+
public promise: Promise<any>;
3+
public resolve: (...any) => any;
4+
public reject: (...any) => any;
5+
6+
constructor () {
7+
this.promise = new Promise((resolve, reject) => {
8+
this.resolve = resolve;
9+
this.reject = reject;
10+
});
11+
}
12+
}

0 commit comments

Comments
 (0)