@nxtedition/lib 27.0.3 → 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 +7 -13
  2. package/numa.js +64 -30
  3. package/package.json +4 -4
  4. 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,27 @@ 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((numaNode) => {
595
- if (numaNode != null) {
588
+ .subscribe((numa) => {
589
+ if (numa != null) {
596
590
  try {
597
- const affinity = numa.setAffinity(numaNode)
591
+ const affinity = numa.setAffinity(numa)
598
592
  logger.debug(
599
- { hostname: config.hostname, numaNode, affinity },
593
+ { hostname: config.hostname, numa, affinity },
600
594
  'net numa succeeded',
601
595
  )
602
596
  } catch (err) {
603
- logger.error({ err, hostname: config.hostname, numaNode }, 'net numa failed')
597
+ logger.error({ err, hostname: config.hostname, numa }, 'net numa failed')
604
598
  }
605
599
  } else {
606
- logger.warn({ hostname: config.hostname, numaNode }, 'net numa missing')
600
+ logger.warn({ hostname: config.hostname, numa }, 'net numa missing')
607
601
  }
608
602
  })
609
603
  }
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.3",
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": "383b7fe1d10a8b7d80aa80ea81e7828f415d248d"
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