@serve.zone/interfaces 32.45.0 → 32.47.0
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/changelog.md +44 -0
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/data/clusteringress.d.ts +37 -0
- package/dist_ts/data/clusteringress.js +43 -1
- package/dist_ts/data/runtimeassignmentlogs.d.ts +136 -0
- package/dist_ts/data/runtimeassignmentlogs.js +168 -3
- package/dist_ts/data/serviceruntime.d.ts +10 -1
- package/dist_ts/data/serviceruntime.js +2 -1
- package/dist_ts/private/networkcontract.d.ts +8 -2
- package/dist_ts/private/networkcontract.js +12 -6
- package/dist_ts/requests/service.d.ts +37 -0
- package/package.json +1 -1
- package/readme.md +85 -2
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/data/clusteringress.ts +70 -0
- package/ts/data/runtimeassignmentlogs.ts +307 -2
- package/ts/data/serviceruntime.ts +10 -0
- package/ts/private/networkcontract.ts +14 -5
- package/ts/requests/service.ts +50 -0
package/readme.md
CHANGED
|
@@ -1553,6 +1553,62 @@ session, that the assignment is this node's and carries the batch's attempt, and
|
|
|
1553
1553
|
joined length against the policy. `bindReportRuntimeAssignmentLogsResponse(response, request)`
|
|
1554
1554
|
binds the acknowledgement to the batch.
|
|
1555
1555
|
|
|
1556
|
+
**Reading kept logs.** An operator reads what Cloudly keeps through two service requests, which
|
|
1557
|
+
take `{ identity, serviceId }` and are authorized like every other read of that service. Cloudly
|
|
1558
|
+
keeps an attempt's logs from the moment it issues the attempt, so an attempt that failed before
|
|
1559
|
+
writing a line is listed with nothing held, and an attempt whose slot was replaced or released stays
|
|
1560
|
+
readable until its logs expire.
|
|
1561
|
+
|
|
1562
|
+
- `requests.service.getServiceRuntimeLogAttempts { identity, serviceId, cursor, limit }` answers an
|
|
1563
|
+
`IRuntimeAssignmentLogAttemptPage { attempts, nextCursor }`: the service's attempts, newest issued
|
|
1564
|
+
first and ties in `assignmentId` order. An `IRuntimeAssignmentLogAttemptView` states `serviceId`,
|
|
1565
|
+
`replicaId`, `nodeId`, `assignmentId`, `attemptId`, `issuedAt`, `lastAcceptedAt` (null until a
|
|
1566
|
+
batch was accepted), `expiresAt` (when the attempt's logs are dropped whole: the latest of
|
|
1567
|
+
`issuedAt`, `lastAcceptedAt` and the time the attempt's slot released it, plus the config's
|
|
1568
|
+
`logs.retentionMs`; null while the slot still holds the attempt, which schedules no expiry however
|
|
1569
|
+
quiet it is), `captures` and `capturesEnded` (captures with a batch, and
|
|
1570
|
+
those whose final batch arrived), `keptBytes` (UTF-8 bytes of `text` held) and `truncatedBatches`.
|
|
1571
|
+
`limit` runs from 1 to `runtimeAssignmentLogReadContract.maximumAttemptPageSize` (256).
|
|
1572
|
+
- `requests.service.getRuntimeAssignmentLogs { identity, serviceId, assignmentId, cursor, limit }`
|
|
1573
|
+
answers an `IRuntimeAssignmentLogPage { attempt, records, nextCursor }`: one attempt's records in
|
|
1574
|
+
delivery order, captures in the order the first batch of each was accepted and each capture by
|
|
1575
|
+
sequence. `limit` counts records, from 1 to `maximumPageRecords` (256). A page also ends before
|
|
1576
|
+
its records would hold more than `maximumPageEntries` (4 096, four full batches) entries or its
|
|
1577
|
+
canonical JSON would cross `maximumPageCanonicalBytes` (2 MiB), so it may hold fewer than `limit`
|
|
1578
|
+
while `nextCursor` names more. One batch always fits: its record is smaller than the report that
|
|
1579
|
+
carried it.
|
|
1580
|
+
|
|
1581
|
+
A `TRuntimeAssignmentLogRecord` is one of three, so nothing is ever dropped silently:
|
|
1582
|
+
|
|
1583
|
+
- `batch { captureId, sequence, acceptedAt, final, entries }`: a batch as the node sent it, its
|
|
1584
|
+
lines and loss entries unchanged;
|
|
1585
|
+
- `gap { captureId, fromSequence, toSequence }`: sequence numbers whose batches Cloudly did not
|
|
1586
|
+
receive or no longer holds, its own loss: numbers that never arrived and that the node did not
|
|
1587
|
+
account for, and accepted batches whose payload Cloudly has lost. Numbers the node skipped are not
|
|
1588
|
+
a gap: the batch after them opens with the `not-kept` loss that counts them;
|
|
1589
|
+
- `truncated { captureId, throughSequence, reasons, batches, bytes, entries }`: every batch Cloudly
|
|
1590
|
+
held of the capture up to `throughSequence` and has since dropped, oldest first, with exact counts.
|
|
1591
|
+
`reasons` (`TRuntimeAssignmentLogTruncationReason`, duplicate-free, in the canonical order
|
|
1592
|
+
`batch-cap`, `byte-cap`, `retention`) are `batch-cap`, Cloudly's bound on how many batches it
|
|
1593
|
+
keeps of one attempt, `byte-cap`, its bound on the text it keeps of one attempt, and `retention`,
|
|
1594
|
+
the config's `logs.retentionMs`. A node delivers about once a second, so a workload that writes
|
|
1595
|
+
now and then sends many small batches, which the batch bound holds where the text bound would
|
|
1596
|
+
not. Gaps inside the range are no longer stated.
|
|
1597
|
+
|
|
1598
|
+
`snapshotRuntimeAssignmentLogPage` enforces that order: a capture's records are contiguous and never
|
|
1599
|
+
return, a truncation only opens its capture, a gap starts at the number after the record before it,
|
|
1600
|
+
a gap never directly follows another gap (the controller states adjacent losses, received or not,
|
|
1601
|
+
as one gap, across pages too), a batch continues at the next number or, past numbers the node
|
|
1602
|
+
skipped, opens with a `not-kept` loss, and nothing follows a final batch. A page may resume inside a
|
|
1603
|
+
capture, so a capture's first record on a page is not judged against what came before it. A `nextCursor` is
|
|
1604
|
+
an opaque identifier, null on the last page, and never follows an empty page.
|
|
1605
|
+
`snapshotRuntimeAssignmentLogAttemptView` and `snapshotRuntimeAssignmentLogAttemptPage` read the
|
|
1606
|
+
attempt side. `bindRuntimeAssignmentLogAttemptPage(page, serviceId, limit)` and
|
|
1607
|
+
`bindRuntimeAssignmentLogPage(page, serviceId, assignmentId, limit)` match an answer to its read. A
|
|
1608
|
+
refused read carries `IRuntimeAssignmentLogReadErrorData { code, retryable: false }`, where
|
|
1609
|
+
`TRuntimeAssignmentLogReadRefusal` is `attempt-not-found` (no logs of that attempt for that
|
|
1610
|
+
service), `cursor-invalid` or `limit-invalid`.
|
|
1611
|
+
|
|
1556
1612
|
**Stats.** `IReq_Controller_Pallet_ReadRuntimeAssignmentStats` (`readRuntimeAssignmentStats`)
|
|
1557
1613
|
reads one sample of one attempt: `{ session, assignment }` names the exact current revision, and
|
|
1558
1614
|
the answer is `{ status: 'sampled', assignment, sample }` or `{ status: 'not-running', assignment,
|
|
@@ -1906,7 +1962,8 @@ evidence has not arrived). `refused` carries a `TServiceRuntimeRefusal`:
|
|
|
1906
1962
|
`volume-options-unsupported`, `volume-shared-unsupported`, `volume-placement-unsupported`,
|
|
1907
1963
|
`volume-initialization-unknown`, `volume-ownership-unknown`, `objectstorage-endpoint-missing`,
|
|
1908
1964
|
`objectstorage-binding-invalid`, `objectstorage-environment-conflict`,
|
|
1909
|
-
`objectstorage-endpoint-unselected`, `database-endpoint-missing
|
|
1965
|
+
`objectstorage-endpoint-unselected`, `database-endpoint-missing`, `database-endpoint-unselected`,
|
|
1966
|
+
`cluster-relay-missing` or `cluster-ingress-environment-conflict`.
|
|
1910
1967
|
The first four volume
|
|
1911
1968
|
refusals name what a local storage claim cannot express: a volume driver other than a node-local
|
|
1912
1969
|
directory, driver or mount options, one volume mounted by more than one replica or service, and a
|
|
@@ -1916,7 +1973,12 @@ refusals, `objectstorage-endpoint-missing`, `objectstorage-binding-invalid` and
|
|
|
1916
1973
|
`objectstorage-environment-conflict` name why a binding's environment cannot be composed, see
|
|
1917
1974
|
[Object-Storage Binding Environment](#object-storage-binding-environment), and
|
|
1918
1975
|
`objectstorage-endpoint-unselected`, like `database-endpoint-unselected`, names an endpoint the
|
|
1919
|
-
workload's network does not select (below).
|
|
1976
|
+
workload's network does not select (below). The two `cluster-` refusals concern only a cluster's
|
|
1977
|
+
designated ingress service, whose environment `composeClusterIngressEnvironment` composes, see
|
|
1978
|
+
[Cluster Ingress](#cluster-ingress): `cluster-relay-missing` is a cluster that states no valid
|
|
1979
|
+
relay, which an administrator enables with `enableClusterRelay`, and
|
|
1980
|
+
`cluster-ingress-environment-conflict` an ingress service whose own environment already sets one of
|
|
1981
|
+
`clusterIngressEnvironmentNames`. `ports-too-many` is a service declaring more target ports than the
|
|
1920
1982
|
`runtimeConfigContract.maximumTargetPorts` (64) a config carries, where `port-protocol-unsupported`
|
|
1921
1983
|
is about one port's protocol; `node-authority-unsupported` is a node whose session resolved fewer
|
|
1922
1984
|
workload authorities than the spec needs, refused at the admit step rather than handed to a node
|
|
@@ -2003,6 +2065,9 @@ through every slot on a cluster's nodes; `limit` runs from 1 to
|
|
|
2003
2065
|
`serviceRuntimeContract.maximumAssignmentPageSize` (256), and Cloudly refuses any other.
|
|
2004
2066
|
`requests.service.pushServiceRuntimeChanged { serviceId }` tells admin UIs that a service's spec,
|
|
2005
2067
|
status or assignments changed; it carries no state, so a late push never overwrites a newer read.
|
|
2068
|
+
A slot's replaced and released attempts leave these views; their kept workload logs stay readable
|
|
2069
|
+
through `getServiceRuntimeLogAttempts` and `getRuntimeAssignmentLogs` (see
|
|
2070
|
+
[Runtime Workload Logs, Stats, Exec And Volume Archives](#runtime-workload-logs-stats-exec-and-volume-archives)).
|
|
2006
2071
|
|
|
2007
2072
|
### Node Retirement
|
|
2008
2073
|
|
|
@@ -3245,6 +3310,24 @@ already applied and ignores a hint that is not newer, so a lost or repeated hint
|
|
|
3245
3310
|
`SERVEZONE_CLUSTER_RELAY_ORIGIN` (the exact `IClusterRelay.origin`) and `SERVEZONE_CLUSTER_NODE_ID`
|
|
3246
3311
|
(the node the assignment runs on). Cloudly writes them; the ingress reads them.
|
|
3247
3312
|
|
|
3313
|
+
`composeClusterIngressEnvironment(serviceEnvironment, { relayOrigin, nodeId })` is how Cloudly
|
|
3314
|
+
writes the two non-secret names for one replica of the designated ingress service: the service's
|
|
3315
|
+
own environment with `SERVEZONE_CLUSTER_RELAY_ORIGIN` and `SERVEZONE_CLUSTER_NODE_ID` added
|
|
3316
|
+
(`IClusterIngressLaunchFacts`; `relayOrigin` is the cluster's `IClusterRelay.origin` or `null`,
|
|
3317
|
+
`nodeId` the node the replica's assignment runs on). The bearer is not added: it reaches the
|
|
3318
|
+
workload through its secret manifest. The answer is a `TClusterIngressEnvironmentComposition`:
|
|
3319
|
+
|
|
3320
|
+
| `status` | `reason` | When |
|
|
3321
|
+
| --- | --- | --- |
|
|
3322
|
+
| `composed` | — | `environment` is the service's own with both names added. |
|
|
3323
|
+
| `refused` | `cluster-relay-missing` | `relayOrigin` is `null` or `validateClusterRelay` refuses it. Answered first. |
|
|
3324
|
+
| `refused` | `cluster-ingress-environment-conflict` | The service's own environment already sets one of the three names; the message names every such key and no value. |
|
|
3325
|
+
|
|
3326
|
+
`refused` carries the contract's `message`, and both reasons are `TClusterIngressEnvironmentRefusal`,
|
|
3327
|
+
members of `TServiceRuntimeRefusal`. A `nodeId` that is not a node id is the caller's error and is
|
|
3328
|
+
thrown. Both values are part of what the replica runs, so a moved relay or another node is another
|
|
3329
|
+
environment. The inputs are not changed.
|
|
3330
|
+
|
|
3248
3331
|
A cluster born on Pallet has no cutover scope, census or target set, so its ingress is designated by
|
|
3249
3332
|
`IClusterIngressDesignation { id, generation, previous, clusterId, runtime, ingressServiceId,
|
|
3250
3333
|
preparedSpecRevision, credential, actorId, acceptedAt, digest }` instead of the fleet-cutover
|
package/ts/00_commitinfo_data.ts
CHANGED
|
@@ -3,6 +3,8 @@ import { isUtcCalendarDay } from '../private/utccalendarday.js';
|
|
|
3
3
|
import { validateProtocolOfferOn } from '../protocol.js';
|
|
4
4
|
import { hasExactKeys, readExactDataRecord } from '../private/dataproperties.js';
|
|
5
5
|
import { isClusterNodeId } from '../private/nodeidentifier.js';
|
|
6
|
+
import { validateClusterRelay } from './cluster.js';
|
|
7
|
+
import type { TServiceRuntimeRefusal } from './serviceruntime.js';
|
|
6
8
|
|
|
7
9
|
/**
|
|
8
10
|
* The cluster ingress contracts.
|
|
@@ -440,6 +442,74 @@ export const clusterIngressEnvironmentNames = Object.freeze({
|
|
|
440
442
|
nodeId: 'SERVEZONE_CLUSTER_NODE_ID',
|
|
441
443
|
} as const);
|
|
442
444
|
|
|
445
|
+
/**
|
|
446
|
+
* The service runtime refusals the ingress environment reports: `cluster-relay-missing`, a cluster
|
|
447
|
+
* that states no valid relay, and `cluster-ingress-environment-conflict`, a service environment
|
|
448
|
+
* that already sets one of `clusterIngressEnvironmentNames`.
|
|
449
|
+
*/
|
|
450
|
+
export type TClusterIngressEnvironmentRefusal = Extract<
|
|
451
|
+
TServiceRuntimeRefusal,
|
|
452
|
+
'cluster-relay-missing' | 'cluster-ingress-environment-conflict'
|
|
453
|
+
>;
|
|
454
|
+
|
|
455
|
+
/** What the designated ingress service runs with, or the one reason it cannot be composed. */
|
|
456
|
+
export type TClusterIngressEnvironmentComposition =
|
|
457
|
+
| { status: 'composed'; environment: Record<string, string> }
|
|
458
|
+
| { status: 'refused'; reason: TClusterIngressEnvironmentRefusal; message: string };
|
|
459
|
+
|
|
460
|
+
/** The facts Cloudly states about one replica of the designated ingress service. */
|
|
461
|
+
export interface IClusterIngressLaunchFacts {
|
|
462
|
+
/** The cluster's `IClusterRelay.origin`, or `null` when the cluster states no relay. */
|
|
463
|
+
relayOrigin: string | null;
|
|
464
|
+
/** The node the replica's assignment runs on. */
|
|
465
|
+
nodeId: string;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
/**
|
|
469
|
+
* The environment one replica of a cluster's designated ingress service runs with: the service's
|
|
470
|
+
* own values with `relayOrigin` and `nodeId` added under their `clusterIngressEnvironmentNames`.
|
|
471
|
+
*
|
|
472
|
+
* The bearer is not added here: it is a secret and reaches the workload through its secret manifest.
|
|
473
|
+
* Its name is still Cloudly's, so a service environment that sets any of the three names is
|
|
474
|
+
* refused `cluster-ingress-environment-conflict`, naming every such key and none of their values,
|
|
475
|
+
* rather than having one value silently replace another. A relay origin that is `null` or that
|
|
476
|
+
* `validateClusterRelay` refuses is `cluster-relay-missing`, answered before a conflict. A node id
|
|
477
|
+
* that is not a node id is the caller's error, not a refusal: the assignment named it, and it is
|
|
478
|
+
* thrown. The inputs are not changed. Both values are part of what the replica runs, so a relay or
|
|
479
|
+
* node that changes is a new environment.
|
|
480
|
+
*/
|
|
481
|
+
export const composeClusterIngressEnvironment = (
|
|
482
|
+
serviceEnvironmentArg: Readonly<Record<string, string>>,
|
|
483
|
+
launchArg: IClusterIngressLaunchFacts,
|
|
484
|
+
): TClusterIngressEnvironmentComposition => {
|
|
485
|
+
const { relayOrigin, nodeId } = launchArg;
|
|
486
|
+
if (!isClusterNodeId(nodeId)) {
|
|
487
|
+
throw new Error('cluster ingress environment requires a node id');
|
|
488
|
+
}
|
|
489
|
+
if (relayOrigin === null) {
|
|
490
|
+
return { status: 'refused', reason: 'cluster-relay-missing',
|
|
491
|
+
message: 'cluster ingress has no relay origin: the cluster states no relay' };
|
|
492
|
+
}
|
|
493
|
+
const relayErrors = validateClusterRelay({ origin: relayOrigin });
|
|
494
|
+
if (relayErrors.length > 0) {
|
|
495
|
+
return { status: 'refused', reason: 'cluster-relay-missing',
|
|
496
|
+
message: `cluster ingress has no relay origin: ${relayErrors.join('; ')}` };
|
|
497
|
+
}
|
|
498
|
+
const collisions = Object.values(clusterIngressEnvironmentNames)
|
|
499
|
+
.filter((nameArg) => Object.hasOwn(serviceEnvironmentArg, nameArg))
|
|
500
|
+
.sort();
|
|
501
|
+
if (collisions.length > 0) {
|
|
502
|
+
return { status: 'refused', reason: 'cluster-ingress-environment-conflict',
|
|
503
|
+
message: `cluster ingress environment collides with ${collisions.join(', ')} already set on the `
|
|
504
|
+
+ 'service' };
|
|
505
|
+
}
|
|
506
|
+
return { status: 'composed', environment: {
|
|
507
|
+
...serviceEnvironmentArg,
|
|
508
|
+
[clusterIngressEnvironmentNames.relayOrigin]: relayOrigin,
|
|
509
|
+
[clusterIngressEnvironmentNames.nodeId]: nodeId,
|
|
510
|
+
} };
|
|
511
|
+
};
|
|
512
|
+
|
|
443
513
|
/**
|
|
444
514
|
* Every reason Cloudly refuses a cluster ingress registration, in the order it judges them.
|
|
445
515
|
*
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import type { IRuntimeAssignment } from './runtimeassignment.js';
|
|
2
|
-
import { snapshotRuntimeAssignment, validateRuntimeAssignment } from './runtimeassignment.js';
|
|
2
|
+
import { isRuntimeReplicaId, snapshotRuntimeAssignment, validateRuntimeAssignment } from './runtimeassignment.js';
|
|
3
3
|
import type { IRuntimeConfigLogPolicy } from './runtimeconfig.js';
|
|
4
4
|
import type { IRuntimeSessionBinding } from './runtimesession.js';
|
|
5
5
|
import { snapshotRuntimeSessionBinding } from './runtimesession.js';
|
|
6
6
|
import {
|
|
7
|
-
captureNetwork, networkCounter, networkEqual, networkId, networkList, networkRecord,
|
|
7
|
+
captureNetwork, networkCounter, networkEqual, networkId, networkList, networkNodeId, networkRecord, networkSorted,
|
|
8
|
+
requireNetwork,
|
|
8
9
|
} from '../private/networkcontract.js';
|
|
10
|
+
import { isOneOf, variantOf } from '../private/producercontract.js';
|
|
9
11
|
import { isRuntimeLogPolicy, runtimePolicyLimits } from '../private/runtimepolicy.js';
|
|
10
12
|
|
|
11
13
|
/** The container output a log line was written to. */
|
|
@@ -333,3 +335,306 @@ export const bindReportRuntimeAssignmentLogsResponse = (
|
|
|
333
335
|
&& response.sequence === batch.sequence);
|
|
334
336
|
return response;
|
|
335
337
|
});
|
|
338
|
+
|
|
339
|
+
// Reading kept logs
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Why a controller no longer holds batches it accepted: `batch-cap`, its bound on how many batches
|
|
343
|
+
* it keeps of one attempt; `byte-cap`, its bound on the text it keeps of one attempt; both drop
|
|
344
|
+
* the oldest batches first; `retention`, the config's `logs.retentionMs` since it accepted them.
|
|
345
|
+
* A node delivers about once a second, so a workload that writes now and then sends many small
|
|
346
|
+
* batches, which the batch bound holds where the text bound would not.
|
|
347
|
+
*/
|
|
348
|
+
export type TRuntimeAssignmentLogTruncationReason = 'batch-cap' | 'byte-cap' | 'retention';
|
|
349
|
+
|
|
350
|
+
/** One batch the controller accepted and still holds, as the node sent it. */
|
|
351
|
+
export interface IRuntimeAssignmentLogBatchRecord {
|
|
352
|
+
kind: 'batch';
|
|
353
|
+
captureId: string;
|
|
354
|
+
sequence: number;
|
|
355
|
+
/** When the controller accepted the batch, in milliseconds since the Unix epoch. */
|
|
356
|
+
acceptedAt: number;
|
|
357
|
+
final: boolean;
|
|
358
|
+
entries: TRuntimeAssignmentLogEntry[];
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* Sequence numbers of one capture whose batches the controller did not receive or no longer holds
|
|
363
|
+
* (its own loss): numbers that never arrived and that the node did not account for, and batches it
|
|
364
|
+
* accepted whose payload it has lost. A gap the node left by discarding is not one: the batch
|
|
365
|
+
* after it opens with the `not-kept` loss that counts it.
|
|
366
|
+
*/
|
|
367
|
+
export interface IRuntimeAssignmentLogGapRecord {
|
|
368
|
+
kind: 'gap';
|
|
369
|
+
captureId: string;
|
|
370
|
+
fromSequence: number;
|
|
371
|
+
toSequence: number;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* Batches of one capture the controller accepted and has since dropped: every batch it held of the
|
|
376
|
+
* capture up to and including `throughSequence`, oldest first, so a truncation is always the start
|
|
377
|
+
* of what remains of a capture. Gaps inside that range are no longer stated. The counts are exact.
|
|
378
|
+
*/
|
|
379
|
+
export interface IRuntimeAssignmentLogTruncationRecord {
|
|
380
|
+
kind: 'truncated';
|
|
381
|
+
captureId: string;
|
|
382
|
+
throughSequence: number;
|
|
383
|
+
/** In the canonical order `batch-cap`, `byte-cap`, `retention`; duplicate-free, at least one. */
|
|
384
|
+
reasons: TRuntimeAssignmentLogTruncationReason[];
|
|
385
|
+
/** The accepted batches dropped, at least one and at most `throughSequence`. */
|
|
386
|
+
batches: number;
|
|
387
|
+
/** The UTF-8 bytes of `text` the dropped batches held. */
|
|
388
|
+
bytes: number;
|
|
389
|
+
/** The entries, lines and losses alike, the dropped batches held. */
|
|
390
|
+
entries: number;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
export type TRuntimeAssignmentLogRecord =
|
|
394
|
+
| IRuntimeAssignmentLogBatchRecord | IRuntimeAssignmentLogGapRecord | IRuntimeAssignmentLogTruncationRecord;
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* One attempt whose logs the controller keeps, as an operator reads it. The controller keeps an
|
|
398
|
+
* attempt's logs from the moment it issues the attempt, so an attempt that failed before writing a
|
|
399
|
+
* line is listed with nothing held, and an attempt its slot released stays readable until
|
|
400
|
+
* `expiresAt`.
|
|
401
|
+
*/
|
|
402
|
+
export interface IRuntimeAssignmentLogAttemptView {
|
|
403
|
+
serviceId: string;
|
|
404
|
+
replicaId: string;
|
|
405
|
+
nodeId: string;
|
|
406
|
+
assignmentId: string;
|
|
407
|
+
attemptId: string;
|
|
408
|
+
/** When the controller issued the attempt, in milliseconds since the Unix epoch. */
|
|
409
|
+
issuedAt: number;
|
|
410
|
+
/** When it last accepted a batch of the attempt; null until it accepted one. */
|
|
411
|
+
lastAcceptedAt: number | null;
|
|
412
|
+
/**
|
|
413
|
+
* When the controller drops the attempt's logs whole: the latest of `issuedAt`, `lastAcceptedAt`
|
|
414
|
+
* and the time the attempt's slot released it, plus the attempt's `logs.retentionMs`. Null while
|
|
415
|
+
* the slot still holds the attempt: no expiry is scheduled, however quiet the attempt is.
|
|
416
|
+
*/
|
|
417
|
+
expiresAt: number | null;
|
|
418
|
+
/** The captures of the attempt the controller accepted a batch of. */
|
|
419
|
+
captures: number;
|
|
420
|
+
/** Those of them whose final batch it accepted. */
|
|
421
|
+
capturesEnded: number;
|
|
422
|
+
/** The UTF-8 bytes of `text` the controller holds of the attempt. */
|
|
423
|
+
keptBytes: number;
|
|
424
|
+
/** The accepted batches of the attempt it has since dropped, stated in truncation records. */
|
|
425
|
+
truncatedBatches: number;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/** One page of a service's attempts, newest issued first, ties in `assignmentId` order. */
|
|
429
|
+
export interface IRuntimeAssignmentLogAttemptPage {
|
|
430
|
+
attempts: IRuntimeAssignmentLogAttemptView[];
|
|
431
|
+
/** Where the next page starts; null when this page is the last. Opaque to the reader. */
|
|
432
|
+
nextCursor: string | null;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* One page of one attempt's kept logs, in delivery order: captures in the order the controller
|
|
437
|
+
* first accepted a batch of each, and within a capture by sequence. A capture's truncation record
|
|
438
|
+
* comes first, a gap record directly before the batch that follows it.
|
|
439
|
+
*/
|
|
440
|
+
export interface IRuntimeAssignmentLogPage {
|
|
441
|
+
attempt: IRuntimeAssignmentLogAttemptView;
|
|
442
|
+
records: TRuntimeAssignmentLogRecord[];
|
|
443
|
+
/** Where the next page starts; null when this page is the last. Opaque to the reader. */
|
|
444
|
+
nextCursor: string | null;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
/**
|
|
448
|
+
* Why a controller refuses a log read: `attempt-not-found` (it keeps no logs of that attempt for
|
|
449
|
+
* that service, never did or no longer does), `cursor-invalid` (a cursor it did not hand out for
|
|
450
|
+
* this read) and `limit-invalid` (a `limit` outside its bound).
|
|
451
|
+
*/
|
|
452
|
+
export type TRuntimeAssignmentLogReadRefusal = 'attempt-not-found' | 'cursor-invalid' | 'limit-invalid';
|
|
453
|
+
|
|
454
|
+
/** The named refusal of a log read, sent as `TypedResponseError.errorData`. */
|
|
455
|
+
export interface IRuntimeAssignmentLogReadErrorData {
|
|
456
|
+
code: TRuntimeAssignmentLogReadRefusal;
|
|
457
|
+
retryable: false;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
export const runtimeAssignmentLogReadContract = Object.freeze({
|
|
461
|
+
error: 'Runtime assignment log read contract rejected.',
|
|
462
|
+
/** The largest `limit` `getServiceRuntimeLogAttempts` accepts; a controller refuses a larger one. */
|
|
463
|
+
maximumAttemptPageSize: 256,
|
|
464
|
+
/** The largest `limit` `getRuntimeAssignmentLogs` accepts, counted in records. */
|
|
465
|
+
maximumPageRecords: 256,
|
|
466
|
+
/**
|
|
467
|
+
* The most entries the records of one log page hold together: four full batches. It keeps a page
|
|
468
|
+
* within the node count every captured value is held to; one batch always fits.
|
|
469
|
+
*/
|
|
470
|
+
maximumPageEntries: 4 * runtimeAssignmentLogContract.maximumEntriesPerBatch,
|
|
471
|
+
/**
|
|
472
|
+
* The canonical JSON of a whole log page is at most two mebibytes, and the controller closes a
|
|
473
|
+
* page before the next record would cross it. One batch always fits: its record is smaller than
|
|
474
|
+
* the report that carried it, which `maximumReportCanonicalBytes` bounded.
|
|
475
|
+
*/
|
|
476
|
+
maximumPageCanonicalBytes: 2 * runtimeAssignmentLogContract.maximumReportCanonicalBytes,
|
|
477
|
+
} as const);
|
|
478
|
+
|
|
479
|
+
const readFail = (): never => { throw new Error(runtimeAssignmentLogReadContract.error); };
|
|
480
|
+
const readSafe = <T>(operationArg: () => T): T => { try { return operationArg(); } catch { return readFail(); } };
|
|
481
|
+
/** The canonical order of `reasons`. */
|
|
482
|
+
const truncationReasons = ['batch-cap', 'byte-cap', 'retention'] as const;
|
|
483
|
+
const cursorOf = (valueArg: unknown): string | null => valueArg === null ? null : networkId(valueArg);
|
|
484
|
+
|
|
485
|
+
const batchRecordOf = (valueArg: unknown): IRuntimeAssignmentLogBatchRecord => {
|
|
486
|
+
const value = networkRecord(valueArg, ['kind', 'captureId', 'sequence', 'acceptedAt', 'final', 'entries']);
|
|
487
|
+
const entries = networkList(value.entries, runtimeAssignmentLogContract.maximumEntriesPerBatch, entryOf);
|
|
488
|
+
requireNetwork(value.kind === 'batch' && typeof value.final === 'boolean' && (entries.length > 0 || value.final));
|
|
489
|
+
return {
|
|
490
|
+
kind: 'batch', captureId: networkId(value.captureId), sequence: networkCounter(value.sequence),
|
|
491
|
+
acceptedAt: networkCounter(value.acceptedAt), final: value.final, entries,
|
|
492
|
+
};
|
|
493
|
+
};
|
|
494
|
+
|
|
495
|
+
const gapRecordOf = (valueArg: unknown): IRuntimeAssignmentLogGapRecord => {
|
|
496
|
+
const value = networkRecord(valueArg, ['kind', 'captureId', 'fromSequence', 'toSequence']);
|
|
497
|
+
const fromSequence = networkCounter(value.fromSequence), toSequence = networkCounter(value.toSequence);
|
|
498
|
+
requireNetwork(value.kind === 'gap' && fromSequence <= toSequence);
|
|
499
|
+
return { kind: 'gap', captureId: networkId(value.captureId), fromSequence, toSequence };
|
|
500
|
+
};
|
|
501
|
+
|
|
502
|
+
const truncationRecordOf = (valueArg: unknown): IRuntimeAssignmentLogTruncationRecord => {
|
|
503
|
+
const value = networkRecord(
|
|
504
|
+
valueArg, ['kind', 'captureId', 'throughSequence', 'reasons', 'batches', 'bytes', 'entries'],
|
|
505
|
+
);
|
|
506
|
+
requireNetwork(value.kind === 'truncated');
|
|
507
|
+
const throughSequence = networkCounter(value.throughSequence);
|
|
508
|
+
const reasons = networkSorted(networkList(value.reasons, truncationReasons.length, (reasonArg) => {
|
|
509
|
+
requireNetwork(isOneOf(truncationReasons, reasonArg));
|
|
510
|
+
return reasonArg;
|
|
511
|
+
}), (reasonArg) => String(truncationReasons.indexOf(reasonArg)));
|
|
512
|
+
requireNetwork(reasons.length > 0);
|
|
513
|
+
return {
|
|
514
|
+
kind: 'truncated', captureId: networkId(value.captureId), throughSequence, reasons,
|
|
515
|
+
batches: networkCounter(value.batches, throughSequence), bytes: countOf(value.bytes), entries: countOf(value.entries),
|
|
516
|
+
};
|
|
517
|
+
};
|
|
518
|
+
|
|
519
|
+
const recordOf = (valueArg: unknown): TRuntimeAssignmentLogRecord => {
|
|
520
|
+
const kind = variantOf(valueArg, 'kind');
|
|
521
|
+
if (kind === 'batch') return batchRecordOf(valueArg);
|
|
522
|
+
if (kind === 'gap') return gapRecordOf(valueArg);
|
|
523
|
+
return truncationRecordOf(valueArg);
|
|
524
|
+
};
|
|
525
|
+
|
|
526
|
+
const opensWithNotKept = (recordArg: IRuntimeAssignmentLogBatchRecord): boolean => {
|
|
527
|
+
const first = recordArg.entries[0];
|
|
528
|
+
return first !== undefined && first.kind === 'lost' && first.reason === 'not-kept';
|
|
529
|
+
};
|
|
530
|
+
|
|
531
|
+
/**
|
|
532
|
+
* Delivery order within a page. Each capture's records are contiguous and never return once
|
|
533
|
+
* another capture's began. A truncation only opens its capture. Every other record continues where
|
|
534
|
+
* the one before it ended: a gap at the next number, never right after another gap, and a batch at
|
|
535
|
+
* the next number or, past numbers the node skipped, opening with the `not-kept` loss that counts
|
|
536
|
+
* them. Nothing follows a final batch.
|
|
537
|
+
*/
|
|
538
|
+
const requireDeliveryOrder = (recordsArg: TRuntimeAssignmentLogRecord[]): void => {
|
|
539
|
+
const begun = new Set<string>();
|
|
540
|
+
let previous: TRuntimeAssignmentLogRecord | null = null;
|
|
541
|
+
for (const record of recordsArg) {
|
|
542
|
+
const continues: boolean = previous !== null && previous.captureId === record.captureId;
|
|
543
|
+
if (!continues) {
|
|
544
|
+
requireNetwork(!begun.has(record.captureId));
|
|
545
|
+
begun.add(record.captureId);
|
|
546
|
+
}
|
|
547
|
+
if (previous === null || !continues) {
|
|
548
|
+
previous = record;
|
|
549
|
+
continue;
|
|
550
|
+
}
|
|
551
|
+
requireNetwork(record.kind !== 'truncated' && !(previous.kind === 'batch' && previous.final));
|
|
552
|
+
const next = (previous.kind === 'batch' ? previous.sequence
|
|
553
|
+
: previous.kind === 'gap' ? previous.toSequence : previous.throughSequence) + 1;
|
|
554
|
+
if (record.kind === 'gap') {
|
|
555
|
+
requireNetwork(previous.kind !== 'gap' && record.fromSequence === next);
|
|
556
|
+
} else {
|
|
557
|
+
requireNetwork(record.sequence === next || (record.sequence > next && opensWithNotKept(record)));
|
|
558
|
+
}
|
|
559
|
+
previous = record;
|
|
560
|
+
}
|
|
561
|
+
};
|
|
562
|
+
|
|
563
|
+
const attemptOf = (valueArg: unknown): IRuntimeAssignmentLogAttemptView => {
|
|
564
|
+
const value = networkRecord(valueArg, [
|
|
565
|
+
'serviceId', 'replicaId', 'nodeId', 'assignmentId', 'attemptId', 'issuedAt', 'lastAcceptedAt', 'expiresAt',
|
|
566
|
+
'captures', 'capturesEnded', 'keptBytes', 'truncatedBatches',
|
|
567
|
+
]);
|
|
568
|
+
requireNetwork(isRuntimeReplicaId(value.replicaId));
|
|
569
|
+
const issuedAt = networkCounter(value.issuedAt);
|
|
570
|
+
const lastAcceptedAt = value.lastAcceptedAt === null ? null : networkCounter(value.lastAcceptedAt);
|
|
571
|
+
const expiresAt = value.expiresAt === null ? null : networkCounter(value.expiresAt);
|
|
572
|
+
const captures = countOf(value.captures), capturesEnded = countOf(value.capturesEnded);
|
|
573
|
+
const keptBytes = countOf(value.keptBytes), truncatedBatches = countOf(value.truncatedBatches);
|
|
574
|
+
requireNetwork((expiresAt === null || expiresAt > Math.max(issuedAt, lastAcceptedAt ?? issuedAt))
|
|
575
|
+
&& capturesEnded <= captures);
|
|
576
|
+
// Nothing accepted means nothing captured, held or dropped.
|
|
577
|
+
requireNetwork(lastAcceptedAt !== null || (captures === 0 && keptBytes === 0 && truncatedBatches === 0));
|
|
578
|
+
return {
|
|
579
|
+
serviceId: networkId(value.serviceId), replicaId: value.replicaId, nodeId: networkNodeId(value.nodeId),
|
|
580
|
+
assignmentId: networkId(value.assignmentId), attemptId: networkId(value.attemptId), issuedAt, lastAcceptedAt,
|
|
581
|
+
expiresAt, captures, capturesEnded, keptBytes, truncatedBatches,
|
|
582
|
+
};
|
|
583
|
+
};
|
|
584
|
+
|
|
585
|
+
const pageLimitOf = (limitArg: unknown, maximumArg: number): number => networkCounter(limitArg, maximumArg);
|
|
586
|
+
|
|
587
|
+
/** Detached exact-schema snapshot of one attempt view. */
|
|
588
|
+
export const snapshotRuntimeAssignmentLogAttemptView = (valueArg: unknown): IRuntimeAssignmentLogAttemptView =>
|
|
589
|
+
readSafe(() => attemptOf(captureNetwork(valueArg)));
|
|
590
|
+
|
|
591
|
+
/** Detached exact-schema snapshot of one attempt page, in its one order. */
|
|
592
|
+
export const snapshotRuntimeAssignmentLogAttemptPage = (valueArg: unknown): IRuntimeAssignmentLogAttemptPage =>
|
|
593
|
+
readSafe(() => {
|
|
594
|
+
const value = networkRecord(captureNetwork(valueArg), ['attempts', 'nextCursor']);
|
|
595
|
+
const attempts = networkList(value.attempts, runtimeAssignmentLogReadContract.maximumAttemptPageSize, attemptOf);
|
|
596
|
+
attempts.forEach((attemptArg, indexArg) => {
|
|
597
|
+
const before = attempts[indexArg - 1];
|
|
598
|
+
requireNetwork(before === undefined || before.issuedAt > attemptArg.issuedAt
|
|
599
|
+
|| (before.issuedAt === attemptArg.issuedAt && before.assignmentId < attemptArg.assignmentId));
|
|
600
|
+
});
|
|
601
|
+
const nextCursor = cursorOf(value.nextCursor);
|
|
602
|
+
requireNetwork(nextCursor === null || attempts.length > 0);
|
|
603
|
+
return { attempts, nextCursor };
|
|
604
|
+
});
|
|
605
|
+
|
|
606
|
+
/** Detached exact-schema snapshot of one log page, bounded and in delivery order. */
|
|
607
|
+
export const snapshotRuntimeAssignmentLogPage = (valueArg: unknown): IRuntimeAssignmentLogPage => readSafe(() => {
|
|
608
|
+
const value = networkRecord(
|
|
609
|
+
captureNetwork(valueArg, runtimeAssignmentLogReadContract.maximumPageCanonicalBytes),
|
|
610
|
+
['attempt', 'records', 'nextCursor'],
|
|
611
|
+
);
|
|
612
|
+
const records = networkList(value.records, runtimeAssignmentLogReadContract.maximumPageRecords, recordOf);
|
|
613
|
+
requireNetwork(records.reduce((totalArg, recordArg) => totalArg + (recordArg.kind === 'batch' ? recordArg.entries.length : 0), 0)
|
|
614
|
+
<= runtimeAssignmentLogReadContract.maximumPageEntries);
|
|
615
|
+
requireDeliveryOrder(records);
|
|
616
|
+
const nextCursor = cursorOf(value.nextCursor);
|
|
617
|
+
requireNetwork(nextCursor === null || records.length > 0);
|
|
618
|
+
return { attempt: attemptOf(value.attempt), records, nextCursor };
|
|
619
|
+
});
|
|
620
|
+
|
|
621
|
+
/** Matches an attempt page to the read that asked for it: that service's attempts, within its limit. */
|
|
622
|
+
export const bindRuntimeAssignmentLogAttemptPage = (
|
|
623
|
+
valueArg: unknown, serviceIdArg: string, limitArg: number,
|
|
624
|
+
): IRuntimeAssignmentLogAttemptPage => readSafe(() => {
|
|
625
|
+
const page = snapshotRuntimeAssignmentLogAttemptPage(valueArg);
|
|
626
|
+
const limit = pageLimitOf(limitArg, runtimeAssignmentLogReadContract.maximumAttemptPageSize);
|
|
627
|
+
requireNetwork(page.attempts.length <= limit && page.attempts.every((attemptArg) => attemptArg.serviceId === serviceIdArg));
|
|
628
|
+
return page;
|
|
629
|
+
});
|
|
630
|
+
|
|
631
|
+
/** Matches a log page to the read that asked for it: that attempt of that service, within its limit. */
|
|
632
|
+
export const bindRuntimeAssignmentLogPage = (
|
|
633
|
+
valueArg: unknown, serviceIdArg: string, assignmentIdArg: string, limitArg: number,
|
|
634
|
+
): IRuntimeAssignmentLogPage => readSafe(() => {
|
|
635
|
+
const page = snapshotRuntimeAssignmentLogPage(valueArg);
|
|
636
|
+
const limit = pageLimitOf(limitArg, runtimeAssignmentLogReadContract.maximumPageRecords);
|
|
637
|
+
requireNetwork(page.records.length <= limit && page.attempt.serviceId === serviceIdArg
|
|
638
|
+
&& page.attempt.assignmentId === assignmentIdArg);
|
|
639
|
+
return page;
|
|
640
|
+
});
|
|
@@ -92,6 +92,7 @@ const serviceRuntimeRefusals = [
|
|
|
92
92
|
'objectstorage-endpoint-missing', 'objectstorage-binding-invalid', 'objectstorage-environment-conflict',
|
|
93
93
|
'objectstorage-endpoint-unselected', 'database-endpoint-missing', 'database-endpoint-unselected',
|
|
94
94
|
'published-port-reserved', 'published-port-symmetric-without-egress', 'readiness-network-unsupported',
|
|
95
|
+
'cluster-relay-missing', 'cluster-ingress-environment-conflict',
|
|
95
96
|
] as const;
|
|
96
97
|
|
|
97
98
|
/**
|
|
@@ -165,6 +166,15 @@ export type TServiceRuntimeWaitingReason = typeof serviceRuntimeWaitingReasons[n
|
|
|
165
166
|
* `unsupported-readiness`). `isServiceRuntimeReadinessReachable` is this rule; a producer answers
|
|
166
167
|
* what it returns. The spec snapshot still reads such a spec, so a stored one stays readable and
|
|
167
168
|
* is answered with this refusal rather than refused as data.
|
|
169
|
+
*
|
|
170
|
+
* The two `cluster-` refusals name why the environment of a cluster's designated ingress service
|
|
171
|
+
* cannot be composed (`composeClusterIngressEnvironment`); a service no designation names is never
|
|
172
|
+
* judged by them. `cluster-relay-missing` is a cluster that states no valid relay
|
|
173
|
+
* (`IClusterRelay`): the ingress registers through its relay's exact origin, so without one it has
|
|
174
|
+
* nothing to start with, and an administrator enables the relay (`enableClusterRelay`).
|
|
175
|
+
* `cluster-ingress-environment-conflict` is an ingress service whose own environment already sets
|
|
176
|
+
* one of `clusterIngressEnvironmentNames`: Cloudly owns those names, and a service value would
|
|
177
|
+
* otherwise silently replace Cloudly's or be replaced by it.
|
|
168
178
|
*/
|
|
169
179
|
export type TServiceRuntimeRefusal = typeof serviceRuntimeRefusals[number];
|
|
170
180
|
|
|
@@ -46,14 +46,23 @@ export const networkSafe = <T>(operation: () => T): T => {
|
|
|
46
46
|
}
|
|
47
47
|
};
|
|
48
48
|
|
|
49
|
-
/**
|
|
50
|
-
export const
|
|
49
|
+
/** The canonical JSON bound every runtime network contract captures with, unless it states its own. */
|
|
50
|
+
export const networkMaximumCanonicalBytes = 1_048_576;
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Bound decoded transport input before canonicalization; never execute getters/toJSON.
|
|
54
|
+
* `maximumBytes` bounds the whole value's canonical JSON; a single string stays within
|
|
55
|
+
* `networkMaximumCanonicalBytes` whatever the whole may hold.
|
|
56
|
+
*/
|
|
57
|
+
export const captureNetwork = (
|
|
58
|
+
value: unknown, maximumBytes = networkMaximumCanonicalBytes,
|
|
59
|
+
): unknown => networkSafe(() => {
|
|
51
60
|
const encoder = new TextEncoder(), ancestors = new Set<object>();
|
|
52
61
|
let nodes = 0, bytes = 0;
|
|
53
62
|
const account = (text: string) => {
|
|
54
|
-
requireNetwork(text.length <=
|
|
63
|
+
requireNetwork(text.length <= networkMaximumCanonicalBytes);
|
|
55
64
|
bytes += encoder.encode(text).length;
|
|
56
|
-
requireNetwork(bytes <=
|
|
65
|
+
requireNetwork(bytes <= maximumBytes);
|
|
57
66
|
};
|
|
58
67
|
const copy = (item: unknown, depth: number): unknown => {
|
|
59
68
|
requireNetwork(++nodes <= 65_536 && depth <= 24);
|
|
@@ -90,7 +99,7 @@ export const captureNetwork = (value: unknown): unknown => networkSafe(() => {
|
|
|
90
99
|
} finally { ancestors.delete(item); }
|
|
91
100
|
};
|
|
92
101
|
const result = copy(value, 0);
|
|
93
|
-
requireNetwork(encoder.encode(networkCanonical(result)).length <=
|
|
102
|
+
requireNetwork(encoder.encode(networkCanonical(result)).length <= maximumBytes);
|
|
94
103
|
return result;
|
|
95
104
|
});
|
|
96
105
|
|
package/ts/requests/service.ts
CHANGED
|
@@ -19,6 +19,10 @@ import type {
|
|
|
19
19
|
import type { TSha256Digest } from '../data/immutableimage.js';
|
|
20
20
|
import type { ILegacyServiceRuntimeAttestation } from '../data/deploymentlegacyimport.js';
|
|
21
21
|
import type { IProtocolOffer } from '../protocol.js';
|
|
22
|
+
import type {
|
|
23
|
+
IRuntimeAssignmentLogAttemptPage,
|
|
24
|
+
IRuntimeAssignmentLogPage,
|
|
25
|
+
} from '../data/runtimeassignmentlogs.js';
|
|
22
26
|
import type {
|
|
23
27
|
IServiceRuntimeAssignmentView,
|
|
24
28
|
IServiceRuntimeSpec,
|
|
@@ -629,6 +633,52 @@ extends plugins.typedrequestInterfaces.implementsTR<
|
|
|
629
633
|
};
|
|
630
634
|
}
|
|
631
635
|
|
|
636
|
+
/**
|
|
637
|
+
* The attempts of one service whose workload logs Cloudly keeps, newest issued first, a page at a
|
|
638
|
+
* time: a slot's replaced and released attempts stay readable until their logs expire, which no
|
|
639
|
+
* single response bounds. `limit` runs from 1 to
|
|
640
|
+
* `runtimeAssignmentLogReadContract.maximumAttemptPageSize`; Cloudly refuses any other with
|
|
641
|
+
* `IRuntimeAssignmentLogReadErrorData`. `cursor` is null for the first page, then the page's own
|
|
642
|
+
* `nextCursor`. `bindRuntimeAssignmentLogAttemptPage` checks an answer against its read.
|
|
643
|
+
*/
|
|
644
|
+
export interface IRequest_Any_Cloudly_GetServiceRuntimeLogAttempts
|
|
645
|
+
extends plugins.typedrequestInterfaces.implementsTR<
|
|
646
|
+
plugins.typedrequestInterfaces.ITypedRequest,
|
|
647
|
+
IRequest_Any_Cloudly_GetServiceRuntimeLogAttempts
|
|
648
|
+
> {
|
|
649
|
+
method: 'getServiceRuntimeLogAttempts';
|
|
650
|
+
request: {
|
|
651
|
+
identity: IIdentity;
|
|
652
|
+
serviceId: string;
|
|
653
|
+
cursor: string | null;
|
|
654
|
+
limit: number;
|
|
655
|
+
};
|
|
656
|
+
response: IRuntimeAssignmentLogAttemptPage;
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
/**
|
|
660
|
+
* One attempt's kept workload logs, a page at a time in delivery order, with the gaps Cloudly found
|
|
661
|
+
* and the truncations it made stated in place. `limit` counts records and runs from 1 to
|
|
662
|
+
* `runtimeAssignmentLogReadContract.maximumPageRecords`; a page also ends before it would cross
|
|
663
|
+
* `maximumPageEntries` or `maximumPageCanonicalBytes`, so it may hold fewer records than `limit`
|
|
664
|
+
* while `nextCursor` still names more. `bindRuntimeAssignmentLogPage` checks an answer against its read.
|
|
665
|
+
*/
|
|
666
|
+
export interface IRequest_Any_Cloudly_GetRuntimeAssignmentLogs
|
|
667
|
+
extends plugins.typedrequestInterfaces.implementsTR<
|
|
668
|
+
plugins.typedrequestInterfaces.ITypedRequest,
|
|
669
|
+
IRequest_Any_Cloudly_GetRuntimeAssignmentLogs
|
|
670
|
+
> {
|
|
671
|
+
method: 'getRuntimeAssignmentLogs';
|
|
672
|
+
request: {
|
|
673
|
+
identity: IIdentity;
|
|
674
|
+
serviceId: string;
|
|
675
|
+
assignmentId: string;
|
|
676
|
+
cursor: string | null;
|
|
677
|
+
limit: number;
|
|
678
|
+
};
|
|
679
|
+
response: IRuntimeAssignmentLogPage;
|
|
680
|
+
}
|
|
681
|
+
|
|
632
682
|
/**
|
|
633
683
|
* Cloudly -> admin UIs: a service's runtime spec, status or assignments changed. It carries no
|
|
634
684
|
* state, so a late or repeated push can never overwrite a newer read; the UI reads the views again.
|