@bli-cockpit/cli 0.2.94 → 0.2.96
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/dist/commands/heartbeat-token.js +80 -0
- package/dist/commands/heartbeat.js +24 -12
- package/dist/commands/public-root.js +1 -1
- package/dist/commands/setup-receipt-lines.js +17 -0
- package/dist/commands/sync-followups-autostart.js +99 -0
- package/dist/commands/sync-followups-memory.js +123 -0
- package/dist/commands/sync-followups-self-update.js +127 -0
- package/dist/commands/sync-followups-staging.js +202 -0
- package/dist/commands/sync-followups.js +30 -511
- package/dist/commands/sync-heartbeat.js +36 -0
- package/dist/commands/sync-receipt.js +138 -0
- package/dist/commands/sync-report.js +153 -0
- package/dist/commands/sync-roots.js +28 -0
- package/dist/commands/sync-run.js +52 -0
- package/dist/commands/sync-types.js +1 -0
- package/dist/commands/sync.js +94 -357
- package/dist/disk-usage-classify.js +109 -0
- package/dist/disk-usage-facts.js +4 -0
- package/dist/disk-usage-files.js +99 -0
- package/dist/disk-usage-footprint.js +45 -0
- package/dist/disk-usage-ledger.js +49 -0
- package/dist/disk-usage-scan.js +80 -0
- package/dist/disk-usage-totals.js +83 -0
- package/dist/disk-usage.js +33 -396
- package/dist/local-state-pairing.js +41 -1
- package/dist/local-state.js +2 -2
- package/dist/upload-envelope-build.js +7 -0
- package/package.json +4 -4
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import { redactedSyncErrorDetail, reportInstallEventsBestEffort, } from "./install-receipts.js";
|
|
2
|
+
import { getCollectorRuntimePaths } from "../local-state.js";
|
|
3
|
+
import { runStagingPrune } from "../disk-prune.js";
|
|
4
|
+
import { runEvidenceReconcile, } from "../evidence-reconcile-client.js";
|
|
5
|
+
import { runEvidenceRedelivery, } from "../evidence-redelivery.js";
|
|
6
|
+
/** How many reconcile batches (of up to 500 hashes each) one sync tick may spend. */
|
|
7
|
+
const RECONCILE_BATCHES_PER_TICK = 1;
|
|
8
|
+
/**
|
|
9
|
+
* BLI-3619: staged raw evidence Tower has already accepted stops living on the
|
|
10
|
+
* laptop forever.
|
|
11
|
+
*
|
|
12
|
+
* Same shape as the memory install beside it and for the same reasons: it runs
|
|
13
|
+
* only once collection's own outcome is decided and reported, at most once a
|
|
14
|
+
* day, it cannot throw, and its outcome is its own named receipt rather than a
|
|
15
|
+
* sync failure. A machine that cannot prune is a machine short of disk, and
|
|
16
|
+
* that must never also be a machine that stops collecting.
|
|
17
|
+
*
|
|
18
|
+
* The rule itself is `disk-retention.ts`: only objects the upload ledger
|
|
19
|
+
* vouches for are ever deleted, and anything undelivered is counted, aged and
|
|
20
|
+
* named instead.
|
|
21
|
+
*
|
|
22
|
+
* BLI-3619's second half runs FIRST, every tick, bounded to
|
|
23
|
+
* `RECONCILE_BATCHES_PER_TICK` (one batch of up to 500 hashes): the local
|
|
24
|
+
* ledger's own memory is capped, so a laptop that keeps accumulating
|
|
25
|
+
* `unknown` state needs SOMETHING asking the server on a schedule, not only
|
|
26
|
+
* when a person happens to type `cockpit clean --reconcile`. One batch a tick
|
|
27
|
+
* is not throttled to once a day like the prune below — an `unknown` backlog
|
|
28
|
+
* drains at up to 500 hashes/15 min, and the ordinary case (nothing unknown)
|
|
29
|
+
* costs one cheap local read and no network call at all, so it never competes
|
|
30
|
+
* with collection for the tick's time. A reconcile failure never blocks the
|
|
31
|
+
* prune that follows it.
|
|
32
|
+
*
|
|
33
|
+
* BLI-3797 sits between them: the reconcile has just established which staged
|
|
34
|
+
* objects the SERVER says it never received, so the redelivery drain re-offers
|
|
35
|
+
* exactly those, bounded, before the prune runs and can delete whatever landed.
|
|
36
|
+
* Neither of the two can block the prune, and none of the three can fail a tick.
|
|
37
|
+
*/
|
|
38
|
+
export async function runStagingPruneAfterSync(command, io, dashboardUrl, options = {}) {
|
|
39
|
+
const events = [];
|
|
40
|
+
const reconciled = await runReconcileFollowUp(command, io, dashboardUrl, options);
|
|
41
|
+
if (reconciled)
|
|
42
|
+
events.push(reconcileEvent(reconciled));
|
|
43
|
+
const redelivered = await runRedeliveryFollowUp(command, io, dashboardUrl, options);
|
|
44
|
+
if (redelivered)
|
|
45
|
+
events.push(redeliveryEvent(redelivered));
|
|
46
|
+
let result;
|
|
47
|
+
try {
|
|
48
|
+
result = await runStagingPrune(getCollectorRuntimePaths(command.homeDir), {
|
|
49
|
+
env: io.env,
|
|
50
|
+
...(options.now ? { now: options.now } : {}),
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
catch (error) {
|
|
54
|
+
// runStagingPrune already catches everything it can reach; this is the
|
|
55
|
+
// last-resort net so a prune crash truly cannot touch the sync result.
|
|
56
|
+
result = { ...prunedNothing(), reason: "prune_threw", status: "fail" };
|
|
57
|
+
console.error("[collector prune] the prune follow-up threw", JSON.stringify({
|
|
58
|
+
reason: "prune_followup_threw",
|
|
59
|
+
detail: redactedSyncErrorDetail(error),
|
|
60
|
+
}));
|
|
61
|
+
}
|
|
62
|
+
// The daily throttle is the steady state — reporting it would post a receipt
|
|
63
|
+
// on 95 of every 96 ticks for no new information.
|
|
64
|
+
if (result.reason !== "throttled_recent_run") {
|
|
65
|
+
events.push(stagingPruneEvent(result));
|
|
66
|
+
}
|
|
67
|
+
if (events.length === 0)
|
|
68
|
+
return;
|
|
69
|
+
await reportInstallEventsBestEffort({
|
|
70
|
+
homeDir: command.homeDir,
|
|
71
|
+
dashboardUrl,
|
|
72
|
+
command: "sync",
|
|
73
|
+
events,
|
|
74
|
+
json: command.json,
|
|
75
|
+
io,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Never throws (`runEvidenceReconcile` already never does); returns `null`
|
|
80
|
+
* for the boring, common case — nothing this tick was `unknown` — so that
|
|
81
|
+
* case costs no receipt either, the same rule the prune's own daily throttle
|
|
82
|
+
* follows above.
|
|
83
|
+
*/
|
|
84
|
+
async function runReconcileFollowUp(command, io, dashboardUrl, options) {
|
|
85
|
+
const result = await runEvidenceReconcile({
|
|
86
|
+
homeDir: command.homeDir,
|
|
87
|
+
dashboardUrl,
|
|
88
|
+
maxBatches: RECONCILE_BATCHES_PER_TICK,
|
|
89
|
+
fetch: io.fetch,
|
|
90
|
+
...(options.now ? { now: options.now } : {}),
|
|
91
|
+
});
|
|
92
|
+
return result.reason === "nothing_unknown" ? null : result;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* BLI-3797, between the reconcile above and the prune below, and in that order
|
|
96
|
+
* for a reason: reconcile turns `unknown` into a server-backed answer, this
|
|
97
|
+
* re-offers what that answer says never landed, and the prune then deletes
|
|
98
|
+
* whatever this just made durable. Running the drain first would ask the server
|
|
99
|
+
* about hashes it is about to be told the truth about; running it after the
|
|
100
|
+
* prune would leave a tick's worth of freed cap unused.
|
|
101
|
+
*
|
|
102
|
+
* Never throws (`runEvidenceRedelivery` already never does); returns `null` for
|
|
103
|
+
* the boring, common case — nothing on this disk is undelivered — so the steady
|
|
104
|
+
* state costs no receipt, the same rule the reconcile follow-up above and the
|
|
105
|
+
* prune's daily throttle below both follow.
|
|
106
|
+
*/
|
|
107
|
+
async function runRedeliveryFollowUp(command, io, dashboardUrl, options) {
|
|
108
|
+
const result = await runEvidenceRedelivery({
|
|
109
|
+
homeDir: command.homeDir,
|
|
110
|
+
dashboardUrl,
|
|
111
|
+
env: io.env,
|
|
112
|
+
fetch: io.fetch,
|
|
113
|
+
...(options.now ? { now: options.now } : {}),
|
|
114
|
+
});
|
|
115
|
+
return result.reason === "nothing_uncommitted" ? null : result;
|
|
116
|
+
}
|
|
117
|
+
/** Counts and byte totals only; no pack id and no hash travels in a receipt. */
|
|
118
|
+
function redeliveryEvent(result) {
|
|
119
|
+
const detail = [
|
|
120
|
+
`offered ${result.offered} object(s), ${result.offered_bytes}B, across ${result.packs} pack(s)`,
|
|
121
|
+
`uploaded ${result.uploaded} (${result.uploaded_bytes}B), reused ${result.reused}, failed ${result.failed}`,
|
|
122
|
+
`held ${result.held}, deferred ${result.deferred} (${result.deferred_bytes}B)`,
|
|
123
|
+
result.failure_reasons.length > 0
|
|
124
|
+
? `failure_reasons ${result.failure_reasons.join(",")}`
|
|
125
|
+
: null,
|
|
126
|
+
]
|
|
127
|
+
.filter((part) => Boolean(part))
|
|
128
|
+
.join("; ");
|
|
129
|
+
if (result.status === "fail") {
|
|
130
|
+
return {
|
|
131
|
+
step: "evidence_redelivery",
|
|
132
|
+
status: "fail",
|
|
133
|
+
error_code: result.reason,
|
|
134
|
+
error_detail: detail,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
if (result.status === "skipped") {
|
|
138
|
+
return {
|
|
139
|
+
step: "evidence_redelivery",
|
|
140
|
+
status: "skipped",
|
|
141
|
+
error_code: result.reason,
|
|
142
|
+
error_detail: detail,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
return { step: "evidence_redelivery", status: "ok", error_detail: detail };
|
|
146
|
+
}
|
|
147
|
+
function prunedNothing() {
|
|
148
|
+
return {
|
|
149
|
+
status: "skipped",
|
|
150
|
+
reason: "prune_threw",
|
|
151
|
+
deleted_files: 0,
|
|
152
|
+
deleted_bytes: 0,
|
|
153
|
+
removed_packs: 0,
|
|
154
|
+
kept_uncommitted: 0,
|
|
155
|
+
kept_uncommitted_bytes: 0,
|
|
156
|
+
oldest_uncommitted_age_ms: 0,
|
|
157
|
+
kept_unknown: 0,
|
|
158
|
+
kept_unknown_bytes: 0,
|
|
159
|
+
kept_in_window: 0,
|
|
160
|
+
cap_bytes: 0,
|
|
161
|
+
bytes_before: 0,
|
|
162
|
+
bytes_after: 0,
|
|
163
|
+
cap_blocked_by_uncommitted: false,
|
|
164
|
+
cap_blocked_count: 0,
|
|
165
|
+
failed_deletions: 0,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
/** Counts and byte totals only; no pack id and no path travels in a receipt. */
|
|
169
|
+
function stagingPruneEvent(result) {
|
|
170
|
+
const detail = [
|
|
171
|
+
`freed ${result.deleted_bytes}B in ${result.deleted_files} file(s)`,
|
|
172
|
+
`held ${result.kept_uncommitted_bytes}B uncommitted`,
|
|
173
|
+
result.cap_blocked_by_uncommitted
|
|
174
|
+
? `staging_cap_blocked_by_uncommitted ${result.cap_blocked_count}`
|
|
175
|
+
: null,
|
|
176
|
+
result.failed_deletions > 0
|
|
177
|
+
? `failed_deletions ${result.failed_deletions}`
|
|
178
|
+
: null,
|
|
179
|
+
]
|
|
180
|
+
.filter((part) => Boolean(part))
|
|
181
|
+
.join("; ");
|
|
182
|
+
if (result.status === "fail") {
|
|
183
|
+
return {
|
|
184
|
+
step: "staging_prune",
|
|
185
|
+
status: "fail",
|
|
186
|
+
error_code: result.reason,
|
|
187
|
+
error_detail: detail,
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
if (result.status === "skipped") {
|
|
191
|
+
return { step: "staging_prune", status: "skipped", error_code: result.reason };
|
|
192
|
+
}
|
|
193
|
+
return { step: "staging_prune", status: "ok", error_detail: detail };
|
|
194
|
+
}
|
|
195
|
+
/** Counts only; no hash and no pack id travels in a receipt. */
|
|
196
|
+
function reconcileEvent(result) {
|
|
197
|
+
const detail = `asked ${result.asked} hash(es) across ${result.batches}/${result.total_batches} batch(es); committed ${result.committed}, not_committed ${result.not_committed}, unknown_to_server ${result.unknown_to_server}, failed_batches ${result.failed_batches}`;
|
|
198
|
+
if (result.status === "fail") {
|
|
199
|
+
return { step: "evidence_reconcile", status: "fail", error_code: result.reason, error_detail: detail };
|
|
200
|
+
}
|
|
201
|
+
return { step: "evidence_reconcile", status: "ok", error_detail: detail };
|
|
202
|
+
}
|