@vzn/vx-history 0.0.649 → 0.0.651

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/README.md CHANGED
@@ -19,7 +19,7 @@ import { vxHistory } from '@vzn/vx-history'
19
19
  export default { plugins: [vxHistory()] }
20
20
  ```
21
21
 
22
- `vxHistory({ window?, assume?, file?, url?, issuer?, repository?, headers?, resources?, memory?, reservations? })` learns from the last `window` invocations (default 20) through core's `LocalHistoryProvider` over the cache's own SQLite handle. With one worker and two chains of identical shape, the chain history says is slow starts first; with no history yet, the scheduler's structural order stands.
22
+ `vxHistory({ window?, assume?, file?, url?, issuer?, repository?, headers?, resources?, memory?, reservations? })` learns from the last `window` invocations (default 20) through core's `localHistory(cache)`, which reads the cache's index and returns rows; the plugin never touches cache.db. With one worker and two chains of identical shape, the chain history says is slow starts first; with no history yet, the scheduler's structural order stands.
23
23
 
24
24
  `assume` is for the run that has no history: a fresh CI runner. It names durations (task id → ms) for tasks the history has not seen, so a long leaf task — a docs build, a cross-compile — starts first instead of last:
25
25
 
@@ -47,6 +47,8 @@ for its tasks' numbers, by task definition, in the refs the run reads,
47
47
  as a remote cache does: its branch's, the branch it merges into, then
48
48
  the default branch's (`baseBranch`, else git's or the CI's). A run
49
49
  that reads no ref (a detached HEAD with no default branch) asks nothing.
50
+ A ref over the 512 characters a server takes is said and not read;
51
+ the run reads the others.
50
52
  The run starts on its local history and re-ranks the tasks not yet
51
53
  started when they land; it never waits for them. At its end it sends the
52
54
  timings of the tasks it ran, once: under DTE that is the orchestrator,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vzn/vx-history",
3
- "version": "0.0.649",
3
+ "version": "0.0.651",
4
4
  "license": "MIT",
5
5
  "description": "History-based scheduling for vx — a `schedule` plugin that orders ready tasks by their expected remaining critical path, learned from the local run history.",
6
6
  "keywords": [
@@ -37,7 +37,7 @@
37
37
  "access": "public"
38
38
  },
39
39
  "peerDependencies": {
40
- "@vzn/vx": "^0.0.649"
40
+ "@vzn/vx": "^0.0.651"
41
41
  },
42
42
  "repository": {
43
43
  "type": "git",
@@ -181,7 +181,8 @@ export class HistoryServer {
181
181
  u.searchParams.set('audience', new URL(this.url).origin)
182
182
  const res = await this.askGithub(u, signal)
183
183
  if (!res.ok) throw new Error(`GitHub's OIDC endpoint: ${(await refusal(res)).message}`)
184
- const jwt = ((await res.json()) as { value?: unknown }).value
184
+ // quiet: a body that is not JSON holds no token, said below
185
+ const jwt = ((await res.json().catch(() => null)) as { value?: unknown } | null)?.value
185
186
  if (typeof jwt !== 'string' || jwt === '') throw new Error("GitHub's OIDC endpoint: no token")
186
187
  return { jwt, exp: expiryOf(jwt) }
187
188
  })()
