runcloud 0.2.180 → 0.2.182

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.
@@ -6,6 +6,13 @@ export const num = (value) => typeof value === 'number' && Number.isFinite(value
6
6
  export const stamp = (value) => str(value) && Number.isFinite(Date.parse(String(value))) ? new Date(String(value)).toISOString() : null;
7
7
  export const time = (value) => stamp(value) ? Date.parse(String(value)) : 0;
8
8
  export const validFleetId = (value) => typeof value === 'string' && /^[a-zA-Z0-9][a-zA-Z0-9._:-]{0,255}$/.test(value);
9
+ export function fleetReadError(reason, timeoutMs) {
10
+ if (reason === 'HTTP 401' || reason === 'HTTP 403')
11
+ return new Error(`Fleet access denied (${reason}); use an admin identity with read permission`);
12
+ if (reason === 'request_deadline_exceeded')
13
+ return new Error(`Fleet read timed out after ${timeoutMs / 1000}s (request_deadline_exceeded); retry the read and check API latency if it persists`);
14
+ return new Error(`Fleet read unavailable (${reason ?? 'invalid_response'}); check API connectivity and retry the read`);
15
+ }
9
16
  export function safeFleetText(value) {
10
17
  const text = str(value);
11
18
  return text ? text.slice(0, 8000)
@@ -1,6 +1,6 @@
1
1
  import { ApiError } from '../api.js';
2
- import { componentHosts, hostReport, hostedObservation, num, observation, row, rows, safeFleetText, stamp, str, time, validFleetId } from './run-cloud-fleet-observations.js';
3
- export const SURVEY_REQUEST_BUDGET_MS = 8000;
2
+ import { componentHosts, fleetReadError, hostReport, hostedObservation, num, observation, row, rows, safeFleetText, stamp, str, time, validFleetId } from './run-cloud-fleet-observations.js';
3
+ export const SURVEY_REQUEST_BUDGET_MS = 30000;
4
4
  const ROOT = '/run-cloud/admin/simulators';
5
5
  const RANGES = ['1h', '6h', '24h', '7d'];
6
6
  const DEVICE_LIMIT = 512, POINT_LIMIT = 800, TYPE_LIMIT = 100;
@@ -122,19 +122,21 @@ export async function surveyFleet(client, options = {}, now, requestBudgetMs = S
122
122
  throw new Error('Invalid survey request budget');
123
123
  const started = performance.now(), signal = AbortSignal.timeout(requestBudgetMs);
124
124
  const read = async (path, key, body) => {
125
+ const sourceStarted = performance.now();
126
+ const timed = (source) => ({ ...source, durationSeconds: (performance.now() - sourceStarted) / 1000 });
125
127
  try {
126
128
  const value = body ? await client.post(path, body, { signal }) : await client.get(path, { signal });
127
129
  const data = row(value);
128
130
  if (value instanceof Response || !Array.isArray(data[key]))
129
- return unavailable('invalid_response_shape');
131
+ return timed(unavailable('invalid_response_shape'));
130
132
  if (path === '/run-cloud/admin/jenkins' && data.state === 'unavailable')
131
- return { state: 'unavailable', data, reason: 'jenkins_collection_unavailable' };
133
+ return timed({ state: 'unavailable', data, reason: 'jenkins_collection_unavailable' });
132
134
  if (path === '/run-cloud/admin/fleet-operations' && (data.unavailable === true || data.configured === false))
133
- return { state: 'unavailable', data, reason: 'operation_storage_unavailable_or_unconfigured' };
134
- return { state: 'available', data, reason: null };
135
+ return timed({ state: 'unavailable', data, reason: 'operation_storage_unavailable_or_unconfigured' });
136
+ return timed({ state: 'available', data, reason: null });
135
137
  }
136
138
  catch (error) {
137
- return unavailable(error instanceof ApiError ? `HTTP ${error.status}` : signal.aborted ? 'request_budget_exhausted' : 'read_failed');
139
+ return timed(unavailable(error instanceof ApiError ? `HTTP ${error.status}` : signal.aborted ? 'request_deadline_exceeded' : 'read_failed'));
138
140
  }
139
141
  };
140
142
  const fleetPromise = read(ROOT, 'hosts');
@@ -163,7 +165,7 @@ export async function surveyFleet(client, options = {}, now, requestBudgetMs = S
163
165
  fleetPromise, inventoryPromise, sharedPromise, jenkinsPromise, operationsPromise, analyticsPromise, lagPromise,
164
166
  ]);
165
167
  if (fleet.state !== 'available')
166
- throw new Error(`Fleet read unavailable (${fleet.reason}); use an admin identity with read permission`);
168
+ throw fleetReadError(fleet.reason, requestBudgetMs);
167
169
  now ??= Date.now();
168
170
  const live = new Map(rows(fleet.data.hosts).filter(host => validFleetId(host.id)).map(host => [String(host.id), host]));
169
171
  const canonical = new Map(rows(inventory.data.hosts).filter(host => validFleetId(host.id)).map(host => [String(host.id), host]));
@@ -187,7 +189,7 @@ export async function surveyFleet(client, options = {}, now, requestBudgetMs = S
187
189
  const alerts = alertReport(analytics, now, range, options.hostId);
188
190
  const sources = Object.fromEntries(Object.entries({ fleet, inventory, shared, jenkins, operations,
189
191
  analytics: { ...analytics, state: alerts.state, reason: alerts.reason }, inputLag: { ...lag, state: inputLag.state, reason: inputLag.reason } }).map(([key, source]) => [key, {
190
- state: source.state, reason: source.reason, generatedAt: stamp(source.data.generatedAt ?? source.data.observedAt),
192
+ state: source.state, reason: source.reason, durationSeconds: source.durationSeconds ?? null, generatedAt: stamp(source.data.generatedAt ?? source.data.observedAt),
191
193
  responseAgeSeconds: stamp(source.data.generatedAt ?? source.data.observedAt) ? (now - time(source.data.generatedAt ?? source.data.observedAt)) / 1000 : null,
192
194
  meaning: 'response/collector generation time; does not renew component observations',
193
195
  }]));
@@ -214,8 +216,9 @@ export async function surveyFleet(client, options = {}, now, requestBudgetMs = S
214
216
  function age(value) { return `${value.state}, age ${value.ageSeconds === null ? 'unknown' : `${Math.round(value.ageSeconds)}s`}${value.reason ? `, ${value.reason}` : ''}`; }
215
217
  function bytes(value) { return value === null ? 'unknown' : `${(value / 1024 ** 3).toFixed(1)} GiB`; }
216
218
  export function surveyText(report) {
217
- const lines = [`Fleet survey · ${report.scope} · ${report.generatedAt} · ${report.durationSeconds.toFixed(2)}s (request budget ${report.requestBudgetSeconds}s)`,
219
+ const lines = [`Fleet survey · ${report.scope} · ${report.generatedAt} · ${report.durationSeconds.toFixed(2)}s (read deadline ${report.requestBudgetSeconds}s)`,
218
220
  `${report.summary.hosts} machine records (${report.summary.canonicalHosts ?? 'unknown'} canonical); ${report.coverage.sourcesAvailable}/7 sources available.`,
221
+ `Source read times: ${Object.entries(report.sources).map(([name, source]) => `${name}=${source.durationSeconds === null ? 'not requested' : `${source.durationSeconds.toFixed(2)}s`}`).join(', ')}.`,
219
222
  `Diagnosis states: ${Object.entries(report.summary.diagnosisStates).map(([state, count]) => `${state}=${count}`).join(', ')}.`,
220
223
  `CPU mean: ${report.summary.cpu.meanBusyPercent?.toFixed(1) ?? 'unknown'}% from ${report.summary.cpu.freshHosts}/${report.summary.cpu.totalHosts} fresh host samples. ${report.summary.cpu.meaning}.`];
221
224
  if (report.alerts.state === 'available') {
@@ -1,4 +1,4 @@
1
- import { row, rows, str, num, stamp, time, pick, observation, componentHosts, hostReport, safeFleetText, validFleetId } from './run-cloud-fleet-observations.js';
1
+ import { row, rows, str, num, stamp, time, pick, observation, componentHosts, fleetReadError, hostReport, safeFleetText, validFleetId } from './run-cloud-fleet-observations.js';
2
2
  export { safeFleetText } from './run-cloud-fleet-observations.js';
3
3
  import { readFileSync, statSync } from 'node:fs';
4
4
  import { ApiClient, ApiError } from '../api.js';
@@ -25,14 +25,15 @@ function validate(options) {
25
25
  bound(options.eventPages, 1, 10, 'event-pages');
26
26
  }
27
27
  async function readSource(client, path) {
28
+ const signal = AbortSignal.timeout(30000);
28
29
  try {
29
- const data = await client.get(path, { signal: AbortSignal.timeout(30000) });
30
+ const data = await client.get(path, { signal });
30
31
  if (!data || typeof data !== 'object' || Array.isArray(data) || data instanceof Response)
31
32
  return { state: 'unavailable', data: {}, reason: 'invalid_json_response' };
32
33
  return { state: 'available', data: row(data), reason: null };
33
34
  }
34
35
  catch (error) {
35
- return { state: 'unavailable', data: {}, reason: error instanceof ApiError ? `HTTP ${error.status}` : 'read_failed' };
36
+ return { state: 'unavailable', data: {}, reason: error instanceof ApiError ? `HTTP ${error.status}` : signal.aborted ? 'request_deadline_exceeded' : 'read_failed' };
36
37
  }
37
38
  }
38
39
  export async function readFleetEvents(client, options = {}) {
@@ -169,7 +170,7 @@ export async function inspectFleet(client, options = {}, now) {
169
170
  query.set('include', 'details');
170
171
  const fleet = await readSource(client, ROOT);
171
172
  if (fleet.state === 'unavailable' || !Array.isArray(fleet.data.hosts))
172
- throw new Error(`Fleet read unavailable (${fleet.reason ?? 'invalid_response'}); use an admin identity with read permission`);
173
+ throw fleetReadError(fleet.reason, 30000);
173
174
  const [shared, diagnostics, events, history, jenkins] = await Promise.all([
174
175
  readSource(client, `${ROOT}/operator-inventory`),
175
176
  readSource(client, `${ROOT}/diagnostics?${query}`),
package/dist/version.js CHANGED
@@ -1 +1 @@
1
- export const CLI_VERSION = '0.2.180';
1
+ export const CLI_VERSION = '0.2.182';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "runcloud",
3
- "version": "0.2.180",
3
+ "version": "0.2.182",
4
4
  "description": "Create and control run.cloud remote mobile simulators and cloud sandboxes",
5
5
  "license": "Apache-2.0",
6
6
  "keywords": [
Binary file
Binary file