@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.
Files changed (4) hide show
  1. package/app.js +51 -55
  2. package/numa.js +64 -30
  3. package/package.json +4 -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('CPU range must be a string')
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
- if (part.includes('-')) {
14
- const [start, end, ...rest] = part.split('-').map(Number)
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('Invalid CPU range')
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 if (part) {
22
- range.push(Number(part))
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
- if (range.some((x) => !Number.isInteger(x) || x < 0)) {
26
- throw new Error('Invalid CPU range')
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
- const indices = Array.from(new Set([numa].flat()))
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
- const isolated = parseRange(fs.readFileSync('/sys/devices/system/cpu/isolated', 'utf8').trim())
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
- const allNodes = []
45
- for (const entry of fs.readdirSync('/sys/devices/system/node')) {
46
- if (!entry.startsWith('node')) {
47
- continue
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
- const cpulist = fs
51
- .readFileSync(path.join('/sys/devices/system/node', entry, 'cpulist'), 'utf8')
52
- .trim()
81
+ if (allNodes.length === 0) {
82
+ throw new Error('No NUMA nodes found')
83
+ }
53
84
 
54
- allNodes.push(parseRange(cpulist))
85
+ affinity = numa.flatMap((i) => allNodes[i % allNodes.length] ?? [])
55
86
  }
56
87
 
57
- if (allNodes.length === 0) {
58
- throw new Error('No NUMA nodes found')
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.2",
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-alpha.0",
56
- "@nxtedition/weak-cache": "^1.0.0-alpha.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": "ad258cea82e41d8d196fc344fd7436675abf70a0"
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, isUTC) {
14
- let currentHours = date.getHours()
15
- let currentMinutes = date.getMinutes()
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