mirror of
https://github.com/wavetermdev/waveterm
synced 2026-05-24 09:18:27 +00:00
working on flush, bug fixes, tests
This commit is contained in:
parent
1695ec46b1
commit
4e54b8a9e1
4 changed files with 110 additions and 40 deletions
|
|
@ -20,7 +20,10 @@ const DefaultPartDataSize = 64 * 1024
|
||||||
const DefaultFlushTime = 5 * time.Second
|
const DefaultFlushTime = 5 * time.Second
|
||||||
const NoPartIdx = -1
|
const NoPartIdx = -1
|
||||||
|
|
||||||
|
// for unit tests
|
||||||
var warningCount = &atomic.Int32{}
|
var warningCount = &atomic.Int32{}
|
||||||
|
var flushErrorCount = &atomic.Int32{}
|
||||||
|
|
||||||
var partDataSize int64 = DefaultPartDataSize // overridden in tests
|
var partDataSize int64 = DefaultPartDataSize // overridden in tests
|
||||||
var stopFlush = &atomic.Bool{}
|
var stopFlush = &atomic.Bool{}
|
||||||
|
|
||||||
|
|
@ -445,8 +448,6 @@ func (s *BlockStore) WriteAt(ctx context.Context, blockId string, name string, o
|
||||||
// returns (offset, data, error)
|
// returns (offset, data, error)
|
||||||
// we return the offset because the offset may have been adjusted if the size was too big (for circular files)
|
// we return the offset because the offset may have been adjusted if the size was too big (for circular files)
|
||||||
func (s *BlockStore) ReadAt(ctx context.Context, blockId string, name string, offset int64, size int64) (int64, []byte, error) {
|
func (s *BlockStore) ReadAt(ctx context.Context, blockId string, name string, offset int64, size int64) (int64, []byte, error) {
|
||||||
s.pinCacheEntry(blockId, name)
|
|
||||||
defer s.unpinCacheEntry(blockId, name)
|
|
||||||
file, err := s.Stat(ctx, blockId, name)
|
file, err := s.Stat(ctx, blockId, name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, nil, fmt.Errorf("error getting file: %v", err)
|
return 0, nil, fmt.Errorf("error getting file: %v", err)
|
||||||
|
|
@ -459,18 +460,20 @@ func (s *BlockStore) ReadAt(ctx context.Context, blockId string, name string, of
|
||||||
offset += sizeTooBig
|
offset += sizeTooBig
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
partMap := file.computePartMap(offset, size)
|
||||||
var partsNeeded []int
|
var partsNeeded []int
|
||||||
lastPartOffset := (offset + size) % partDataSize
|
for partIdx := range partMap {
|
||||||
endOffsetOfLastPart := offset + size - lastPartOffset + partDataSize
|
partsNeeded = append(partsNeeded, partIdx)
|
||||||
for i := offset; i < endOffsetOfLastPart; i += partDataSize {
|
|
||||||
partsNeeded = append(partsNeeded, file.partIdxAtOffset(i))
|
|
||||||
}
|
}
|
||||||
dataEntries, err := dbGetFileParts(ctx, blockId, name, partsNeeded)
|
dataEntries, err := dbGetFileParts(ctx, blockId, name, partsNeeded)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, nil, fmt.Errorf("error loading data parts: %v", err)
|
return 0, nil, fmt.Errorf("error loading data parts: %v", err)
|
||||||
}
|
}
|
||||||
// wash the entries through the cache
|
// wash the entries through the cache
|
||||||
err = s.withLockExists(blockId, name, func(entry *CacheEntry) error {
|
s.withLock(blockId, name, false, func(entry *CacheEntry) {
|
||||||
|
if entry == nil || entry.FileEntry == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
if offset+size > entry.FileEntry.File.Size {
|
if offset+size > entry.FileEntry.File.Size {
|
||||||
// limit read to the actual size of the file
|
// limit read to the actual size of the file
|
||||||
size = entry.FileEntry.File.Size - offset
|
size = entry.FileEntry.File.Size - offset
|
||||||
|
|
@ -481,11 +484,7 @@ func (s *BlockStore) ReadAt(ctx context.Context, blockId string, name string, of
|
||||||
}
|
}
|
||||||
dataEntries[partIdx] = entry.DataEntries[partIdx]
|
dataEntries[partIdx] = entry.DataEntries[partIdx]
|
||||||
}
|
}
|
||||||
return nil
|
|
||||||
})
|
})
|
||||||
if err != nil {
|
|
||||||
return 0, nil, fmt.Errorf("error reconciling cache entries: %v", err)
|
|
||||||
}
|
|
||||||
// combine the entries into a single byte slice
|
// combine the entries into a single byte slice
|
||||||
// note that we only want part of the first and last part depending on offset and size
|
// note that we only want part of the first and last part depending on offset and size
|
||||||
var rtn []byte
|
var rtn []byte
|
||||||
|
|
@ -532,19 +531,68 @@ func (s *BlockStore) getDirtyCacheKeys() []cacheKey {
|
||||||
return dirtyCacheKeys
|
return dirtyCacheKeys
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *BlockStore) flushFile(ctx context.Context, blockId string, name string) error {
|
func (s *BlockStore) flushFile(ctx context.Context, blockId string, name string) (rtnErr error) {
|
||||||
// todo
|
fileEntry, dataEntries := s.getDirtyDataEntriesForFlush(blockId, name)
|
||||||
|
if fileEntry == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
// clear flushing flags (always)
|
||||||
|
// clear dirty flags if no error
|
||||||
|
// no lock required, note that we unset dirty before flushing
|
||||||
|
if rtnErr == nil {
|
||||||
|
fileEntry.Dirty.Store(false)
|
||||||
|
}
|
||||||
|
fileEntry.Flushing.Store(false)
|
||||||
|
for _, dataEntry := range dataEntries {
|
||||||
|
if rtnErr == nil {
|
||||||
|
dataEntry.Dirty.Store(false)
|
||||||
|
}
|
||||||
|
dataEntry.Flushing.Store(false)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
rtnErr = dbWriteCacheEntry(ctx, fileEntry, dataEntries)
|
||||||
|
if rtnErr != nil {
|
||||||
|
rtnErr = fmt.Errorf("error writing cache entry: %v", rtnErr)
|
||||||
|
return rtnErr
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *BlockStore) incrementFlushErrors(blockId string, name string) int {
|
||||||
|
var rtn int
|
||||||
|
s.withLock(blockId, name, false, func(entry *CacheEntry) {
|
||||||
|
entry.FlushErrors++
|
||||||
|
rtn = entry.FlushErrors
|
||||||
|
})
|
||||||
|
return rtn
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *BlockStore) deleteCacheEntry(blockId string, name string) {
|
||||||
|
s.Lock.Lock()
|
||||||
|
defer s.Lock.Unlock()
|
||||||
|
delete(s.Cache, cacheKey{BlockId: blockId, Name: name})
|
||||||
|
}
|
||||||
|
|
||||||
func (s *BlockStore) FlushCache(ctx context.Context) error {
|
func (s *BlockStore) FlushCache(ctx context.Context) error {
|
||||||
// get a copy of dirty keys so we can iterate without the lock
|
// get a copy of dirty keys so we can iterate without the lock
|
||||||
dirtyCacheKeys := s.getDirtyCacheKeys()
|
dirtyCacheKeys := s.getDirtyCacheKeys()
|
||||||
for _, key := range dirtyCacheKeys {
|
for _, key := range dirtyCacheKeys {
|
||||||
err := s.flushFile(ctx, key.BlockId, key.Name)
|
err := s.flushFile(ctx, key.BlockId, key.Name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
if ctx.Err() != nil {
|
||||||
|
// context error is transient
|
||||||
|
return fmt.Errorf("context error: %v", ctx.Err())
|
||||||
|
}
|
||||||
// if error is not transient, we should probably delete the offending entry :/
|
// if error is not transient, we should probably delete the offending entry :/
|
||||||
log.Printf("error flushing file %s/%s: %v", key.BlockId, key.Name, err)
|
log.Printf("error flushing file %s/%s: %v", key.BlockId, key.Name, err)
|
||||||
|
flushErrorCount.Add(1)
|
||||||
|
totalErrors := s.incrementFlushErrors(key.BlockId, key.Name)
|
||||||
|
if totalErrors >= 3 {
|
||||||
|
s.deleteCacheEntry(key.BlockId, key.Name)
|
||||||
|
log.Printf("too many errors flushing file %s/%s, clear entry", key.BlockId, key.Name)
|
||||||
|
|
||||||
|
}
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
s.cleanCacheEntry(key.BlockId, key.Name)
|
s.cleanCacheEntry(key.BlockId, key.Name)
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,6 @@ package blockstore
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
@ -61,6 +60,7 @@ type CacheEntry struct {
|
||||||
WriteIntentions map[int]WriteIntention // map from intentionid -> WriteIntention
|
WriteIntentions map[int]WriteIntention // map from intentionid -> WriteIntention
|
||||||
FileEntry *FileCacheEntry
|
FileEntry *FileCacheEntry
|
||||||
DataEntries map[int]*DataCacheEntry
|
DataEntries map[int]*DataCacheEntry
|
||||||
|
FlushErrors int
|
||||||
}
|
}
|
||||||
|
|
||||||
//lint:ignore U1000 used for testing
|
//lint:ignore U1000 used for testing
|
||||||
|
|
@ -186,6 +186,7 @@ func makeCacheEntry(blockId string, name string) *CacheEntry {
|
||||||
WriteIntentions: make(map[int]WriteIntention),
|
WriteIntentions: make(map[int]WriteIntention),
|
||||||
FileEntry: nil,
|
FileEntry: nil,
|
||||||
DataEntries: make(map[int]*DataCacheEntry),
|
DataEntries: make(map[int]*DataCacheEntry),
|
||||||
|
FlushErrors: 0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -296,16 +297,21 @@ func (e *CacheEntry) modifyFileData(fn func(*BlockFile)) {
|
||||||
fn(&fileEntry.File)
|
fn(&fileEntry.File)
|
||||||
}
|
}
|
||||||
|
|
||||||
// also sets Flushing to true
|
// also sets Flushing to true on fileentry / dataentries
|
||||||
func (s *BlockStore) getDirtyDataEntries(entry *CacheEntry) (*FileCacheEntry, []*DataCacheEntry) {
|
func (s *BlockStore) getDirtyDataEntriesForFlush(blockId string, name string) (*FileCacheEntry, []*DataCacheEntry) {
|
||||||
s.Lock.Lock()
|
s.Lock.Lock()
|
||||||
defer s.Lock.Unlock()
|
defer s.Lock.Unlock()
|
||||||
|
entry := s.Cache[cacheKey{BlockId: blockId, Name: name}]
|
||||||
|
if entry == nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
if entry.Deleted || entry.FileEntry == nil {
|
if entry.Deleted || entry.FileEntry == nil {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
var dirtyData []*DataCacheEntry
|
var dirtyData []*DataCacheEntry
|
||||||
for _, dce := range entry.DataEntries {
|
for _, dce := range entry.DataEntries {
|
||||||
if dce != nil && dce.Dirty.Load() {
|
if dce != nil && dce.Dirty.Load() {
|
||||||
|
dce.Flushing.Store(true)
|
||||||
dirtyData = append(dirtyData, dce)
|
dirtyData = append(dirtyData, dce)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -318,20 +324,6 @@ func (s *BlockStore) getDirtyDataEntries(entry *CacheEntry) (*FileCacheEntry, []
|
||||||
return entry.FileEntry, dirtyData
|
return entry.FileEntry, dirtyData
|
||||||
}
|
}
|
||||||
|
|
||||||
// clean is true if the block was clean (nothing to write)
|
|
||||||
// returns (clean, error)
|
|
||||||
func (s *BlockStore) flushEntry(ctx context.Context, entry *CacheEntry) error {
|
|
||||||
fileEntry, dirtyData := s.getDirtyDataEntries(entry)
|
|
||||||
if fileEntry == nil && len(dirtyData) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
err := dbWriteCacheEntry(ctx, fileEntry, dirtyData)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (entry *CacheEntry) isDataBlockPinned(partIdx int) bool {
|
func (entry *CacheEntry) isDataBlockPinned(partIdx int) bool {
|
||||||
if entry.FileEntry == nil {
|
if entry.FileEntry == nil {
|
||||||
warningCount.Add(1)
|
warningCount.Add(1)
|
||||||
|
|
|
||||||
|
|
@ -61,6 +61,11 @@ func dbGetFileParts(ctx context.Context, blockId string, name string, parts []in
|
||||||
rtn := make(map[int]*DataCacheEntry)
|
rtn := make(map[int]*DataCacheEntry)
|
||||||
for _, d := range data {
|
for _, d := range data {
|
||||||
d.Dirty = &atomic.Bool{}
|
d.Dirty = &atomic.Bool{}
|
||||||
|
if cap(d.Data) != int(partDataSize) {
|
||||||
|
newData := make([]byte, len(d.Data), partDataSize)
|
||||||
|
copy(newData, d.Data)
|
||||||
|
d.Data = newData
|
||||||
|
}
|
||||||
rtn[d.PartIdx] = d
|
rtn[d.PartIdx] = d
|
||||||
}
|
}
|
||||||
return rtn, nil
|
return rtn, nil
|
||||||
|
|
@ -96,16 +101,6 @@ func dbWriteCacheEntry(ctx context.Context, fileEntry *FileCacheEntry, dataEntri
|
||||||
}
|
}
|
||||||
tx.Exec(dataPartQuery, fileEntry.File.BlockId, fileEntry.File.Name, dataEntry.PartIdx, dataEntry.Data)
|
tx.Exec(dataPartQuery, fileEntry.File.BlockId, fileEntry.File.Name, dataEntry.PartIdx, dataEntry.Data)
|
||||||
}
|
}
|
||||||
if tx.Err == nil {
|
|
||||||
// clear dirty flags
|
|
||||||
fileEntry.Dirty.Store(false)
|
|
||||||
for _, dataEntry := range dataEntries {
|
|
||||||
if dataEntry != nil {
|
|
||||||
dataEntry.Dirty.Store(false)
|
|
||||||
dataEntry.Flushing.Store(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -39,6 +39,9 @@ func cleanupDb(t *testing.T) {
|
||||||
if warningCount.Load() > 0 {
|
if warningCount.Load() > 0 {
|
||||||
t.Errorf("warning count: %d", warningCount.Load())
|
t.Errorf("warning count: %d", warningCount.Load())
|
||||||
}
|
}
|
||||||
|
if flushErrorCount.Load() > 0 {
|
||||||
|
t.Errorf("flush error count: %d", flushErrorCount.Load())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCreate(t *testing.T) {
|
func TestCreate(t *testing.T) {
|
||||||
|
|
@ -502,3 +505,35 @@ func TestComputePartMap(t *testing.T) {
|
||||||
m = file.computePartMap(2005, 1105)
|
m = file.computePartMap(2005, 1105)
|
||||||
testIntMapsEq(t, "map9", m, map[int]int{0: 100, 1: 10, 2: 100, 3: 100, 4: 100, 5: 100, 6: 100, 7: 100, 8: 100, 9: 100})
|
testIntMapsEq(t, "map9", m, map[int]int{0: 100, 1: 10, 2: 100, 3: 100, 4: 100, 5: 100, 6: 100, 7: 100, 8: 100, 9: 100})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestSimpleDBFlush(t *testing.T) {
|
||||||
|
initDb(t)
|
||||||
|
defer cleanupDb(t)
|
||||||
|
|
||||||
|
ctx, cancelFn := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
|
defer cancelFn()
|
||||||
|
blockId := uuid.New().String()
|
||||||
|
fileName := "t1"
|
||||||
|
err := GBS.MakeFile(ctx, blockId, fileName, nil, FileOptsType{})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("error creating file: %v", err)
|
||||||
|
}
|
||||||
|
err = GBS.WriteFile(ctx, blockId, fileName, []byte("hello world!"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("error writing data: %v", err)
|
||||||
|
}
|
||||||
|
checkFileData(t, ctx, blockId, fileName, "hello world!")
|
||||||
|
err = GBS.FlushCache(ctx)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("error flushing cache: %v", err)
|
||||||
|
}
|
||||||
|
if GBS.getCacheSize() != 0 {
|
||||||
|
t.Errorf("cache size mismatch")
|
||||||
|
}
|
||||||
|
checkFileData(t, ctx, blockId, fileName, "hello world!")
|
||||||
|
if GBS.getCacheSize() != 0 {
|
||||||
|
t.Errorf("cache size mismatch (after read)")
|
||||||
|
}
|
||||||
|
checkFileDataAt(t, ctx, blockId, fileName, 6, "world!")
|
||||||
|
checkFileSize(t, ctx, blockId, fileName, 12)
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue