@bridge4dev/runner 0.57.0 → 0.58.2
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/adapters/claude.js +23 -4
- package/dist/adapters/codex-protocol.js +6 -1
- package/dist/adapters/codex.js +11 -2
- package/dist/adapters/types.js +14 -0
- package/dist/cage-authority.d.ts +118 -0
- package/dist/cage-authority.js +241 -0
- package/dist/config.d.ts +83 -5
- package/dist/config.js +59 -1
- package/dist/daemon-lock.d.ts +43 -0
- package/dist/daemon-lock.js +107 -0
- package/dist/host-load.d.ts +9 -0
- package/dist/host-load.js +9 -0
- package/dist/index.js +222 -20
- package/dist/policy.d.ts +9 -0
- package/dist/policy.js +71 -1
- package/dist/protocol.d.ts +43 -27
- package/dist/recipe-schema.d.ts +12 -12
- package/dist/regex-guard.js +6 -6
- package/dist/self-update.js +22 -1
- package/dist/service-unit.d.ts +35 -3
- package/dist/service-unit.js +82 -5
- package/dist/session-allocator.d.ts +259 -0
- package/dist/session-allocator.js +492 -0
- package/dist/session-cage.d.ts +229 -2
- package/dist/session-cage.js +590 -40
- package/dist/session-limits.d.ts +71 -0
- package/dist/session-limits.js +93 -0
- package/dist/session-stall.d.ts +353 -0
- package/dist/session-stall.js +760 -0
- package/dist/supervisor.d.ts +223 -33
- package/dist/supervisor.js +845 -94
- package/dist/systemd-memory.js +2 -5
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/dist/service-unit.js
CHANGED
|
@@ -129,7 +129,7 @@ export function buildUnit(execStart, nodeBinary = process.execPath) {
|
|
|
129
129
|
* which is the only way to fix the servers that already have the bad numbers
|
|
130
130
|
* baked in — and it never overwrites a unit the operator edited by hand.
|
|
131
131
|
*/
|
|
132
|
-
export const LIMITS_VERSION =
|
|
132
|
+
export const LIMITS_VERSION = 6;
|
|
133
133
|
const LIMITS_MARKER = '# devbridge-limits-version:';
|
|
134
134
|
/** `zz-` so it sorts last: an operator's own drop-in should still win. */
|
|
135
135
|
const LIMITS_FILE = 'zz-devbridge-limits.conf';
|
|
@@ -277,6 +277,21 @@ const CEILING_FLOOR_BYTES = 2 * GIB;
|
|
|
277
277
|
* - cap 85 % of total: on an idle dedicated box `MemAvailable` is nearly the whole
|
|
278
278
|
* machine, and a ceiling of «everything» is the bug this function exists to fix.
|
|
279
279
|
*/
|
|
280
|
+
/**
|
|
281
|
+
* What the machine keeps for itself, whatever the agents are doing.
|
|
282
|
+
*
|
|
283
|
+
* Enough for sshd, journald, the kernel and some page cache to keep working
|
|
284
|
+
* while our cgroups sit at their ceiling. Proportional on a big machine,
|
|
285
|
+
* absolute on a small one, because 15 % of 4 GB is not enough to stay reachable.
|
|
286
|
+
*
|
|
287
|
+
* Hoisted out of {@link memoryPolicy} for #398 S3: the live allocator has to
|
|
288
|
+
* subtract exactly the same reserve when it decides how much of the machine is
|
|
289
|
+
* free right now, and a second copy of this number would drift from this one on
|
|
290
|
+
* the first machine where it mattered.
|
|
291
|
+
*/
|
|
292
|
+
export function machineReserveBytes(totalBytes) {
|
|
293
|
+
return Math.max(1.5 * GIB, totalBytes * 0.15);
|
|
294
|
+
}
|
|
280
295
|
export function memoryPolicy(facts, minCeilingBytes = managedUsageFloorBytes(facts)) {
|
|
281
296
|
const { totalBytes, availableBytes, ownUsageBytes, sessionsUsageBytes, uptimeSec } = facts;
|
|
282
297
|
const swapMaxBytes = Math.floor(Math.max(0, facts.swapTotalBytes ?? 0) * SESSIONS_SWAP_SHARE);
|
|
@@ -307,7 +322,7 @@ export function memoryPolicy(facts, minCeilingBytes = managedUsageFloorBytes(fac
|
|
|
307
322
|
// Enough for sshd, journald, the kernel and some page cache to keep working
|
|
308
323
|
// while the cgroup sits at its ceiling. Proportional on a big machine, absolute
|
|
309
324
|
// on a small one, because 15 % of 4 GB is not enough to stay reachable.
|
|
310
|
-
const reserve =
|
|
325
|
+
const reserve = machineReserveBytes(totalBytes);
|
|
311
326
|
// Both cgroups are added back, and for the same reason ours always was: what
|
|
312
327
|
// they hold is already OUT of `MemAvailable`, so leaving the sessions out
|
|
313
328
|
// makes a loaded machine look starved and walks the ceiling down under the
|
|
@@ -682,7 +697,7 @@ const MANAGED_HEADER = [
|
|
|
682
697
|
* can measure one (the drift check treats a file with no
|
|
683
698
|
* `MemoryMax` as outdated).
|
|
684
699
|
*/
|
|
685
|
-
export function buildSessionsSliceOverride(facts = readMemoryFacts(), sessionsUsageBytes = readSessionsSliceUsageOrNull()) {
|
|
700
|
+
export function buildSessionsSliceOverride(facts = readMemoryFacts(), sessionsUsageBytes = readSessionsSliceUsageOrNull(), maxSessions = DEFAULT_MAX_SESSIONS) {
|
|
686
701
|
const memory = facts ? memoryPolicy(facts) : null;
|
|
687
702
|
return ([
|
|
688
703
|
...MANAGED_HEADER,
|
|
@@ -691,6 +706,41 @@ export function buildSessionsSliceOverride(facts = readMemoryFacts(), sessionsUs
|
|
|
691
706
|
? [
|
|
692
707
|
`# ${memory.measured ? 'measured headroom' : 'still booting — conservative fraction of total'}`,
|
|
693
708
|
`MemoryMax=${asMiB(memory.maxBytes)}`,
|
|
709
|
+
/**
|
|
710
|
+
* The COLLECTIVE brake, and the thing that makes #398 S3 safe.
|
|
711
|
+
*
|
|
712
|
+
* Since S3 each session's own brake is deliberately oversold — every
|
|
713
|
+
* session may grow into everything that is free — so the sum of the
|
|
714
|
+
* brakes is larger than this slice. That is only safe while THIS
|
|
715
|
+
* line exists: as the sessions together approach the ceiling the
|
|
716
|
+
* kernel slows all of them and reclaims first from whoever is above
|
|
717
|
+
* their `MemoryLow`, and the hard ceiling above, where the OOM
|
|
718
|
+
* killer picks a victim across the whole slice, is never reached.
|
|
719
|
+
*
|
|
720
|
+
* Measured on this host before it existed: the slice read
|
|
721
|
+
* `MemoryHigh=infinity`, overflowing it ended in a kill rather than
|
|
722
|
+
* throttling, and the one killed was the neighbour sitting under its
|
|
723
|
+
* own limit (`memory.events` of the victim: `high 0, max 0, oom 0,
|
|
724
|
+
* oom_kill 1`). The allocator reads this property back and falls
|
|
725
|
+
* back to a non-oversold formula when it is not in force.
|
|
726
|
+
*/
|
|
727
|
+
`MemoryHigh=${asMiB(Math.floor(memory.maxBytes * SLICE_BRAKE_OF_CEILING))}`,
|
|
728
|
+
/**
|
|
729
|
+
* …and the protection the guarantees are built on.
|
|
730
|
+
*
|
|
731
|
+
* `memory.low` of a child is bounded by its ancestors' protection:
|
|
732
|
+
* without this line a session's own `MemoryLow` protects nothing at
|
|
733
|
+
* all. Deliberately a CONSTANT (seats × one guarantee), not the sum
|
|
734
|
+
* of the guarantees of the sessions that happen to be live: the sum
|
|
735
|
+
* changes on every start and finish, the slice is never rewritten
|
|
736
|
+
* live (a blocker QA found on 07.09 — a rewritten slice kills the
|
|
737
|
+
* sessions under it), so a number that depended on the live roster
|
|
738
|
+
* would be true for the hour after this file is written and a lie
|
|
739
|
+
* for the rest of it. What separates one session from another lives
|
|
740
|
+
* on their own scopes; this line only gives them the right to be
|
|
741
|
+
* protected at all.
|
|
742
|
+
*/
|
|
743
|
+
`MemoryLow=${asMiB(Math.min(Math.max(1, maxSessions) * SESSION_GUARANTEE_BYTES, memory.maxBytes))}`,
|
|
694
744
|
]
|
|
695
745
|
: unmeasuredSliceCeiling(sessionsUsageBytes)),
|
|
696
746
|
// The line the cage is built on. See `session-cage.ts` and the note above.
|
|
@@ -701,6 +751,31 @@ export function buildSessionsSliceOverride(facts = readMemoryFacts(), sessionsUs
|
|
|
701
751
|
'CPUAccounting=yes',
|
|
702
752
|
].join('\n') + '\n');
|
|
703
753
|
}
|
|
754
|
+
/**
|
|
755
|
+
* Where the collective brake sits under the collective ceiling.
|
|
756
|
+
*
|
|
757
|
+
* Nine tenths: close enough that the sessions get almost the whole pot before
|
|
758
|
+
* anything is throttled, far enough that the kernel has a tenth of the pot in
|
|
759
|
+
* which to slow everyone down and reclaim, instead of arriving at the hard
|
|
760
|
+
* ceiling and choosing a victim.
|
|
761
|
+
*/
|
|
762
|
+
export const SLICE_BRAKE_OF_CEILING = 0.9;
|
|
763
|
+
/**
|
|
764
|
+
* The guarantee one session gets, in the one place the slice needs to know it.
|
|
765
|
+
*
|
|
766
|
+
* Copied rather than imported from `session-allocator.ts`: that module imports
|
|
767
|
+
* `session-cage.ts`, which imports this one, and the cycle would be real. The
|
|
768
|
+
* two are compared by a test (`service-unit.test.ts`) so they cannot drift.
|
|
769
|
+
*/
|
|
770
|
+
const SESSION_GUARANTEE_BYTES = 2048 * MIB;
|
|
771
|
+
/**
|
|
772
|
+
* Seats assumed when nobody has said otherwise.
|
|
773
|
+
*
|
|
774
|
+
* Three, which is what `DevServer.maxSessions` defaults to in the database. The
|
|
775
|
+
* runner's own config may say fewer; the slice's protection is sized for the
|
|
776
|
+
* seats the machine sells, not for the sessions that happen to be running.
|
|
777
|
+
*/
|
|
778
|
+
export const DEFAULT_MAX_SESSIONS = 3;
|
|
704
779
|
/**
|
|
705
780
|
* The `[Slice]` lines for a machine whose memory the policy could not measure.
|
|
706
781
|
* See {@link buildSessionsSliceOverride} for why each of the three answers is
|
|
@@ -853,7 +928,9 @@ function swapShareHasDrifted(contents, facts) {
|
|
|
853
928
|
* arrive at the same number — otherwise a write whose floor was binding would be
|
|
854
929
|
* seen as drifted on the very next call and rewritten forever.
|
|
855
930
|
*/
|
|
856
|
-
export function writeLimitsOverride(force = false, home = systemdUserHome(), facts = readMemoryFacts(), sessionsUsageBytes = readSessionsSliceUsageOrNull()
|
|
931
|
+
export function writeLimitsOverride(force = false, home = systemdUserHome(), facts = readMemoryFacts(), sessionsUsageBytes = readSessionsSliceUsageOrNull(),
|
|
932
|
+
/** Seats this machine sells — sizes the slice's collective `MemoryLow` (#398 S3). */
|
|
933
|
+
maxSessions = DEFAULT_MAX_SESSIONS) {
|
|
857
934
|
// Unknown usage can be a busy service whose systemd query timed out. Do not
|
|
858
935
|
// replace an existing drop-in with a guessed percentage, or remove a limit
|
|
859
936
|
// by rewriting the file without its MemoryMax line. Defer the whole policy.
|
|
@@ -866,7 +943,7 @@ export function writeLimitsOverride(force = false, home = systemdUserHome(), fac
|
|
|
866
943
|
// them is a machine whose sessions are capped but whose daemon is not
|
|
867
944
|
// prioritised — the regression described on `buildDevbridgeSliceOverride`.
|
|
868
945
|
write(limitsOverridePath(home), buildLimitsOverride(undefined, facts));
|
|
869
|
-
write(sessionsSliceOverridePath(home), buildSessionsSliceOverride(facts, sessionsUsageBytes));
|
|
946
|
+
write(sessionsSliceOverridePath(home), buildSessionsSliceOverride(facts, sessionsUsageBytes, maxSessions));
|
|
870
947
|
write(devbridgeSliceOverridePath(home), buildDevbridgeSliceOverride());
|
|
871
948
|
return true;
|
|
872
949
|
}
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How much memory each live session may use, decided from what is actually free
|
|
3
|
+
* (#398 S3, ticket #398).
|
|
4
|
+
*
|
|
5
|
+
* The number this replaces was `max(pot / 3, 2 GiB)`, computed ONCE at daemon
|
|
6
|
+
* start and never again. The divisor came from `maxSessions` defaulting to three
|
|
7
|
+
* and from nothing else, so a lone session on an idle machine got a third of the
|
|
8
|
+
* pot and the other two thirds sat unused — the owner's complaint in #398, word
|
|
9
|
+
* for word: «ночью машина свободна ~9 ГБ, а сессии всё равно нельзя выйти за
|
|
10
|
+
* 2.5 ГБ».
|
|
11
|
+
*
|
|
12
|
+
* **The decisive idea is three levels, and it has to be understood before any of
|
|
13
|
+
* the arithmetic below.** Today a session has two numbers, the brake and the
|
|
14
|
+
* wall, and both of the jobs — «do not crowd your neighbour» and «do not kill
|
|
15
|
+
* the machine» — are being asked of them at once. That is unsatisfiable: for a
|
|
16
|
+
* lone session to take nearly the whole pot its brake must be nearly the whole
|
|
17
|
+
* pot, and then two such sessions overflow the pot between them without either
|
|
18
|
+
* being braked — and what overflows is the SLICE, where the victim is chosen by
|
|
19
|
+
* the kernel and the one that dies is not the one at fault (measured, #387).
|
|
20
|
+
*
|
|
21
|
+
* So there are three, the way every grown-up scheduler has three:
|
|
22
|
+
*
|
|
23
|
+
* - **the guarantee** (`MemoryLow`) — small and untouchable. The sum of the
|
|
24
|
+
* guarantees has to fit in the pot, and this is the ONLY place a sum is
|
|
25
|
+
* checked. It answers «how much will not be taken from me».
|
|
26
|
+
* - **the brake** (`MemoryHigh`) — large and DELIBERATELY oversold. Every
|
|
27
|
+
* session may grow into everything that is free right now. The sum of the
|
|
28
|
+
* brakes is greater than the pot, and that is not a mistake.
|
|
29
|
+
* - **the collective brake on the slice** (`MemoryHigh` on
|
|
30
|
+
* `devbridge-sessions.slice`, `service-unit.ts`) — the thing that makes the
|
|
31
|
+
* overselling safe. As the sessions together approach the pot the kernel
|
|
32
|
+
* slows all of them and reclaims first from whoever is above their guarantee,
|
|
33
|
+
* so the hard ceiling — where the OOM killer picks — is never reached.
|
|
34
|
+
* - **the wall** (`MemoryMax`) — still one guarantee below the pot, so a lone
|
|
35
|
+
* runaway meets ITS OWN wall before the shared pot overflows (rule of #387).
|
|
36
|
+
*
|
|
37
|
+
* Overselling the brakes WITHOUT the collective brake on the slice is a bug, not
|
|
38
|
+
* a solution. The two halves are one change, and {@link allocateSessionMemory}
|
|
39
|
+
* refuses to oversell when the caller reports that the slice has no live
|
|
40
|
+
* `MemoryHigh` — see {@link AllocatorMachine.collectiveBrake}.
|
|
41
|
+
*/
|
|
42
|
+
/**
|
|
43
|
+
* What one session is guaranteed and will not have taken away.
|
|
44
|
+
*
|
|
45
|
+
* 2 GiB, sized over the 1571 MB peak measured for a workspace `pnpm typecheck`
|
|
46
|
+
* in this monorepo. A dev server that cannot run one of those is not a dev
|
|
47
|
+
* server, so this is the number below which a session is not worth starting.
|
|
48
|
+
*/
|
|
49
|
+
export declare const SESSION_GUARANTEE_BYTES: number;
|
|
50
|
+
/**
|
|
51
|
+
* Growth allowance over what a session already holds.
|
|
52
|
+
*
|
|
53
|
+
* The same 1.25 `service-unit.ts` uses for the ceiling it writes over a cgroup's
|
|
54
|
+
* current usage, and for the same reason: a limit at exactly what is held is a
|
|
55
|
+
* limit that is already breached.
|
|
56
|
+
*/
|
|
57
|
+
export declare const SESSION_HEADROOM_OVER_HOLD = 1.25;
|
|
58
|
+
/**
|
|
59
|
+
* The floor under the BRAKE, over what a session already holds — 1.05, and it
|
|
60
|
+
* is a different number from the one above on purpose (#398 §8, #403).
|
|
61
|
+
*
|
|
62
|
+
* The wall may sit a quarter above the hold: it is a hard limit, and headroom
|
|
63
|
+
* there costs nothing. The brake may not, because it is followed every tick: at
|
|
64
|
+
* 1.25 a session that merely holds its ground has its brake RAISED on every
|
|
65
|
+
* pass, `addMemoryTo` reads that as «the machine found it more memory», and the
|
|
66
|
+
* stall deadline resets for ever — measured in the suite, four tests waiting
|
|
67
|
+
* for a stop that could no longer come.
|
|
68
|
+
*
|
|
69
|
+
* 1.05 is enough for the one thing this floor is for: `memory.current` counts
|
|
70
|
+
* page cache, so a brake at exactly the anonymous hold is already breached.
|
|
71
|
+
*/
|
|
72
|
+
export declare const SESSION_BRAKE_OVER_HOLD = 1.05;
|
|
73
|
+
/**
|
|
74
|
+
* The step the DIVIDED guarantee is rounded down to — 64 MiB (#403).
|
|
75
|
+
*
|
|
76
|
+
* `MemoryLow` is the one number here with no dead zone of its own, on purpose:
|
|
77
|
+
* a guarantee is what a person is promised, and a promise that lags a tick
|
|
78
|
+
* behind the machine is worse than one that follows it. But the divided
|
|
79
|
+
* guarantee is `pool / n`, and the pool follows `MemAvailable`, which breathes:
|
|
80
|
+
* measured on a hungry machine with two sessions, a pot of 3800 MiB and 11 MiB
|
|
81
|
+
* of breathing wrote `MemoryLow` on ten ticks out of ten where the code before
|
|
82
|
+
* these fixes wrote it on one.
|
|
83
|
+
*
|
|
84
|
+
* Rounding down rather than to nearest, because the sum of the guarantees is
|
|
85
|
+
* the one sum in this module that has to fit inside the pot.
|
|
86
|
+
*/
|
|
87
|
+
export declare const GUARANTEE_STEP_BYTES: number;
|
|
88
|
+
/** Below this pot the machine is honestly single-session — see the formula. */
|
|
89
|
+
export declare function hungryPotBytes(baseBand: number): number;
|
|
90
|
+
/** The band this machine uses before any session-specific widening. */
|
|
91
|
+
export declare function baseBandBytes(oomContinue: boolean): number;
|
|
92
|
+
/** One live session, as the allocator needs to see it. */
|
|
93
|
+
export interface AllocatorSession {
|
|
94
|
+
id: string;
|
|
95
|
+
/**
|
|
96
|
+
* `memory.current − (file − shmem)`: what the session holds and cannot simply
|
|
97
|
+
* give back. Page cache is excluded because lowering a limit over clean file
|
|
98
|
+
* pages makes the kernel reclaim them, not kill anything — counting them would
|
|
99
|
+
* inflate every number by whatever the machine happened to have cached.
|
|
100
|
+
*/
|
|
101
|
+
holdBytes: number;
|
|
102
|
+
/** `memory.current`, the whole reading — what `MemAvailable` cannot see. */
|
|
103
|
+
currentBytes: number;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* What the machine's OWNER has said, in `config.toml` (#398 S6).
|
|
107
|
+
*
|
|
108
|
+
* Every field optional, and every absent one means «the shipped default». The
|
|
109
|
+
* knobs exist because this formula travels to dev servers nobody here has ever
|
|
110
|
+
* seen: the two previous memory rules were both reasonable on the author's
|
|
111
|
+
* machine and destructive somewhere else, and an owner who has to get work done
|
|
112
|
+
* tonight needs a way back that is not «downgrade the runner».
|
|
113
|
+
*/
|
|
114
|
+
export interface AllocatorKnobs {
|
|
115
|
+
/** `false` — the pre-0.58.0 fixed third, computed once and never moved. */
|
|
116
|
+
adaptive?: boolean;
|
|
117
|
+
/** The guarantee, bytes. Default {@link SESSION_GUARANTEE_BYTES}. */
|
|
118
|
+
guaranteeBytes?: number;
|
|
119
|
+
/** A ceiling on the ceiling, bytes: no session grows past this whatever is free. */
|
|
120
|
+
sessionMaxBytes?: number;
|
|
121
|
+
}
|
|
122
|
+
/** What the machine says about itself right now. */
|
|
123
|
+
export interface AllocatorMachine {
|
|
124
|
+
/** `MemoryMax` systemd has in force on the sessions slice. Never rewritten live. */
|
|
125
|
+
potBytes: number | null;
|
|
126
|
+
/** `MemAvailable`. */
|
|
127
|
+
availableBytes: number;
|
|
128
|
+
/** `MemTotal`. */
|
|
129
|
+
totalBytes: number;
|
|
130
|
+
/** What the machine keeps for itself — `machineReserveBytes(totalBytes)`. */
|
|
131
|
+
reserveBytes: number;
|
|
132
|
+
/** Does a kill inside a scope cost one process or the whole session? */
|
|
133
|
+
oomContinue: boolean;
|
|
134
|
+
/**
|
|
135
|
+
* Is there a LIVE `MemoryHigh` on the sessions slice, read back off systemd?
|
|
136
|
+
*
|
|
137
|
+
* The hard invariant of this module. Overselling the brakes is safe only while
|
|
138
|
+
* the collective brake exists; without it, overflowing the slice ends in a
|
|
139
|
+
* kill rather than in throttling, and the one killed is the neighbour sitting
|
|
140
|
+
* under its own limit — measured on this host, victim `memory.events`:
|
|
141
|
+
* `high 0, max 0, oom 0, oom_kill 1`.
|
|
142
|
+
*/
|
|
143
|
+
collectiveBrake: boolean;
|
|
144
|
+
/** Swap one session may push into, as `session-cage.ts` computes it. */
|
|
145
|
+
swapBytes: number;
|
|
146
|
+
}
|
|
147
|
+
/** The three numbers of one session, plus the band they were built with. */
|
|
148
|
+
export interface SessionAllocation {
|
|
149
|
+
id: string;
|
|
150
|
+
guaranteedBytes: number;
|
|
151
|
+
brakeBytes: number;
|
|
152
|
+
wallBytes: number;
|
|
153
|
+
swapBytes: number;
|
|
154
|
+
bandBytes: number;
|
|
155
|
+
/** This session's guarantee does not fit in the pot beside the others (D9). */
|
|
156
|
+
tight: boolean;
|
|
157
|
+
}
|
|
158
|
+
export interface Allocation {
|
|
159
|
+
sessions: SessionAllocation[];
|
|
160
|
+
/** The live pot — what is actually available to sessions right now. */
|
|
161
|
+
poolBytes: number;
|
|
162
|
+
/** What the sessions hold between them. */
|
|
163
|
+
heldBytes: number;
|
|
164
|
+
/** What nobody holds. */
|
|
165
|
+
freeBytes: number;
|
|
166
|
+
/** How many guarantees fit in the live pot. */
|
|
167
|
+
guaranteesFit: number;
|
|
168
|
+
/** The conservative formula was used because the slice has no collective brake. */
|
|
169
|
+
conservative: boolean;
|
|
170
|
+
/** The machine is honestly single-session: the pot is below `2G + band`. */
|
|
171
|
+
hungry: boolean;
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* The whole decision, as a pure function of the numbers.
|
|
175
|
+
*
|
|
176
|
+
* Kept pure and kept here, away from every timer and socket, because what it has
|
|
177
|
+
* to be is CORRECT on machines nobody has: the formula travels with the runner
|
|
178
|
+
* to every customer's dev server, and the last two times a memory rule shipped
|
|
179
|
+
* it was reasonable on the author's machine and destructive somewhere else
|
|
180
|
+
* (#387: the victim was the neighbour; `service-unit.ts:314`: a 4 GB machine got
|
|
181
|
+
* `MemoryHigh == MemoryMax`). So the fleet's profiles are fixtures, and the
|
|
182
|
+
* eight properties below are tests rather than comments.
|
|
183
|
+
*/
|
|
184
|
+
export declare function allocateSessionMemory(machine: AllocatorMachine, sessions: AllocatorSession[], knobs?: AllocatorKnobs): Allocation;
|
|
185
|
+
/**
|
|
186
|
+
* What was last written to one session's scope, and how long it has been asking
|
|
187
|
+
* to shrink.
|
|
188
|
+
*
|
|
189
|
+
* The asymmetry below is not caution, it is a measurement: `MemoryMax` lowered
|
|
190
|
+
* below what a process holds ends it in seconds (verified on this host — a scope
|
|
191
|
+
* holding 200 MiB, `MemoryMax` set to 64 MiB, the process read «Killed» and the
|
|
192
|
+
* unit went `inactive`). An oversold limit is safe; a killed honest session is
|
|
193
|
+
* not recoverable.
|
|
194
|
+
*/
|
|
195
|
+
export interface WrittenLimits {
|
|
196
|
+
brakeBytes: number;
|
|
197
|
+
wallBytes: number;
|
|
198
|
+
guaranteedBytes: number;
|
|
199
|
+
/**
|
|
200
|
+
* `MemorySwapMax`, which moves only when the MACHINE changes under us.
|
|
201
|
+
*
|
|
202
|
+
* It is here because of a live example: on 09.09.2026 a swap file appeared on
|
|
203
|
+
* vmi3219930 and every session on it went on being told `MemorySwapMax=0`,
|
|
204
|
+
* because the cage measures the machine once at daemon start. Without swap the
|
|
205
|
+
* brake stops instead of slowing, so that machine kept the exact failure this
|
|
206
|
+
* whole plan is about while the means to avoid it sat unused on its disk.
|
|
207
|
+
*/
|
|
208
|
+
swapBytes: number;
|
|
209
|
+
/** Consecutive ticks on which the session's own numbers asked to come down. */
|
|
210
|
+
shrinkTicks: number;
|
|
211
|
+
/**
|
|
212
|
+
* What the session held when these numbers were written.
|
|
213
|
+
*
|
|
214
|
+
* The difference between «the session shrank» and «a neighbour arrived», and
|
|
215
|
+
* without it there was no difference at all: the wall came down whenever the
|
|
216
|
+
* formula wanted less, whatever the reason — which the design and the comment
|
|
217
|
+
* beside it both forbid. Found by the independent review of 10.09.2026.
|
|
218
|
+
*/
|
|
219
|
+
holdBytes: number;
|
|
220
|
+
}
|
|
221
|
+
/** Ticks in a row a session must ask to shrink before its wall comes down. */
|
|
222
|
+
export declare const SHRINK_TICKS_TO_LOWER = 4;
|
|
223
|
+
/**
|
|
224
|
+
* Neither number moves for less than this — `max(256 MiB, 10 %)`.
|
|
225
|
+
*
|
|
226
|
+
* Every write is a `systemctl` call, and the bus was measured at 2.7 s under
|
|
227
|
+
* load. A dead zone is what keeps a pot that breathes by a few megabytes from
|
|
228
|
+
* costing a bus call per session per tick.
|
|
229
|
+
*/
|
|
230
|
+
export declare function deadZoneBytes(value: number): number;
|
|
231
|
+
export interface LimitMove {
|
|
232
|
+
brakeBytes: number;
|
|
233
|
+
wallBytes: number;
|
|
234
|
+
guaranteedBytes: number;
|
|
235
|
+
swapBytes: number;
|
|
236
|
+
holdBytes: number;
|
|
237
|
+
shrinkTicks: number;
|
|
238
|
+
/** Properties to write, empty when nothing moved enough to be worth a call. */
|
|
239
|
+
properties: string[];
|
|
240
|
+
/** The brake came DOWN — the stall detector has to be muted for a while. */
|
|
241
|
+
lowered: boolean;
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* What to actually write, given what is already in force.
|
|
245
|
+
*
|
|
246
|
+
* Up is free and immediate: memory nobody is using costs nobody anything.
|
|
247
|
+
* Down is where every rule lives.
|
|
248
|
+
*/
|
|
249
|
+
export declare function planLimitMove(written: WrittenLimits | null, wanted: SessionAllocation, holdBytes: number,
|
|
250
|
+
/**
|
|
251
|
+
* Does this systemd take `MemoryLow=` at all? (#398 S3)
|
|
252
|
+
*
|
|
253
|
+
* False costs the machine its guarantees and nothing else. It has to be
|
|
254
|
+
* honoured HERE and not only at spawn, because every property goes to systemd
|
|
255
|
+
* in ONE `set-property` call: a single directive this systemd refuses would
|
|
256
|
+
* take the brake and the wall down with it, every tick, for every session.
|
|
257
|
+
*/
|
|
258
|
+
memoryLow?: boolean): LimitMove;
|
|
259
|
+
//# sourceMappingURL=session-allocator.d.ts.map
|