@nxtedition/lib 27.0.3 → 27.0.6
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/app.js +7 -16
- package/couch.js +69 -23
- package/numa.js +64 -30
- package/package.json +16 -11
- package/time.js +3 -13
package/app.js
CHANGED
|
@@ -562,7 +562,7 @@ export function makeApp(appConfig, onTerminateOrMeta, metaOrNull) {
|
|
|
562
562
|
|
|
563
563
|
if (process.platform === 'linux' && appConfig.numa != null && appConfig.numa !== '') {
|
|
564
564
|
let numa = appConfig.numa
|
|
565
|
-
if (numa == 'auto' || numa === true) {
|
|
565
|
+
if (numa == 'auto' || numa === true || numa === 'hash') {
|
|
566
566
|
numa = hashString(
|
|
567
567
|
JSON.stringify({ serviceName, serviceModule, serviceInstanceId, serviceWorkerId }),
|
|
568
568
|
)
|
|
@@ -577,33 +577,24 @@ export function makeApp(appConfig, onTerminateOrMeta, metaOrNull) {
|
|
|
577
577
|
.observe(`${config.hostname}:monitor.stats?`, ds.record.PROVIDER)
|
|
578
578
|
.pipe(
|
|
579
579
|
rxjs.map((stats) => stats?.net?.bonding?.[0]?.numaNode ?? null),
|
|
580
|
-
rxjs.filter((n) => Number.isInteger(n) && n >= 0),
|
|
581
|
-
rxjs.timeout({
|
|
582
|
-
first: 10e3,
|
|
583
|
-
with: () => rxjs.of(null),
|
|
584
|
-
}),
|
|
585
580
|
rxjs.distinctUntilChanged(),
|
|
586
581
|
rxjs.retry({
|
|
587
|
-
resetOnSuccess: true,
|
|
588
582
|
delay(err, retryCount) {
|
|
589
583
|
logger.error({ err, retryCount }, 'net numa failed')
|
|
590
584
|
return rxjs.timer(10e3)
|
|
591
585
|
},
|
|
592
586
|
}),
|
|
593
587
|
)
|
|
594
|
-
.subscribe((
|
|
595
|
-
if (
|
|
588
|
+
.subscribe((numa) => {
|
|
589
|
+
if (numa != null) {
|
|
596
590
|
try {
|
|
597
|
-
const affinity = numa.setAffinity(
|
|
598
|
-
logger.debug(
|
|
599
|
-
{ hostname: config.hostname, numaNode, affinity },
|
|
600
|
-
'net numa succeeded',
|
|
601
|
-
)
|
|
591
|
+
const affinity = numa.setAffinity(numa)
|
|
592
|
+
logger.debug({ hostname: config.hostname, numa, affinity }, 'net numa succeeded')
|
|
602
593
|
} catch (err) {
|
|
603
|
-
logger.error({ err, hostname: config.hostname,
|
|
594
|
+
logger.error({ err, hostname: config.hostname, numa }, 'net numa failed')
|
|
604
595
|
}
|
|
605
596
|
} else {
|
|
606
|
-
logger.warn({ hostname: config.hostname,
|
|
597
|
+
logger.warn({ hostname: config.hostname, numa }, 'net numa missing')
|
|
607
598
|
}
|
|
608
599
|
})
|
|
609
600
|
}
|
package/couch.js
CHANGED
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
import assert from 'node:assert'
|
|
2
2
|
import stream from 'node:stream'
|
|
3
3
|
import querystring from 'node:querystring'
|
|
4
|
+
import tp from 'node:timers/promises'
|
|
5
|
+
|
|
4
6
|
import createError from 'http-errors'
|
|
5
|
-
import { makeWeakCache } from '@nxtedition/weak-cache'
|
|
6
|
-
import { defaultDelay as delay } from './http.js'
|
|
7
7
|
import urljoin from 'url-join'
|
|
8
|
-
import { AbortError } from './errors.js'
|
|
9
|
-
import { dispatch, Agent, Pool, request as undiciRequest } from '@nxtedition/nxt-undici'
|
|
10
8
|
import urlJoin from 'url-join'
|
|
11
9
|
|
|
10
|
+
import { dispatch, Agent, Pool, request as undiciRequest } from '@nxtedition/nxt-undici'
|
|
11
|
+
import { makeWeakCache } from '@nxtedition/weak-cache'
|
|
12
|
+
|
|
13
|
+
import { AbortError } from './errors.js'
|
|
14
|
+
|
|
12
15
|
export function makeCouch(opts) {
|
|
13
16
|
let config
|
|
14
17
|
if (typeof opts === 'string') {
|
|
@@ -249,6 +252,7 @@ export function makeCouch(opts) {
|
|
|
249
252
|
blocking = live || !params.limit || params.limit > 256,
|
|
250
253
|
}) {
|
|
251
254
|
let retryCount = 0
|
|
255
|
+
let retryTime = 0
|
|
252
256
|
while (true) {
|
|
253
257
|
let src
|
|
254
258
|
try {
|
|
@@ -279,6 +283,7 @@ export function makeCouch(opts) {
|
|
|
279
283
|
const ures = await undiciRequest(ureq)
|
|
280
284
|
|
|
281
285
|
retryCount = 0
|
|
286
|
+
retryTime = 0
|
|
282
287
|
|
|
283
288
|
src = ures.body
|
|
284
289
|
|
|
@@ -288,6 +293,7 @@ export function makeCouch(opts) {
|
|
|
288
293
|
let error = null
|
|
289
294
|
let ended = false
|
|
290
295
|
let state = 0
|
|
296
|
+
let since = params.since || '0'
|
|
291
297
|
|
|
292
298
|
function maybeResume() {
|
|
293
299
|
if (resume) {
|
|
@@ -328,8 +334,8 @@ export function makeCouch(opts) {
|
|
|
328
334
|
for (const line of lines) {
|
|
329
335
|
if (line === '') {
|
|
330
336
|
// hearbeat
|
|
331
|
-
const ret =
|
|
332
|
-
ret.lastSeq = params.since
|
|
337
|
+
const ret = changes.splice(0)
|
|
338
|
+
ret.lastSeq = params.since = since
|
|
333
339
|
yield ret
|
|
334
340
|
} else if (line === ',') {
|
|
335
341
|
// Do nothing. Couch sometimes insert new line between
|
|
@@ -337,9 +343,9 @@ export function makeCouch(opts) {
|
|
|
337
343
|
} else if (live) {
|
|
338
344
|
const data = JSON.parse(line)
|
|
339
345
|
if (data.last_seq) {
|
|
340
|
-
|
|
346
|
+
since = data.last_seq
|
|
341
347
|
} else {
|
|
342
|
-
|
|
348
|
+
since = data.seq || since
|
|
343
349
|
changes.push(data)
|
|
344
350
|
}
|
|
345
351
|
} else {
|
|
@@ -358,7 +364,7 @@ export function makeCouch(opts) {
|
|
|
358
364
|
try {
|
|
359
365
|
assert(idx >= 0, 'invalid row: ' + idx + ' ' + line)
|
|
360
366
|
const change = JSON.parse(line.slice(0, idx))
|
|
361
|
-
|
|
367
|
+
since = change.seq || since
|
|
362
368
|
changes.push(change)
|
|
363
369
|
} catch (err) {
|
|
364
370
|
throw Object.assign(err, { data: line })
|
|
@@ -373,7 +379,7 @@ export function makeCouch(opts) {
|
|
|
373
379
|
}
|
|
374
380
|
} else if (changes.length) {
|
|
375
381
|
const ret = changes.splice(0)
|
|
376
|
-
ret.lastSeq = params.since
|
|
382
|
+
ret.lastSeq = params.since = since
|
|
377
383
|
yield ret
|
|
378
384
|
} else if (error) {
|
|
379
385
|
throw error
|
|
@@ -382,26 +388,66 @@ export function makeCouch(opts) {
|
|
|
382
388
|
resume = resolve
|
|
383
389
|
})
|
|
384
390
|
} else {
|
|
385
|
-
const ret =
|
|
386
|
-
ret.lastSeq = params.since
|
|
391
|
+
const ret = changes.splice(0)
|
|
392
|
+
ret.lastSeq = params.since = since
|
|
387
393
|
yield ret
|
|
388
394
|
return
|
|
389
395
|
}
|
|
390
396
|
}
|
|
391
397
|
} catch (err) {
|
|
392
|
-
if (err.name === 'AbortError') {
|
|
398
|
+
if (err.name === 'AbortError' || !retry) {
|
|
393
399
|
throw err
|
|
394
|
-
} else if (typeof retry === 'function') {
|
|
395
|
-
const retryState = { since: params.since }
|
|
396
|
-
Object.assign(
|
|
397
|
-
retryState,
|
|
398
|
-
await retry(err, retryCount++, retryState, { signal, logger }, () =>
|
|
399
|
-
delay(err, retryCount, { signal, logger }),
|
|
400
|
-
),
|
|
401
|
-
)
|
|
402
|
-
params.since = retryState.since ?? 0
|
|
403
400
|
} else {
|
|
404
|
-
|
|
401
|
+
retryTime ||= Date.now()
|
|
402
|
+
const retryState = { since: params.since, time: retryTime, count: retryCount }
|
|
403
|
+
await retry(err, retryCount++, retryState, { signal, logger }, () => {
|
|
404
|
+
const { statusCode, code, message, headers } = err
|
|
405
|
+
|
|
406
|
+
if (retryTime && Date.now() - retryTime > 2 * 60e3) {
|
|
407
|
+
throw err
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
if (statusCode && [420, 429, 502, 503, 504].includes(statusCode)) {
|
|
411
|
+
const retryAfter = headers?.['retry-after']
|
|
412
|
+
? Number(headers['retry-after']) * 1e3
|
|
413
|
+
: null
|
|
414
|
+
const delay =
|
|
415
|
+
retryAfter != null && Number.isFinite(retryAfter)
|
|
416
|
+
? retryAfter
|
|
417
|
+
: Math.min(10e3, retryCount * 1e3)
|
|
418
|
+
return tp.setTimeout(delay, true, { signal: opts?.signal ?? undefined })
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
if (
|
|
422
|
+
[
|
|
423
|
+
'ECONNRESET',
|
|
424
|
+
'ECONNREFUSED',
|
|
425
|
+
'ENOTFOUND',
|
|
426
|
+
'ENETDOWN',
|
|
427
|
+
'ENETUNREACH',
|
|
428
|
+
'EHOSTDOWN',
|
|
429
|
+
'EHOSTUNREACH',
|
|
430
|
+
'EPIPE',
|
|
431
|
+
'EAI_AGAIN',
|
|
432
|
+
'ENODATA',
|
|
433
|
+
'UND_ERR_CONNECT_TIMEOUT',
|
|
434
|
+
'UND_ERR_SOCKET',
|
|
435
|
+
].includes(code)
|
|
436
|
+
) {
|
|
437
|
+
return tp.setTimeout(Math.min(10e3, retryCount * 1e3), true, {
|
|
438
|
+
signal: opts?.signal ?? undefined,
|
|
439
|
+
})
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
if (['other side closed'].includes(message)) {
|
|
443
|
+
return tp.setTimeout(Math.min(10e3, retryCount * 1e3), true, {
|
|
444
|
+
signal: opts?.signal ?? undefined,
|
|
445
|
+
})
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
throw err
|
|
449
|
+
})
|
|
450
|
+
params.since = retryState.since ?? 0
|
|
405
451
|
}
|
|
406
452
|
} finally {
|
|
407
453
|
src?.on('error', () => {}).destroy()
|
package/numa.js
CHANGED
|
@@ -5,66 +5,100 @@ import { sched_setaffinity } from '@nxtedition/sched'
|
|
|
5
5
|
|
|
6
6
|
function parseRange(value) {
|
|
7
7
|
if (typeof value !== 'string') {
|
|
8
|
-
throw new Error('
|
|
8
|
+
throw new Error('range must be a string')
|
|
9
9
|
}
|
|
10
10
|
|
|
11
11
|
const range = []
|
|
12
12
|
for (const part of value.split(',')) {
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
const trimmed = part.trim()
|
|
14
|
+
if (!trimmed) {
|
|
15
|
+
continue
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if (trimmed.includes('-')) {
|
|
19
|
+
const [startStr, endStr, ...rest] = trimmed.split('-')
|
|
20
|
+
|
|
15
21
|
if (rest.length > 0) {
|
|
16
|
-
throw new Error(
|
|
22
|
+
throw new Error(`Invalid range: "${trimmed}"`)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const start = Number(startStr)
|
|
26
|
+
const end = Number(endStr)
|
|
27
|
+
|
|
28
|
+
if (!Number.isInteger(start) || !Number.isInteger(end) || start < 0 || end < 0) {
|
|
29
|
+
throw new Error(`Invalid number in range: "${trimmed}"`)
|
|
17
30
|
}
|
|
31
|
+
|
|
32
|
+
if (end < start) {
|
|
33
|
+
throw new Error(`Invalid range order: "${trimmed}" (end < start)`)
|
|
34
|
+
}
|
|
35
|
+
|
|
18
36
|
for (let i = start; i <= end; i++) {
|
|
19
37
|
range.push(i)
|
|
20
38
|
}
|
|
21
|
-
} else
|
|
22
|
-
|
|
39
|
+
} else {
|
|
40
|
+
const num = Number(trimmed)
|
|
41
|
+
if (!Number.isInteger(num) || num < 0) {
|
|
42
|
+
throw new Error(`Invalid number: "${trimmed}"`)
|
|
43
|
+
}
|
|
44
|
+
range.push(num)
|
|
23
45
|
}
|
|
24
46
|
}
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
}
|
|
28
|
-
return range
|
|
47
|
+
|
|
48
|
+
return [...new Set(range)].sort((a, b) => a - b)
|
|
29
49
|
}
|
|
30
50
|
|
|
31
51
|
/**
|
|
32
52
|
*
|
|
33
|
-
* @param {number|number[]} numa
|
|
53
|
+
* @param {null|number|number[]} numa
|
|
34
54
|
* @returns {number[]}
|
|
35
55
|
*/
|
|
36
56
|
export function setAffinity(numa) {
|
|
37
|
-
|
|
38
|
-
if (indices.some((x) => !Number.isInteger(x) || x < 0)) {
|
|
39
|
-
throw new Error('NUMA node must be a non-negative integer')
|
|
40
|
-
}
|
|
57
|
+
let affinity
|
|
41
58
|
|
|
42
|
-
|
|
59
|
+
if (numa === null) {
|
|
60
|
+
affinity = parseRange(fs.readFileSync('/sys/devices/system/cpu/present', 'utf8').trim())
|
|
61
|
+
} else {
|
|
62
|
+
if (numa.some((x) => !Number.isInteger(x) || x < 0)) {
|
|
63
|
+
throw new Error(
|
|
64
|
+
'NUMA node must be null, a non-negative integer or array of non-negative integers',
|
|
65
|
+
)
|
|
66
|
+
}
|
|
43
67
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
68
|
+
const allNodes = []
|
|
69
|
+
for (const entry of fs.readdirSync('/sys/devices/system/node')) {
|
|
70
|
+
if (!/^node\d+$/.test(entry)) {
|
|
71
|
+
continue
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const cpulist = fs
|
|
75
|
+
.readFileSync(path.join('/sys/devices/system/node', entry, 'cpulist'), 'utf8')
|
|
76
|
+
.trim()
|
|
77
|
+
|
|
78
|
+
allNodes.push(parseRange(cpulist))
|
|
48
79
|
}
|
|
49
80
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
81
|
+
if (allNodes.length === 0) {
|
|
82
|
+
throw new Error('No NUMA nodes found')
|
|
83
|
+
}
|
|
53
84
|
|
|
54
|
-
|
|
85
|
+
affinity = numa.flatMap((i) => allNodes[i % allNodes.length] ?? [])
|
|
55
86
|
}
|
|
56
87
|
|
|
57
|
-
|
|
58
|
-
|
|
88
|
+
const isolated = parseRange(fs.readFileSync('/sys/devices/system/cpu/isolated', 'utf8').trim())
|
|
89
|
+
|
|
90
|
+
affinity = affinity.filter((cpu) => !isolated.includes(cpu))
|
|
91
|
+
affinity = [...new Set(affinity)].sort((a, b) => a - b)
|
|
92
|
+
|
|
93
|
+
if (affinity.length === 0) {
|
|
94
|
+
throw new Error('Resulting CPU affinity is empty')
|
|
59
95
|
}
|
|
60
96
|
|
|
61
|
-
const affinity = indices
|
|
62
|
-
.flatMap((i) => allNodes[i % allNodes.length] ?? [])
|
|
63
|
-
.filter((cpu) => !isolated.includes(cpu))
|
|
64
97
|
sched_setaffinity(0, affinity)
|
|
98
|
+
|
|
65
99
|
globalThis.__nxt_sched_affinity = {
|
|
66
100
|
cpulist: affinity,
|
|
67
|
-
nodelist: indices,
|
|
68
101
|
}
|
|
102
|
+
|
|
69
103
|
return affinity
|
|
70
104
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nxtedition/lib",
|
|
3
|
-
"version": "27.0.
|
|
3
|
+
"version": "27.0.6",
|
|
4
4
|
"license": "UNLICENSED",
|
|
5
5
|
"author": "Robert Nagy <robert.nagy@boffins.se>",
|
|
6
6
|
"type": "module",
|
|
@@ -44,19 +44,20 @@
|
|
|
44
44
|
"under-pressure.js"
|
|
45
45
|
],
|
|
46
46
|
"scripts": {
|
|
47
|
-
"test": "node --test-timeout 60000 --test"
|
|
48
|
-
"test:types": "tsd"
|
|
47
|
+
"test": "node --test-timeout 60000 --test"
|
|
49
48
|
},
|
|
50
49
|
"dependencies": {
|
|
51
50
|
"@elastic/elasticsearch": "^8.17.1",
|
|
52
51
|
"@elastic/transport": "^8.9.3",
|
|
53
|
-
"@nxtedition/nxt-undici": "^
|
|
52
|
+
"@nxtedition/nxt-undici": "^7.1.4",
|
|
54
53
|
"@nxtedition/sched": "^1.0.2",
|
|
55
|
-
"@nxtedition/template": "^1.0.
|
|
56
|
-
"@nxtedition/weak-cache": "^1.0.
|
|
54
|
+
"@nxtedition/template": "^1.0.2",
|
|
55
|
+
"@nxtedition/weak-cache": "^1.0.1",
|
|
57
56
|
"diff": "5.2.0",
|
|
57
|
+
"eslint": "^9.38.0",
|
|
58
58
|
"fast-querystring": "^1.1.2",
|
|
59
59
|
"http-errors": "^2.0.0",
|
|
60
|
+
"lerna": "^9.0.0",
|
|
60
61
|
"lodash": "^4.17.21",
|
|
61
62
|
"lru-cache": "^11.2.2",
|
|
62
63
|
"mime": "^4.0.7",
|
|
@@ -66,18 +67,22 @@
|
|
|
66
67
|
"pino": "^10.1.0",
|
|
67
68
|
"qs": "^6.14.0",
|
|
68
69
|
"request-target": "^1.0.2",
|
|
70
|
+
"typescript-eslint": "^8.46.2",
|
|
69
71
|
"url-join": "^5.0.0",
|
|
70
72
|
"xuid": "^4.1.5",
|
|
71
|
-
"yocto-queue": "^1.2.
|
|
73
|
+
"yocto-queue": "^1.2.2"
|
|
72
74
|
},
|
|
73
75
|
"devDependencies": {
|
|
74
|
-
"@nxtedition/deepstream.io-client-js": ">=31.2.
|
|
76
|
+
"@nxtedition/deepstream.io-client-js": ">=31.2.9",
|
|
75
77
|
"@types/lodash": "^4.17.20",
|
|
76
|
-
"@types/node": "^24.
|
|
78
|
+
"@types/node": "^24.10.1",
|
|
77
79
|
"canvas": "^3.1.0",
|
|
80
|
+
"eslint": "^9.39.1",
|
|
81
|
+
"lerna": "^9.0.1",
|
|
78
82
|
"rxjs": "^7.8.2",
|
|
79
83
|
"tsd": "^0.33.0",
|
|
80
|
-
"typescript": "^5.9.3"
|
|
84
|
+
"typescript": "^5.9.3",
|
|
85
|
+
"typescript-eslint": "^8.46.4"
|
|
81
86
|
},
|
|
82
87
|
"peerDependencies": {
|
|
83
88
|
"@elastic/elasticsearch": "^8.6.0",
|
|
@@ -87,5 +92,5 @@
|
|
|
87
92
|
"pino": ">=7.0.0",
|
|
88
93
|
"rxjs": "^7.0.0"
|
|
89
94
|
},
|
|
90
|
-
"gitHead": "
|
|
95
|
+
"gitHead": "0a3d683b790eeeb5f198316a27e205f84bf93c9e"
|
|
91
96
|
}
|
package/time.js
CHANGED
|
@@ -10,19 +10,9 @@ export function fastNow() {
|
|
|
10
10
|
return fastNowTime
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
-
export function isTimeBetween(date, startTime, endTime
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
if (isUTC) {
|
|
18
|
-
currentHours = date.getUTCHours()
|
|
19
|
-
currentMinutes = date.getUTCMinutes()
|
|
20
|
-
} else {
|
|
21
|
-
// Convert local time to UTC equivalent
|
|
22
|
-
const utcDate = new Date(date.getTime() + date.getTimezoneOffset() * 60000)
|
|
23
|
-
currentHours = utcDate.getUTCHours()
|
|
24
|
-
currentMinutes = utcDate.getUTCMinutes()
|
|
25
|
-
}
|
|
13
|
+
export function isTimeBetween(date, startTime, endTime) {
|
|
14
|
+
const currentHours = date.getHours()
|
|
15
|
+
const currentMinutes = date.getMinutes()
|
|
26
16
|
|
|
27
17
|
// Validate and parse start and end times
|
|
28
18
|
if (!startTime) startTime = '00:00' // Default start at midnight
|