@nxtedition/lib 27.0.2 → 27.0.4
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 +51 -55
- package/numa.js +64 -30
- package/package.json +4 -4
- package/time.js +3 -13
package/app.js
CHANGED
|
@@ -363,60 +363,6 @@ export function makeApp(appConfig, onTerminateOrMeta, metaOrNull) {
|
|
|
363
363
|
}
|
|
364
364
|
}
|
|
365
365
|
|
|
366
|
-
let affinity = null
|
|
367
|
-
|
|
368
|
-
if (process.platform === 'linux' && appConfig.numa != null && appConfig.numa !== '') {
|
|
369
|
-
let numa = appConfig.numa
|
|
370
|
-
if (numa == 'auto' || numa === true) {
|
|
371
|
-
numa = hashString(JSON.stringify({ serviceName, serviceModule }))
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
if (numa === 'net') {
|
|
375
|
-
if (config.hostname) {
|
|
376
|
-
ds.record
|
|
377
|
-
.observe(`${config.hostname}:monitor.stats?`, ds.record.PROVIDER)
|
|
378
|
-
.pipe(
|
|
379
|
-
rxjs.map((stats) => stats?.net?.bonding?.[0]?.numaNode ?? null),
|
|
380
|
-
rxjs.filter((n) => Number.isInteger(n) && n >= 0),
|
|
381
|
-
rxjs.timeout({
|
|
382
|
-
first: 10e3,
|
|
383
|
-
with: () => rxjs.of(null),
|
|
384
|
-
}),
|
|
385
|
-
rxjs.distinctUntilChanged(),
|
|
386
|
-
rxjs.retry({
|
|
387
|
-
resetOnSuccess: true,
|
|
388
|
-
delay(err, retryCount) {
|
|
389
|
-
logger.error({ err, retryCount }, 'net numa failed')
|
|
390
|
-
return rxjs.timer(10e3)
|
|
391
|
-
},
|
|
392
|
-
}),
|
|
393
|
-
)
|
|
394
|
-
.subscribe((numaNode) => {
|
|
395
|
-
if (numaNode != null) {
|
|
396
|
-
try {
|
|
397
|
-
affinity = numa.setAffinity(numaNode)
|
|
398
|
-
logger.debug(
|
|
399
|
-
{ hostname: config.hostname, numaNode, affinity },
|
|
400
|
-
'net numa succeeded',
|
|
401
|
-
)
|
|
402
|
-
} catch (err) {
|
|
403
|
-
logger.error({ err, hostname: config.hostname, numaNode }, 'net numa failed')
|
|
404
|
-
}
|
|
405
|
-
} else {
|
|
406
|
-
logger.warn({ hostname: config.hostname, numaNode }, 'net numa missing')
|
|
407
|
-
}
|
|
408
|
-
})
|
|
409
|
-
}
|
|
410
|
-
} else {
|
|
411
|
-
try {
|
|
412
|
-
affinity = setAffinity(numa)
|
|
413
|
-
logger.debug({ data: { numa: appConfig.numa, affinity } }, 'set numa affinity succeeded')
|
|
414
|
-
} catch (err) {
|
|
415
|
-
logger.error({ err, data: { numa: appConfig.numa } }, 'set numa affinity failed')
|
|
416
|
-
}
|
|
417
|
-
}
|
|
418
|
-
}
|
|
419
|
-
|
|
420
366
|
if (appConfig.toobusy) {
|
|
421
367
|
const resolution = appConfig.toobusy.resolution ?? 10
|
|
422
368
|
const interval = appConfig.toobusy.interval ?? 500
|
|
@@ -614,6 +560,57 @@ export function makeApp(appConfig, onTerminateOrMeta, metaOrNull) {
|
|
|
614
560
|
appDestroyers.unshift(() => ds.close())
|
|
615
561
|
}
|
|
616
562
|
|
|
563
|
+
if (process.platform === 'linux' && appConfig.numa != null && appConfig.numa !== '') {
|
|
564
|
+
let numa = appConfig.numa
|
|
565
|
+
if (numa == 'auto' || numa === true || numa === 'hash') {
|
|
566
|
+
numa = hashString(
|
|
567
|
+
JSON.stringify({ serviceName, serviceModule, serviceInstanceId, serviceWorkerId }),
|
|
568
|
+
)
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
if (numa === 'net') {
|
|
572
|
+
if (config.hostname) {
|
|
573
|
+
if (!ds) {
|
|
574
|
+
throw new Error('deepstream is required for net numa')
|
|
575
|
+
}
|
|
576
|
+
ds.record
|
|
577
|
+
.observe(`${config.hostname}:monitor.stats?`, ds.record.PROVIDER)
|
|
578
|
+
.pipe(
|
|
579
|
+
rxjs.map((stats) => stats?.net?.bonding?.[0]?.numaNode ?? null),
|
|
580
|
+
rxjs.distinctUntilChanged(),
|
|
581
|
+
rxjs.retry({
|
|
582
|
+
delay(err, retryCount) {
|
|
583
|
+
logger.error({ err, retryCount }, 'net numa failed')
|
|
584
|
+
return rxjs.timer(10e3)
|
|
585
|
+
},
|
|
586
|
+
}),
|
|
587
|
+
)
|
|
588
|
+
.subscribe((numa) => {
|
|
589
|
+
if (numa != null) {
|
|
590
|
+
try {
|
|
591
|
+
const affinity = numa.setAffinity(numa)
|
|
592
|
+
logger.debug(
|
|
593
|
+
{ hostname: config.hostname, numa, affinity },
|
|
594
|
+
'net numa succeeded',
|
|
595
|
+
)
|
|
596
|
+
} catch (err) {
|
|
597
|
+
logger.error({ err, hostname: config.hostname, numa }, 'net numa failed')
|
|
598
|
+
}
|
|
599
|
+
} else {
|
|
600
|
+
logger.warn({ hostname: config.hostname, numa }, 'net numa missing')
|
|
601
|
+
}
|
|
602
|
+
})
|
|
603
|
+
}
|
|
604
|
+
} else {
|
|
605
|
+
try {
|
|
606
|
+
const affinity = setAffinity(numa)
|
|
607
|
+
logger.debug({ data: { numa: appConfig.numa, affinity } }, 'set numa affinity succeeded')
|
|
608
|
+
} catch (err) {
|
|
609
|
+
logger.error({ err, data: { numa: appConfig.numa } }, 'set numa affinity failed')
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
|
|
617
614
|
if (appConfig.compiler) {
|
|
618
615
|
compiler = makeTemplateCompiler({ ds, logger, ...appConfig.compiler })
|
|
619
616
|
}
|
|
@@ -1327,6 +1324,5 @@ export function makeApp(appConfig, onTerminateOrMeta, metaOrNull) {
|
|
|
1327
1324
|
serviceInstanceId,
|
|
1328
1325
|
serviceWorkerId,
|
|
1329
1326
|
signal: ac.signal,
|
|
1330
|
-
affinity,
|
|
1331
1327
|
})
|
|
1332
1328
|
}
|
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.4",
|
|
4
4
|
"license": "UNLICENSED",
|
|
5
5
|
"author": "Robert Nagy <robert.nagy@boffins.se>",
|
|
6
6
|
"type": "module",
|
|
@@ -52,8 +52,8 @@
|
|
|
52
52
|
"@elastic/transport": "^8.9.3",
|
|
53
53
|
"@nxtedition/nxt-undici": "^6.4.17",
|
|
54
54
|
"@nxtedition/sched": "^1.0.2",
|
|
55
|
-
"@nxtedition/template": "^1.0.0
|
|
56
|
-
"@nxtedition/weak-cache": "^1.0.0
|
|
55
|
+
"@nxtedition/template": "^1.0.0",
|
|
56
|
+
"@nxtedition/weak-cache": "^1.0.0",
|
|
57
57
|
"diff": "5.2.0",
|
|
58
58
|
"fast-querystring": "^1.1.2",
|
|
59
59
|
"http-errors": "^2.0.0",
|
|
@@ -87,5 +87,5 @@
|
|
|
87
87
|
"pino": ">=7.0.0",
|
|
88
88
|
"rxjs": "^7.0.0"
|
|
89
89
|
},
|
|
90
|
-
"gitHead": "
|
|
90
|
+
"gitHead": "be0475e7b0d724ba6f2f9fdbaa0c3f069e49e6a7"
|
|
91
91
|
}
|
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
|