@miso.ai/server-sdk 0.6.0-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/cli/delete.js +85 -0
- package/cli/ids.js +32 -0
- package/cli/index.js +102 -0
- package/cli/upload.js +114 -0
- package/package.json +26 -0
- package/src/client.js +124 -0
- package/src/index.js +2 -0
- package/src/logger/api-progress.js +106 -0
- package/src/logger/constants.js +5 -0
- package/src/logger/delete-progress.js +22 -0
- package/src/logger/index.js +41 -0
- package/src/logger/progress.legacy.js +60 -0
- package/src/logger/standard.js +58 -0
- package/src/stream/api-sink.js +66 -0
- package/src/stream/delete-sink.js +26 -0
- package/src/stream/delete.js +111 -0
- package/src/stream/deletion-state.js +32 -0
- package/src/stream/service-stats.js +43 -0
- package/src/stream/upload-buffer.js +34 -0
- package/src/stream/upload-sink.js +104 -0
- package/src/stream/upload.js +81 -0
- package/src/stream/upload.legacy.js +410 -0
- package/src/version.js +1 -0
|
@@ -0,0 +1,410 @@
|
|
|
1
|
+
import { Transform } from 'stream';
|
|
2
|
+
import { trimObj, log } from '@miso.ai/server-commons';
|
|
3
|
+
import version from '../version.js';
|
|
4
|
+
|
|
5
|
+
function getDefaultRecordsPerRequest(type) {
|
|
6
|
+
switch (type) {
|
|
7
|
+
case 'interactions':
|
|
8
|
+
return 1000;
|
|
9
|
+
default:
|
|
10
|
+
return 200;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function normalizeParams(params = []) {
|
|
15
|
+
return params.reduce((acc, param) => {
|
|
16
|
+
const [key, value = '1'] = param.split('=');
|
|
17
|
+
acc[key] = value;
|
|
18
|
+
return acc;
|
|
19
|
+
}, {});
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const PAYLOAD_PREFIX = '{"data":[';
|
|
23
|
+
const PAYLOAD_SUFFIX = ']}';
|
|
24
|
+
const PAYLOAD_OVERHEAD_BYTES = (PAYLOAD_PREFIX.length + PAYLOAD_SUFFIX.length) * 2;
|
|
25
|
+
|
|
26
|
+
const MIN_HREATBEAT = 100;
|
|
27
|
+
|
|
28
|
+
const requestPromises = new WeakMap();
|
|
29
|
+
|
|
30
|
+
export default class LegacyUploadStream extends Transform {
|
|
31
|
+
|
|
32
|
+
constructor(client, type, {
|
|
33
|
+
name,
|
|
34
|
+
objectMode,
|
|
35
|
+
async,
|
|
36
|
+
dryRun,
|
|
37
|
+
params,
|
|
38
|
+
heartbeat,
|
|
39
|
+
recordsPerRequest,
|
|
40
|
+
bytesPerRequest,
|
|
41
|
+
bytesPerSecond,
|
|
42
|
+
experimentId,
|
|
43
|
+
} = {}) {
|
|
44
|
+
super({
|
|
45
|
+
readableObjectMode: true,
|
|
46
|
+
writableObjectMode: objectMode,
|
|
47
|
+
});
|
|
48
|
+
this._client = client;
|
|
49
|
+
this._type = type;
|
|
50
|
+
this._options = {
|
|
51
|
+
name,
|
|
52
|
+
objectMode: !!objectMode,
|
|
53
|
+
async: !!async,
|
|
54
|
+
dryRun: !!dryRun,
|
|
55
|
+
params: normalizeParams(params),
|
|
56
|
+
heartbeat,
|
|
57
|
+
recordsPerRequest: recordsPerRequest || getDefaultRecordsPerRequest(type),
|
|
58
|
+
bytesPerRequest: bytesPerRequest || 1024 * 1024,
|
|
59
|
+
bytesPerSecond: bytesPerSecond || 4 * 1024 * 1024,
|
|
60
|
+
experimentId,
|
|
61
|
+
};
|
|
62
|
+
if (heartbeat !== undefined && (typeof heartbeat !== 'number') || heartbeat < MIN_HREATBEAT) {
|
|
63
|
+
throw new Error(`Heartbeat must be a number at least ${MIN_HREATBEAT}: ${heartbeat}`);
|
|
64
|
+
}
|
|
65
|
+
this._singleRecordPerRequest = false;
|
|
66
|
+
|
|
67
|
+
switch (type) {
|
|
68
|
+
case 'experiment-events':
|
|
69
|
+
if (!experimentId) {
|
|
70
|
+
throw new Error(`Experiment id is required for experiment APIs`);
|
|
71
|
+
}
|
|
72
|
+
this._singleRecordPerRequest = true;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
this._state = new State();
|
|
76
|
+
this._resetBuffer();
|
|
77
|
+
// log functions
|
|
78
|
+
for (const level of log.LEVELS) {
|
|
79
|
+
this[`_${level}`] = this._log.bind(this, level);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
_construct(done) {
|
|
84
|
+
const { config } = this;
|
|
85
|
+
this._info('construct', { config });
|
|
86
|
+
const heartbeat = this._options.heartbeat;
|
|
87
|
+
if (heartbeat) {
|
|
88
|
+
this._heartbeatIntervalId = setInterval(this._heartbeat.bind(this), heartbeat);
|
|
89
|
+
}
|
|
90
|
+
done();
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
_transform(record, _, next) {
|
|
94
|
+
this._pushStartEventIfNecessary();
|
|
95
|
+
const { objectMode, bytesPerRequest } = this._options;
|
|
96
|
+
const str = objectMode ? JSON.stringify(record) : record;
|
|
97
|
+
const newSize = str.length * 2;
|
|
98
|
+
|
|
99
|
+
if (this._singleRecordPerRequest) {
|
|
100
|
+
this._buffer.content = str;
|
|
101
|
+
this._buffer.bytes = newSize;
|
|
102
|
+
this._buffer.records = 1;
|
|
103
|
+
this._dispatch();
|
|
104
|
+
|
|
105
|
+
} else {
|
|
106
|
+
if (this._buffer.records && this._buffer.bytes + newSize >= bytesPerRequest) {
|
|
107
|
+
// flush previous records if this record is large enough to exceed BPR threshold
|
|
108
|
+
this._dispatch();
|
|
109
|
+
}
|
|
110
|
+
if (this._buffer.records > 0) {
|
|
111
|
+
this._buffer.content += ',';
|
|
112
|
+
}
|
|
113
|
+
this._buffer.content += str;
|
|
114
|
+
this._buffer.bytes += newSize;
|
|
115
|
+
this._buffer.records++;
|
|
116
|
+
|
|
117
|
+
this._dispatchIfNecessary();
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const restTime = this._state.restTime(this._getBpsLimit());
|
|
121
|
+
if (restTime > 0) {
|
|
122
|
+
this._debug('rest', { restTime });
|
|
123
|
+
setTimeout(next, restTime);
|
|
124
|
+
} else if (this._state._pending.length > 15) {
|
|
125
|
+
// TODO: figure out best strategy on this
|
|
126
|
+
// release event loop for downstream
|
|
127
|
+
setTimeout(next);
|
|
128
|
+
} else {
|
|
129
|
+
next();
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
_log(level, event, args = {}) {
|
|
134
|
+
this.push(trimObj({
|
|
135
|
+
name: this._options.name,
|
|
136
|
+
level,
|
|
137
|
+
event,
|
|
138
|
+
timestamp: Date.now(),
|
|
139
|
+
...args,
|
|
140
|
+
state: this.state,
|
|
141
|
+
}));
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
_heartbeat() {
|
|
145
|
+
this._log(log.DEBUG, 'heartbeat');
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
async _flush(done) {
|
|
149
|
+
this._pushStartEventIfNecessary();
|
|
150
|
+
this._dispatch();
|
|
151
|
+
await Promise.all(this._state.pending.map(r => requestPromises.get(r)));
|
|
152
|
+
const { successful, failed } = this.state;
|
|
153
|
+
|
|
154
|
+
if (this._heartbeatIntervalId) {
|
|
155
|
+
clearInterval(this._heartbeatIntervalId);
|
|
156
|
+
delete this._heartbeatIntervalId;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
this._info('end', { successful, failed });
|
|
160
|
+
done();
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
get state() {
|
|
164
|
+
return this._state.export();
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
get config() {
|
|
168
|
+
return Object.freeze({
|
|
169
|
+
version,
|
|
170
|
+
type: this._type,
|
|
171
|
+
...this._client.options,
|
|
172
|
+
...this._options,
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// helper //
|
|
177
|
+
_pushStartEventIfNecessary() {
|
|
178
|
+
if (this._state.next.request === 0 && this._buffer.records === 0) {
|
|
179
|
+
this._info('start');
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
_dispatchIfNecessary() {
|
|
184
|
+
const { records, bytes } = this._buffer;
|
|
185
|
+
const { recordsPerRequest, bytesPerRequest } = this._options;
|
|
186
|
+
if (records > 0 && (records >= recordsPerRequest || bytes >= bytesPerRequest)) {
|
|
187
|
+
this._dispatch();
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
_dispatch() {
|
|
192
|
+
const singleRecord = this._singleRecordPerRequest;
|
|
193
|
+
const { records, bytes, content } = this._resetBuffer();
|
|
194
|
+
if (records === 0) {
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
const request = this._state.createRequest(records, bytes);
|
|
198
|
+
|
|
199
|
+
let requestResolve;
|
|
200
|
+
requestPromises.set(request, new Promise(r => {
|
|
201
|
+
requestResolve = r;
|
|
202
|
+
}));
|
|
203
|
+
|
|
204
|
+
this._debug('request', { request });
|
|
205
|
+
|
|
206
|
+
this._state.open(request);
|
|
207
|
+
|
|
208
|
+
const payload = singleRecord ? content : content + PAYLOAD_SUFFIX;
|
|
209
|
+
|
|
210
|
+
(async () => {
|
|
211
|
+
let response;
|
|
212
|
+
try {
|
|
213
|
+
response = await this._upload(payload);
|
|
214
|
+
} catch(error) {
|
|
215
|
+
response = !error.response ? trimObj({ errors: true, cause: error.message }) :
|
|
216
|
+
typeof error.response.data !== 'object' ? trimObj({ errors: true, cause: error.response.data }) :
|
|
217
|
+
error.response.data;
|
|
218
|
+
}
|
|
219
|
+
response.timestamp = Date.now();
|
|
220
|
+
response.took = response.took || 0; // TODO: ad-hoc
|
|
221
|
+
|
|
222
|
+
requestResolve();
|
|
223
|
+
this._state.close(request, response);
|
|
224
|
+
|
|
225
|
+
const failed = response.errors;
|
|
226
|
+
|
|
227
|
+
(failed ? this._error : this._debug)('response', { request, response, payload: failed ? JSON.parse(payload) : undefined });
|
|
228
|
+
this._info('upload', {
|
|
229
|
+
result: failed ? 'failed' : 'successful',
|
|
230
|
+
index: request.index,
|
|
231
|
+
records: request.records,
|
|
232
|
+
bytes: request.bytes,
|
|
233
|
+
took: response.took,
|
|
234
|
+
latency: response.timestamp - request.timestamp - response.took
|
|
235
|
+
});
|
|
236
|
+
})();
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
async _upload(payload) {
|
|
240
|
+
switch (this._type) {
|
|
241
|
+
case 'experiment-events':
|
|
242
|
+
const { experimentId } = this._options;
|
|
243
|
+
return (await this._client.uploadExperimentEvent(experimentId, payload)).data;
|
|
244
|
+
default:
|
|
245
|
+
const { async, dryRun, params } = this._options;
|
|
246
|
+
const response = await this._client.upload(this._type, payload, { async, dryRun, params });
|
|
247
|
+
return response.data;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
_resetBuffer() {
|
|
252
|
+
const buffer = { ...this._buffer };
|
|
253
|
+
this._buffer = {
|
|
254
|
+
records: 0,
|
|
255
|
+
bytes: PAYLOAD_OVERHEAD_BYTES,
|
|
256
|
+
content: PAYLOAD_PREFIX,
|
|
257
|
+
};
|
|
258
|
+
return buffer;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Note that when API is effectively in async mode, apiBps will be overestimated, but there is no harm to respect it.
|
|
263
|
+
*/
|
|
264
|
+
_getBpsLimit() {
|
|
265
|
+
const { bytesPerSecond } = this._options;
|
|
266
|
+
const { pending, apiBps, completed } = this.state;
|
|
267
|
+
// TODO: threshold to switch to real API BPS should be based on bytes, not requests
|
|
268
|
+
// respect API BPS only when
|
|
269
|
+
// 1. has pending requests
|
|
270
|
+
// 2. has enouch data points from completed requests
|
|
271
|
+
return pending.length > 0 && completed.requests > 9 && !isNaN(apiBps) && apiBps < bytesPerSecond ? apiBps : bytesPerSecond;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
class State {
|
|
277
|
+
|
|
278
|
+
constructor() {
|
|
279
|
+
this._start = Date.now();
|
|
280
|
+
this._next = Object.freeze({
|
|
281
|
+
request: 0,
|
|
282
|
+
record: 0,
|
|
283
|
+
});
|
|
284
|
+
this._pending = [];
|
|
285
|
+
this._successful = {
|
|
286
|
+
requests: 0,
|
|
287
|
+
records: 0,
|
|
288
|
+
bytes: 0,
|
|
289
|
+
took: 0,
|
|
290
|
+
latency: 0,
|
|
291
|
+
};
|
|
292
|
+
this._failed = {
|
|
293
|
+
requests: 0,
|
|
294
|
+
records: 0,
|
|
295
|
+
bytes: 0,
|
|
296
|
+
took: 0,
|
|
297
|
+
latency: 0,
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
get start() {
|
|
302
|
+
return this._start;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
get next() {
|
|
306
|
+
return this._next;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
get pending() {
|
|
310
|
+
return Object.freeze(this._pending.slice());
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
get successful() {
|
|
314
|
+
return Object.freeze({ ...this._successful });
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
get failed() {
|
|
318
|
+
return Object.freeze({ ...this._failed });
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
elapsed(time) {
|
|
322
|
+
return (time || Date.now()) - this.start;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
createRequest(records, bytes) {
|
|
326
|
+
const { next } = this;
|
|
327
|
+
const request = Object.freeze({
|
|
328
|
+
index: next.request,
|
|
329
|
+
recordOffset: next.record,
|
|
330
|
+
records,
|
|
331
|
+
bytes,
|
|
332
|
+
timestamp: Date.now(),
|
|
333
|
+
});
|
|
334
|
+
this._next = Object.freeze({
|
|
335
|
+
request: next.request + 1,
|
|
336
|
+
record: next.record + records,
|
|
337
|
+
});
|
|
338
|
+
return request;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
get sent() {
|
|
342
|
+
const { _pending, completed } = this;
|
|
343
|
+
return Object.freeze(_pending.reduce((acc, request) => {
|
|
344
|
+
acc.requests++;
|
|
345
|
+
acc.records += request.records;
|
|
346
|
+
acc.bytes += request.bytes;
|
|
347
|
+
return acc;
|
|
348
|
+
}, {
|
|
349
|
+
requests: completed.requests,
|
|
350
|
+
records: completed.records,
|
|
351
|
+
bytes: completed.bytes,
|
|
352
|
+
}));
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
get completed() {
|
|
356
|
+
const { _successful, _failed } = this;
|
|
357
|
+
return Object.freeze({
|
|
358
|
+
requests: _successful.requests + _failed.requests,
|
|
359
|
+
records: _successful.records + _failed.records,
|
|
360
|
+
bytes: _successful.bytes + _failed.bytes,
|
|
361
|
+
took: _successful.took + _failed.took,
|
|
362
|
+
latency: _successful.latency + _failed.latency,
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
sentBps(timestamp) {
|
|
367
|
+
const { sent } = this;
|
|
368
|
+
const elapsed = this.elapsed(timestamp);
|
|
369
|
+
return elapsed > 0 ? sent.bytes / elapsed * 1000 : NaN;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
get apiBps() {
|
|
373
|
+
const { _successful, _failed } = this;
|
|
374
|
+
const took = _successful.took + _failed.took;
|
|
375
|
+
const bytes = _successful.bytes + _failed.bytes;
|
|
376
|
+
return took > 0 ? bytes / took * 1000 : NaN;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
restTime(bps) {
|
|
380
|
+
const elapsed = this.elapsed();
|
|
381
|
+
const { sent } = this;
|
|
382
|
+
return Math.max(0, sent.bytes / bps * 1000 - elapsed) * 1.05;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
export(timestamp) {
|
|
386
|
+
const { next, pending, successful, failed, sent, completed, apiBps } = this;
|
|
387
|
+
timestamp = timestamp || Date.now();
|
|
388
|
+
return Object.freeze({
|
|
389
|
+
next, pending, sent, successful, failed, completed,
|
|
390
|
+
elapsed: this.elapsed(timestamp),
|
|
391
|
+
apiBps,
|
|
392
|
+
sentBps: this.sentBps(timestamp),
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
open(request) {
|
|
397
|
+
this._pending.push(request);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
close(request, response) {
|
|
401
|
+
this._pending = this._pending.filter(r => r.index !== request.index);
|
|
402
|
+
const category = response.errors ? this._failed : this._successful;
|
|
403
|
+
category.requests++;
|
|
404
|
+
category.records += request.records;
|
|
405
|
+
category.bytes += request.bytes;
|
|
406
|
+
category.took += response.took;
|
|
407
|
+
category.latency += response.timestamp - request.timestamp - response.took;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
}
|
package/src/version.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export default '0.6.0-beta.0';
|