Skip to content

Commit 8d09b23

Browse files
authored
fix(mountsync): prioritize writeback during full pulls (#352)
* fix(mountsync): prioritize writeback during full pulls * test(mountsync): harden full-pull concurrency
1 parent 2e687eb commit 8d09b23

4 files changed

Lines changed: 1278 additions & 65 deletions

File tree

cmd/relayfile-cli/main.go

Lines changed: 103 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)