package/src/index.ts CHANGED
@@ -14,7 +14,8 @@
14
14
  import {
15
15
  currentBranch,
16
16
  definePlugin,
17
- LocalHistoryProvider,
17
+ type LocalHistory,
18
+ localHistory,
18
19
  machineMemoryBytes,
19
20
  Cache,
20
21
  type HistoryTable,
@@ -108,6 +109,9 @@ export interface VxHistoryOptions {
108
109
  // hint needs less and reads a 2.5× smaller slice of the run history.
109
110
  const DEFAULT_WINDOW = 20
110
111
 
112
+ /** The longest ref a history server takes in a query (its PROTOCOL.md). */
113
+ const REF_MAX = 512
114
+
111
115
  // Each option is read in ONE place that a run's hooks and `vx history`
112
116
  // share: two copies of each let either drift unseen (item 805).
113
117
  const windowOf = (options: VxHistoryOptions): number =>
@@ -193,7 +197,7 @@ const readHistory = (
193
197
  branch: string | null,
194
198
  defs: ReadonlyMap<string, string>,
195
199
  ): Promise<ReadonlyMap<string, TaskResources>> =>
196
- new LocalHistoryProvider(cache.dbHandle(), windowOf(options), branch, defs).resourcesFor(ids)
200
+ localHistory(cache, { recent: windowOf(options), branch, defs }).resourcesFor(ids)
197
201
 
198
202
  /** What each task reserves: learned from the history unless `resources: false`, a declared reservation over either. */
199
203
  const reservationsFor = (
@@ -276,6 +280,11 @@ function namedOf(issuer: unknown, repository: unknown): Named {
276
280
  return { issuer: issuer.replace(/\/$/, ''), repository: repository.toLowerCase() }
277
281
  }
278
282
 
283
+ /** `url` as a refusal may print it: a `user:pass@` would reach the CI log. */
284
+ function shown(url: string): string {
285
+ return url.replace(/^([a-z][a-z0-9+.-]*:\/\/)[^/?#]*@/i, '$1***@')
286
+ }
287
+
279
288
  /**
280
289
  * https, or http to this machine: every request carries the CI token, and
281
290
  * http:// to another host sent it in clear.
@@ -298,12 +307,12 @@ function serverUrlOf(
298
307
  const from = option === undefined ? 'VX_HISTORY_URL' : 'url'
299
308
  if (!secure(url))
300
309
  throw new UserError(
301
- `vxHistory(): ${from} must be https (http only to 127.0.0.1, ::1 or localhost), got ${url}`,
310
+ `vxHistory(): ${from} must be https (http only to 127.0.0.1, ::1 or localhost), got ${shown(url)}`,
302
311
  )
303
312
  // Each route is the server's /v1/…: one that names /v1 asked /v1/v1/…, and every answer was a 404.
304
313
  if (url.endsWith('/v1'))
305
314
  throw new UserError(
306
- `vxHistory(): ${from} is the server's base, without /v1: ${url.slice(0, -3)}`,
315
+ `vxHistory(): ${from} is the server's base, without /v1: ${shown(url.slice(0, -3))}`,
307
316
  )
308
317
  return url
309
318
  }
@@ -411,14 +420,13 @@ export function vxHistory(options: VxHistoryOptions = {}): VxPlugin {
411
420
  const ids: string[] = []
412
421
  for (const n of nodes.values()) if (n.config.exec?.command !== undefined) ids.push(n.id)
413
422
  let table: ReadonlyMap<string, TaskResources>
414
- let provider: LocalHistoryProvider
423
+ let provider: LocalHistory
415
424
  try {
416
- provider = new LocalHistoryProvider(
417
- ctx.localCache.dbHandle(),
418
- windowOf(options),
419
- ctx.branch,
425
+ provider = localHistory(ctx.localCache, {
426
+ recent: windowOf(options),
427
+ branch: ctx.branch,
420
428
  defs,
421
- )
429
+ })
422
430
  table = await provider.resourcesFor(ids)
423
431
  } catch (err) {
424
432
  // Failing open: a broken history read costs the ordering and the
@@ -440,14 +448,22 @@ export function vxHistory(options: VxHistoryOptions = {}): VxPlugin {
440
448
  // branch it merges into, the default branch (`baseBranch`). `main`
441
449
  // was assumed, so a `master` or `baseBranch: 'trunk'` repository
442
450
  // read no trunk's numbers; a run with none asks nothing.
443
- const refs = ctx.refs
451
+ // A ref the server cannot name was a 400 for the whole query, and
452
+ // the run read no ref's numbers, its base branch's too.
453
+ const refs = [...new Set(ctx.refs)].filter((ref) => {
454
+ if (ref.length <= REF_MAX) return true
455
+ ctx.warn(
456
+ `[vx] vx-history: ${ref} is ${ref.length} characters, over the ${REF_MAX} a history server takes: the run does not read it`,
457
+ )
458
+ return false
459
+ })
444
460
  if (remote !== undefined && refs.length > 0) {
445
461
  const server = remote
446
462
  let late = false
447
463
  stopImport = () => (late = true)
448
464
  const weights = (async () => {
449
465
  const hashes = [...new Set(ids.flatMap((id) => defs.get(id) ?? []))]
450
- const got = await server.query([...new Set(refs)], hashes)
466
+ const got = await server.query(refs, hashes)
451
467
  const timings = new Map<string, number>()
452
468
  const needs = new Map<string, TaskResources>()
453
469
  for (const id of ids) {
@@ -800,7 +816,7 @@ async function historyCmd(
800
816
  // A plugin verb owns its help; completions offer `--help` for every verb.
801
817
  if (a === '--help' || a === '-h') {
802
818
  ui.say('info', 'Usage: vx history [--format pretty|json]', 'out')
803
- process.stdout.write('\n')
819
+ ui.print('out', ui.gap())
804
820
  ui.say(
805
821
  'info',
806
822
  'What vx-history learned per task (p50, peak RSS, CPU parallelism) and the reservation it packs.',
@@ -849,13 +865,12 @@ async function historyCmd(
849
865
  // Each task's rows of its definition as it is now, as `schedule` reads
850
866
  // them: read with every definition's, a task whose command changed
851
867
  // showed the old command's peak and a reservation no run packs.
852
- table = await new LocalHistoryProvider(
853
- cache.dbHandle(),
854
- window,
855
- currentBranch(ctx.workspaceRoot),
868
+ table = await localHistory(cache, {
869
+ recent: window,
870
+ branch: currentBranch(ctx.workspaceRoot),
856
871
  // As a run reads them: a task whose command changed has no rows yet.
857
- taskDefinitions(tasks as TaskNode[]),
858
- ).loadFor(ids)
872
+ defs: taskDefinitions(tasks as TaskNode[]),
873
+ }).loadFor(ids)
859
874
  } finally {
860
875
  cache.close()
861
876
  }
@@ -877,7 +892,7 @@ async function historyCmd(
877
892
  }
878
893
  })
879
894
  if (format === 'json') {
880
- process.stdout.write(`${JSON.stringify({ window, budgets, tasks: rows })}\n`)
895
+ ui.data('out', `${JSON.stringify({ window, budgets, tasks: rows })}\n`)
881
896
  return 0
882
897
  }
883
898
  // The table's module loads with the verb, off every run's path.