@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.
Files changed (5) hide show
  1. package/app.js +7 -16
  2. package/couch.js +69 -23
  3. package/numa.js +64 -30
  4. package/package.json +16 -11
  5. 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((numaNode) => {
595
- if (numaNode != null) {
588
+ .subscribe((numa) => {
589
+ if (numa != null) {
596
590
  try {
597
- const affinity = numa.setAffinity(numaNode)
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, numaNode }, 'net numa failed')
594
+ logger.error({ err, hostname: config.hostname, numa }, 'net numa failed')
604
595
  }
605
596
  } else {
606
- logger.warn({ hostname: config.hostname, numaNode }, 'net numa missing')
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
- params.since = data.last_seq
346
+ since = data.last_seq
341
347
  } else {
342
- params.since = data.seq || params.since
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
- params.since = change.seq || params.since
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
- await delay(err, retryCount, { signal, logger })
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('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.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": "^6.4.17",
52
+ "@nxtedition/nxt-undici": "^7.1.4",
54
53
  "@nxtedition/sched": "^1.0.2",
55
- "@nxtedition/template": "^1.0.0-alpha.0",
56
- "@nxtedition/weak-cache": "^1.0.0-alpha.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.1"
73
+ "yocto-queue": "^1.2.2"
72
74
  },
73
75
  "devDependencies": {
74
- "@nxtedition/deepstream.io-client-js": ">=31.2.1",
76
+ "@nxtedition/deepstream.io-client-js": ">=31.2.9",
75
77
  "@types/lodash": "^4.17.20",
76
- "@types/node": "^24.9.1",
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": "383b7fe1d10a8b7d80aa80ea81e7828f415d248d"
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, 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