{"record":{"id":"1f226edc91d62dce","repo":"benbjohnson/litestream","slug":"read-dirty-page-from-buffer-w","errorCode":null,"errorMessage":"read dirty page from buffer: %w","messagePattern":"read dirty page from buffer: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"vfs.go","lineNumber":1533,"sourceCode":"func (f *VFSFile) ReadAt(p []byte, off int64) (n int, err error) {\n\tf.logger.Debug(\"reading at\", \"off\", off, \"len\", len(p))\n\tpageSize, err := f.pageSizeBytes()\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\n\tpgno := uint32(off/int64(pageSize)) + 1\n\tpageOffset := int(off % int64(pageSize))\n\n\t// Check dirty pages first (takes priority over cache and remote)\n\tf.mu.Lock()\n\tif f.writeEnabled {\n\t\tif bufferOff, ok := f.dirty[pgno]; ok {\n\t\t\t// Read page from buffer file\n\t\t\tdata := make([]byte, pageSize)\n\t\t\tif _, err := f.bufferFile.ReadAt(data, bufferOff); err != nil {\n\t\t\t\tf.mu.Unlock()\n\t\t\t\treturn 0, fmt.Errorf(\"read dirty page from buffer: %w\", err)\n\t\t\t}\n\t\t\tn = copy(p, data[pageOffset:])\n\t\t\tf.mu.Unlock()\n\t\t\tf.logger.Debug(\"dirty page hit\", \"page\", pgno, \"n\", n)\n\n\t\t\t// Update the first page to pretend like we are in journal mode.\n\t\t\tif off == 0 && len(p) >= 28 {\n\t\t\t\tp[18], p[19] = 0x01, 0x01\n\t\t\t\t_, _ = rand.Read(p[24:28])\n\t\t\t}\n\n\t\t\treturn n, nil\n\t\t}\n\t}\n\tf.mu.Unlock()\n\n\t// If hydration complete, read from local file\n\tif f.hydrator != nil && f.hydrator.Complete() {","sourceCodeStart":1515,"sourceCodeEnd":1551,"githubUrl":"https://github.com/benbjohnson/litestream/blob/4ed7a308f6271ebfd2b0a6e4b70b03011a37e4a3/vfs.go#L1515-L1551","documentation":"In the VFS Read path, if the requested page is dirty, its data is read from the local buffer file at the recorded offset. A ReadAt failure there produces \"read dirty page from buffer\" and unlocks the mutex before returning. Like error 593, it signals buffer-file/offset inconsistency or local I/O failure.","triggerScenarios":"A page read (ReadAt on the VFS) hitting a f.dirty entry whose buffer offset is beyond the buffer file's current size, or an underlying file read error (disk full, EBADF, tmpfs eviction).","commonSituations":"Buffer file recreated/compacted without updating f.dirty; local disk errors; running with the buffer on ephemeral storage that was cleared; concurrent code paths mutating f.dirty without holding f.mu.","solutions":["Read the wrapped cause; a short read/EOF means stale dirty offsets — invalidate and re-read from remote.","Re-open or re-hydrate the VFSFile to rebuild the buffer/dirty map.","Ensure all mutations of f.dirty and f.bufferFile happen under f.mu in the same order.","Check local disk health and free space for the buffer file."],"exampleFix":null,"handlingStrategy":"fallback","validationCode":"if bufferOff, ok := f.dirty[pgno]; ok {\n    if st, err := f.bufferFile.Stat(); err != nil || bufferOff+int64(pageSize) > st.Size() {\n        delete(f.dirty, pgno) // fall back to remote fetch below\n    }\n}","typeGuard":null,"tryCatchPattern":"if _, err := f.bufferFile.ReadAt(data, bufferOff); err != nil {\n    f.mu.Unlock()\n    if errors.Is(err, io.EOF) {\n        return f.readFromRemote(pgno, p) // fallback path\n    }\n    return 0, err\n}","preventionTips":["Keep dirty-map and buffer-file lifecycles in lockstep under f.mu.","Surface stale-offset inconsistencies during tests with short buffer files.","Check disk health/free space before starting write-heavy workloads."],"tags":["io","buffer-file","read-path","litestream"],"backgroundTag":"file-read-failed","analyzedSha":"4ed7a308f6271ebfd2b0a6e4b70b03011a37e4a3","analyzedAt":"2026-09-06T18:29:25.564Z","contentChangedAt":"2026-09-06T18:29:25.564Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}