@@ -11632,39 +11632,85 @@ func runMountLoop(rootCtx context.Context, syncer *mountsync.Syncer, localDir, w
1163211632 const degradedNoticeInterval = time .Minute
1163311633 degradedAttempts := 0
1163411634 var nextDegradedAttempt time.Time
11635+ var statusMu sync.Mutex
1163511636 const degradedStallReason = "delegated relayfile credentials expired or revoked — re-bootstrap relayfile credentials with agent-relay cloud login"
1163611637
1163711638 enterDegraded := func () {
11639+ statusMu .Lock ()
11640+ changed := false
1163811641 if ! degraded {
1163911642 degraded = true
1164011643 stallReason = degradedStallReason
1164111644 lastDegradedNotice = time.Time {}
1164211645 nextDegradedAttempt = time.Time {}
11646+ changed = true
11647+ }
11648+ statusMu .Unlock ()
11649+ if changed {
1164311650 log .Printf ("mount entering read-only degraded state: %s" , degradedStallReason )
1164411651 }
1164511652 }
1164611653 exitDegraded := func () {
11654+ statusMu .Lock ()
11655+ changed := false
1164711656 if degraded {
1164811657 degraded = false
1164911658 stallReason = ""
1165011659 lastDegradedNotice = time.Time {}
1165111660 degradedAttempts = 0
1165211661 nextDegradedAttempt = time.Time {}
11662+ changed = true
11663+ }
11664+ statusMu .Unlock ()
11665+ if changed {
1165311666 log .Printf ("mount exiting degraded state; delegated relayfile credentials restored" )
1165411667 }
1165511668 }
1165611669 maybePrintRecovery := func () {
11670+ statusMu .Lock ()
1165711671 if ! degraded {
11672+ statusMu .Unlock ()
1165811673 return
1165911674 }
1166011675 if time .Since (lastDegradedNotice ) < degradedNoticeInterval {
11676+ statusMu .Unlock ()
1166111677 return
1166211678 }
11663- log .Printf ("mount degraded: %s" , degradedStallReason )
1166411679 lastDegradedNotice = time .Now ()
11680+ statusMu .Unlock ()
11681+ log .Printf ("mount degraded: %s" , degradedStallReason )
11682+ }
11683+ isDegraded := func () bool {
11684+ statusMu .Lock ()
11685+ defer statusMu .Unlock ()
11686+ return degraded
11687+ }
11688+ setStallReason := func (reason string ) {
11689+ statusMu .Lock ()
11690+ stallReason = reason
11691+ statusMu .Unlock ()
11692+ }
11693+ markCycleSuccess := func () {
11694+ statusMu .Lock ()
11695+ stallReason = ""
11696+ lastSuccess = time .Now ()
11697+ statusMu .Unlock ()
11698+ }
11699+ lastSuccessAt := func () time.Time {
11700+ statusMu .Lock ()
11701+ defer statusMu .Unlock ()
11702+ return lastSuccess
11703+ }
11704+ currentStallReason := func () string {
11705+ statusMu .Lock ()
11706+ defer statusMu .Unlock ()
11707+ return stallReason
1166511708 }
1166611709
11710+ var authMu sync.Mutex
1166711711 refreshMountAuth := func (force bool ) error {
11712+ authMu .Lock ()
11713+ defer authMu .Unlock ()
1166811714 if httpClient == nil {
1166911715 return nil
1167011716 }
@@ -11735,6 +11781,7 @@ func runMountLoop(rootCtx context.Context, syncer *mountsync.Syncer, localDir, w
1173511781 snapshotMu .Lock ()
1173611782 needFetch := ! hasCachedStatus || time .Since (lastSnapshotFetchAt ) >= syncStatusMinPollInterval
1173711783 cached := lastSnapshotStatus
11784+ hadCachedStatus := hasCachedStatus
1173811785 snapshotMu .Unlock ()
1173911786
1174011787 status := cached
@@ -11748,7 +11795,7 @@ func runMountLoop(rootCtx context.Context, syncer *mountsync.Syncer, localDir, w
1174811795 // Fetch failed; fall back to cached snapshot if we have one,
1174911796 // otherwise skip this write — we'd rather have a stale
1175011797 // snapshot than spin retrying on every file event.
11751- if ! hasCachedStatus {
11798+ if ! hadCachedStatus {
1175211799 return
1175311800 }
1175411801 } else {
@@ -11760,14 +11807,18 @@ func runMountLoop(rootCtx context.Context, syncer *mountsync.Syncer, localDir, w
1176011807 status = fetched
1176111808 }
1176211809 }
11763- _ = writeMirrorStateFile (localDir , buildSyncStateSnapshot (status , workspaceID , defaultMountMode , interval , localDir , readDaemonPID (localDir ), stallReason ))
11810+ _ = writeMirrorStateFile (localDir , buildSyncStateSnapshot (status , workspaceID , defaultMountMode , interval , localDir , readDaemonPID (localDir ), currentStallReason () ))
1176411811 }
1176511812
1176611813 runCycle := func (reconcile bool ) error {
11767- if degraded {
11814+ statusMu .Lock ()
11815+ currentlyDegraded := degraded
11816+ degradedRetryAt := nextDegradedAttempt
11817+ statusMu .Unlock ()
11818+ if currentlyDegraded {
1176811819 // Exponential backoff: skip recovery attempts until nextDegradedAttempt.
1176911820 // This prevents hammering the auth endpoint on consecutive cycles.
11770- if ! nextDegradedAttempt .IsZero () && time .Now ().Before (nextDegradedAttempt ) {
11821+ if ! degradedRetryAt .IsZero () && time .Now ().Before (degradedRetryAt ) {
1177111822 maybePrintRecovery ()
1177211823 writeSnapshot ()
1177311824 return errors .New (degradedStallReason )
@@ -11777,6 +11828,7 @@ func runMountLoop(rootCtx context.Context, syncer *mountsync.Syncer, localDir, w
1177711828 // Compute backoff for next attempt: base * 2^attempts, capped.
1177811829 // Cap the exponent at 14 to prevent int64 overflow on 30s<<uint(n)
1177911830 // after ~28 consecutive failures (would shift to negative).
11831+ statusMu .Lock ()
1178011832 exp := degradedAttempts
1178111833 if exp > 14 {
1178211834 exp = 14
@@ -11787,6 +11839,7 @@ func runMountLoop(rootCtx context.Context, syncer *mountsync.Syncer, localDir, w
1178711839 }
1178811840 degradedAttempts ++
1178911841 nextDegradedAttempt = time .Now ().Add (backoff )
11842+ statusMu .Unlock ()
1179011843 if isMountCredentialExpired (err ) {
1179111844 maybePrintRecovery ()
1179211845 writeSnapshot ()
@@ -11818,60 +11871,67 @@ func runMountLoop(rootCtx context.Context, syncer *mountsync.Syncer, localDir, w
1181811871 // only for that expected timeout case.
1181911872 if errors .Is (err , context .DeadlineExceeded ) {
1182011873 if bs := readBootstrapStatus (localDir ); bs != nil {
11821- stallReason = ""
11874+ setStallReason ( "" )
1182211875 log .Printf ("mount bootstrapping: %d/%d files (in progress)" , bs .FilesSynced , bs .FilesTotal )
1182311876 writeSnapshot ()
1182411877 return err
1182511878 }
1182611879 }
11827- stallReason = err .Error ()
11880+ setStallReason ( err .Error () )
1182811881 log .Printf ("mount sync cycle failed: %v" , err )
1182911882 writeSnapshot ()
1183011883 return err
1183111884 }
11832- stallReason = ""
11833- lastSuccess = time .Now ()
11885+ markCycleSuccess ()
1183411886 log .Printf ("mount sync cycle completed" )
1183511887 writeSnapshot ()
1183611888 return nil
1183711889 }
1183811890
11839- log .Print (mountStartBanner (localDir , interval , intervalJitter ))
11840- initialErr := runCycle (true )
11841- logStuckEventSummary (syncer , initialErr )
11842- if once {
11843- return initialErr
11844- }
11845-
11846- watcher , err := mountsync .NewFileWatcher (localDir , func (relativePath string , op fsnotify.Op ) {
11847- if degraded {
11848- // Local edit observed while delegated credentials are unusable.
11849- // Leave the dirty state in place so the next successful cycle
11850- // after re-login picks it up; do not attempt to push now.
11851- maybePrintRecovery ()
11852- return
11853- }
11854- if err := withAuthRefresh (func (ctx context.Context ) error {
11855- return syncer .HandleLocalChange (ctx , relativePath , op )
11856- }); err != nil {
11857- if isMountCredentialExpired (err ) {
11858- enterDegraded ()
11891+ // The watcher must be accepting local edits before the initial bootstrap
11892+ // starts. A large full-tree export can run for minutes; starting the
11893+ // watcher afterward creates a blind window where a writeback draft is on
11894+ // disk but never reaches /fs/bulk until teardown. Full-pull I/O yields the
11895+ // Syncer's state lock to this callback, and unchanged down-mirror echoes are
11896+ // filtered by HandleLocalChange's tracked hash check.
11897+ if ! once {
11898+ watcher , err := mountsync .NewFileWatcher (localDir , func (relativePath string , op fsnotify.Op ) {
11899+ if isDegraded () {
11900+ // Local edit observed while delegated credentials are unusable.
11901+ // Leave the dirty state in place so the next successful cycle
11902+ // after re-login picks it up; do not attempt to push now.
1185911903 maybePrintRecovery ()
11860- writeSnapshot ()
1186111904 return
1186211905 }
11863- log .Printf ("mount local change failed: %v" , err )
11906+ if err := withAuthRefresh (func (ctx context.Context ) error {
11907+ return syncer .HandleLocalChange (ctx , relativePath , op )
11908+ }); err != nil {
11909+ if isMountCredentialExpired (err ) {
11910+ enterDegraded ()
11911+ maybePrintRecovery ()
11912+ writeSnapshot ()
11913+ return
11914+ }
11915+ log .Printf ("mount local change failed: %v" , err )
11916+ }
11917+ writeSnapshot ()
11918+ })
11919+ if err != nil {
11920+ return fmt .Errorf ("create file watcher: %w" , err )
1186411921 }
11865- writeSnapshot ()
11866- })
11867- if err != nil {
11868- return fmt .Errorf ("create file watcher: %w" , err )
11922+ if err := watcher .Start (rootCtx ); err != nil {
11923+ _ = watcher .Close ()
11924+ return fmt .Errorf ("start file watcher: %w" , err )
11925+ }
11926+ defer watcher .Close ()
1186911927 }
11870- if err := watcher .Start (rootCtx ); err != nil {
11871- _ = watcher .Close ()
11872- return fmt .Errorf ("start file watcher: %w" , err )
11928+
11929+ log .Print (mountStartBanner (localDir , interval , intervalJitter ))
11930+ initialErr := runCycle (true )
11931+ logStuckEventSummary (syncer , initialErr )
11932+ if once {
11933+ return initialErr
1187311934 }
11874- defer watcher .Close ()
1187511935
1187611936 timer := time .NewTimer (jitteredIntervalWithSample (interval , intervalJitter , mathrand .Float64 ()))
1187711937 defer timer .Stop ()
@@ -11898,16 +11958,16 @@ func runMountLoop(rootCtx context.Context, syncer *mountsync.Syncer, localDir, w
1189811958 if reconcile {
1189911959 _ = runCycle (true )
1190011960 }
11901- if ! degraded && time .Since (lastSuccess ) >= 10 * time .Minute {
11961+ if ! isDegraded () && time .Since (lastSuccessAt () ) >= 10 * time .Minute {
1190211962 if bs := readBootstrapStatus (localDir ); bs != nil {
1190311963 // Long-running initial mirror is making progress
1190411964 // across cycles — not a stall.
11905- stallReason = ""
11965+ setStallReason ( "" )
1190611966 log .Printf ("mount bootstrapping: %d/%d files (in progress)" , bs .FilesSynced , bs .FilesTotal )
1190711967 writeSnapshot ()
1190811968 } else {
11909- stallReason = "no successful reconcile for 10m"
11910- log .Printf ("mount stalled: %s" , stallReason )
11969+ setStallReason ( "no successful reconcile for 10m" )
11970+ log .Printf ("mount stalled: %s" , currentStallReason () )
1191111971 writeSnapshot ()
1191211972 }
1191311973 }
0 commit comments