@@ -20,6 +20,15 @@ const STABLE_RUN_MS = config.fluxapps.crashBackoffStableRunMs ?? 10 * 60 * 1000;
2020// only the count (capped by the ladder) and the last timestamp are ever read,
2121// so the persisted history never needs to grow beyond the ladder length
2222const MAX_HISTORY = BACKOFF_DELAYS_MS . length ;
23+ // The exit code cannot prove a fault: an image whose entrypoint is a wrapper
24+ // script ending in `exit 0` reports a clean stop for a segfault, and no init we
25+ // wrap around it can recover a status the image already discarded. So pacing on
26+ // the code alone would leave such a container restarting without limit. This is
27+ // the cause-blind backstop - this many automatic restarts inside the window is
28+ // itself evidence of a fault, whatever Docker reported, and it disposes into the
29+ // same ladder rather than into a state a human has to clear.
30+ const RESTART_BURST_COUNT = config . fluxapps . restartBurstCount ?? 5 ;
31+ const RESTART_BURST_WINDOW_MS = config . fluxapps . restartBurstWindowMs ?? 60 * 1000 ;
2332
2433function collection ( ) {
2534 const db = dbHelper . databaseConnection ( ) ;
@@ -93,7 +102,10 @@ async function setOperatorStopped(identifier, stopped) {
93102 // app. Swallowing a write failure would let the API report success while the
94103 // lock never persisted - the caller must surface the failure instead.
95104 const fields = { operatorStopped : stopped } ;
96- if ( ! stopped ) fields . restartHistory = [ ] ;
105+ if ( ! stopped ) {
106+ fields . restartHistory = [ ] ;
107+ fields . autoRestartWindow = [ ] ;
108+ }
97109 await setFields ( identifier , fields ) ;
98110}
99111
@@ -110,20 +122,39 @@ async function isOperatorStopped(identifier) {
110122}
111123
112124/**
113- * Appends a restart attempt (wall-clock) and trims the history to the ladder
114- * length so a perpetually crashing container never grows the array unbounded.
125+ * Whether the automatic restarts already recorded fill the burst window. Read
126+ * BEFORE the current attempt is appended, so it answers "have there already
127+ * been enough" and the caller's own restart is the one over the line.
128+ *
129+ * @param {object|null } state
130+ * @returns {boolean }
131+ */
132+ function burstExceeded ( state ) {
133+ const recent = ( state && state . autoRestartWindow ) || [ ] ;
134+ if ( recent . length < RESTART_BURST_COUNT ) return false ;
135+ return Date . now ( ) - recent [ 0 ] <= RESTART_BURST_WINDOW_MS ;
136+ }
137+
138+ /**
139+ * Appends a restart attempt (wall-clock). Every automatic restart lands in the
140+ * burst window; only one with crash evidence - or one that fills the burst
141+ * window, which is the same conclusion reached without the exit code - also
142+ * walks the ladder. Both arrays are trimmed to their own bound so a
143+ * perpetually restarting container never grows the document unbounded.
115144 *
116145 * @param {string } identifier
146+ * @param {boolean } crashed - Docker reported a fault (non-zero exit or OOM kill)
117147 */
118- async function recordRestart ( identifier ) {
148+ async function recordRestart ( identifier , crashed = true ) {
119149 try {
120150 const state = await getState ( identifier ) ;
121- const history = ( state && state . restartHistory ) || [ ] ;
122- history . push ( Date . now ( ) ) ;
123- if ( history . length > MAX_HISTORY ) {
124- history . splice ( 0 , history . length - MAX_HISTORY ) ;
151+ const fields = {
152+ autoRestartWindow : [ ...( ( state && state . autoRestartWindow ) || [ ] ) , Date . now ( ) ] . slice ( - RESTART_BURST_COUNT ) ,
153+ } ;
154+ if ( crashed || burstExceeded ( state ) ) {
155+ fields . restartHistory = [ ...( ( state && state . restartHistory ) || [ ] ) , Date . now ( ) ] . slice ( - MAX_HISTORY ) ;
125156 }
126- await setFields ( identifier , { restartHistory : history } ) ;
157+ await setFields ( identifier , fields ) ;
127158 } catch ( err ) {
128159 log . error ( `appsRuntimeState - failed to record restart for ${ identifier } : ${ err . message } ` ) ;
129160 }
@@ -150,10 +181,16 @@ async function recordRestart(identifier) {
150181 * @param {string } identifier
151182 * @param {number|null } lastFinishedAtMs - docker State.FinishedAt of the
152183 * stopped container (ms epoch), when the caller has inspect data
184+ * @param {boolean } crashed - Docker reported a fault (non-zero exit or OOM kill)
153185 * @returns {Promise<number> }
154186 */
155- async function restartWaitMs ( identifier , lastFinishedAtMs = null ) {
187+ async function restartWaitMs ( identifier , lastFinishedAtMs = null , crashed = true ) {
156188 const state = await getState ( identifier ) ;
189+ // A clean exit is the operator's own restart far more often than it is a
190+ // fault, and pacing it makes a deliberate restart look like an outage. It
191+ // goes back immediately - unless the restarts are arriving fast enough to
192+ // fill the burst window, which is a fault however the exit code reads.
193+ if ( ! crashed && ! burstExceeded ( state ) ) return 0 ;
157194 const history = ( state && state . restartHistory ) || [ ] ;
158195 if ( history . length === 0 ) return 0 ;
159196
@@ -331,6 +368,7 @@ async function prepareCollection() {
331368 networkHealRemoval : twins . some ( ( t ) => t . networkHealRemoval === true ) ,
332369 networkHealHistory : [ ...new Set ( twins . flatMap ( ( t ) => t . networkHealHistory || [ ] ) ) ] . sort ( ( a , b ) => a - b ) . slice ( - MAX_HISTORY ) ,
333370 restartHistory : [ ...new Set ( twins . flatMap ( ( t ) => t . restartHistory || [ ] ) ) ] . sort ( ( a , b ) => a - b ) . slice ( - MAX_HISTORY ) ,
371+ autoRestartWindow : [ ...new Set ( twins . flatMap ( ( t ) => t . autoRestartWindow || [ ] ) ) ] . sort ( ( a , b ) => a - b ) . slice ( - RESTART_BURST_COUNT ) ,
334372 updatedAt : Math . max ( ...twins . map ( ( t ) => t . updatedAt || 0 ) ) ,
335373 } ;
336374 const newestExit = twins . filter ( ( t ) => t . lastDiedAt !== undefined ) . sort ( ( a , b ) => b . lastDiedAt - a . lastDiedAt ) [ 0 ] ;
@@ -368,4 +406,6 @@ module.exports = {
368406 BACKOFF_DELAYS_MS ,
369407 STABLE_RUN_MS ,
370408 MAX_HISTORY ,
409+ RESTART_BURST_COUNT ,
410+ RESTART_BURST_WINDOW_MS ,
371411} ;
0 commit comments