{"record":{"id":"6999ebd0b9ce98d5","repo":"wavetermdev/waveterm","slug":"flush-already-in-progress","errorCode":null,"errorMessage":"flush already in progress","messagePattern":"flush already in progress","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"pkg/filestore/blockstore.go","lineNumber":399,"sourceCode":"// returns (offset, data, error)\nfunc (s *FileStore) ReadFile(ctx context.Context, zoneId string, name string) (rtnOffset int64, rtnData []byte, rtnErr error) {\n\twithLock(s, zoneId, name, func(entry *CacheEntry) error {\n\t\trtnOffset, rtnData, rtnErr = entry.readAt(ctx, 0, 0, true)\n\t\treturn nil\n\t})\n\treturn\n}\n\ntype FlushStats struct {\n\tFlushDuration   time.Duration\n\tNumDirtyEntries int\n\tNumCommitted    int\n}\n\nfunc (s *FileStore) FlushCache(ctx context.Context) (stats FlushStats, rtnErr error) {\n\twasFlushing := s.setUnlessFlushing()\n\tif wasFlushing {\n\t\treturn stats, fmt.Errorf(\"flush already in progress\")\n\t}\n\tdefer s.setIsFlushing(false)\n\tstartTime := time.Now()\n\tdefer func() {\n\t\tstats.FlushDuration = time.Since(startTime)\n\t}()\n\n\t// get a copy of dirty keys so we can iterate without the lock\n\tdirtyCacheKeys := s.getDirtyCacheKeys()\n\tstats.NumDirtyEntries = len(dirtyCacheKeys)\n\tfor _, key := range dirtyCacheKeys {\n\t\terr := withLock(s, key.ZoneId, key.Name, func(entry *CacheEntry) error {\n\t\t\treturn entry.flushToDB(ctx, false)\n\t\t})\n\t\tif ctx.Err() != nil {\n\t\t\t// transient error (also must stop the loop)\n\t\t\treturn stats, ctx.Err()\n\t\t}","sourceCodeStart":381,"sourceCodeEnd":417,"githubUrl":"https://github.com/wavetermdev/waveterm/blob/a4447c1563b2df285ab89e76c82f91e1a1a49c1e/pkg/filestore/blockstore.go#L381-L417","documentation":"FlushCache is single-flight per FileStore: an atomic setUnlessFlushing guard ensures only one flush runs at a time. Calling FlushCache while a previous flush (e.g. started by the background runFlushWithNewContext loop) is still in progress returns this error immediately instead of queueing a second flush.","triggerScenarios":"Calling FlushCache from application code while the internal flush loop (runFlushWithNewContext) or another goroutine is already flushing; calling FlushCache concurrently from multiple request handlers.","commonSituations":"Shutdown hooks calling FlushCache at the same time as the periodic flusher; load tests hammering FlushCache concurrently; a long flush (many dirty entries) overlapping a manual flush.","solutions":["Treat this error as benign: skip or retry after a short delay, since a flush is already running.","Coordinate with the internal flush loop — don't call FlushCache manually if runFlushWithNewContext is enabled.","Serialize flush triggers through a single goroutine/channel in your app."],"exampleFix":"// before\nif err := fs.FlushCache(ctx); err != nil { return err }\n// after\nif _, err := fs.FlushCache(ctx); err != nil && !strings.Contains(err.Error(), \"flush already in progress\") {\n    return err\n}","handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"stats, err := fs.FlushCache(ctx)\nif err != nil && strings.Contains(err.Error(), \"flush already in progress\") {\n    return nil // benign: a flush is already running\n} else if err != nil {\n    return err\n}","preventionTips":["Don't call FlushCache manually when the internal flush loop is active.","Route all manual flush requests through one goroutine/channel.","Treat this error as informational, not fatal."],"tags":["filestore","concurrency","flush"],"backgroundTag":"operation-already-in-progress","analyzedSha":"a4447c1563b2df285ab89e76c82f91e1a1a49c1e","analyzedAt":"2026-09-01T15:26:23.972Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-08T10:18:20.063Z"}