// Copyright 2024 GoEdge CDN goedge.cdn@gmail.com. All rights reserved. Official site: https://goedge.cn . package bfs import ( "errors" fsutils "github.com/TeaOSLab/EdgeNode/internal/utils/fs" "github.com/TeaOSLab/EdgeNode/internal/utils/linkedlist" "github.com/TeaOSLab/EdgeNode/internal/zero" "log" "runtime" "sync" "time" ) func IsEnabled() bool { return runtime.GOARCH == "amd64" || runtime.GOARCH == "arm64" } // FS 文件系统对象 type FS struct { dir string opt *FSOptions bMap map[string]*BlocksFile // name => *BlocksFile bList *linkedlist.List[string] // [bName] bItemMap map[string]*linkedlist.Item[string] closingBMap map[string]zero.Zero // filename => Zero closingBChan chan *BlocksFile mu *sync.RWMutex isClosed bool syncTicker *time.Ticker locker *fsutils.Locker } // OpenFS 打开文件系统 func OpenFS(dir string, options *FSOptions) (*FS, error) { if !IsEnabled() { return nil, errors.New("the fs only works under 64 bit system") } if options == nil { options = DefaultFSOptions } else { options.EnsureDefaults() } var locker = fsutils.NewLocker(dir + "/fs") err := locker.Lock() if err != nil { return nil, err } var fs = &FS{ dir: dir, bMap: map[string]*BlocksFile{}, bList: linkedlist.NewList[string](), bItemMap: map[string]*linkedlist.Item[string]{}, closingBMap: map[string]zero.Zero{}, closingBChan: make(chan *BlocksFile, 32), mu: &sync.RWMutex{}, opt: options, syncTicker: time.NewTicker(1 * time.Second), locker: locker, } go fs.init() return fs, nil } func (this *FS) init() { go func() { // sync in background for range this.syncTicker.C { this.syncLoop() } }() go func() { for { this.processClosingBFiles() } }() } // OpenFileWriter 打开文件写入器 func (this *FS) OpenFileWriter(hash string, bodySize int64, isPartial bool) (*FileWriter, error) { if this.isClosed { return nil, errors.New("the fs closed") } if isPartial && bodySize <= 0 { return nil, errors.New("invalid body size for partial content") } bFile, err := this.openBFileForHashWriting(hash) if err != nil { return nil, err } return bFile.OpenFileWriter(hash, bodySize, isPartial) } // OpenFileReader 打开文件读取器 func (this *FS) OpenFileReader(hash string, isPartial bool) (*FileReader, error) { if this.isClosed { return nil, errors.New("the fs closed") } bFile, err := this.openBFileForHashReading(hash) if err != nil { return nil, err } return bFile.OpenFileReader(hash, isPartial) } func (this *FS) ExistFile(hash string) (bool, error) { if this.isClosed { return false, errors.New("the fs closed") } bFile, err := this.openBFileForHashReading(hash) if err != nil { return false, err } return bFile.ExistFile(hash), nil } func (this *FS) RemoveFile(hash string) error { if this.isClosed { return errors.New("the fs closed") } bFile, err := this.openBFileForHashWriting(hash) if err != nil { return err } return bFile.RemoveFile(hash) } func (this *FS) Close() error { if this.isClosed { return nil } this.isClosed = true close(this.closingBChan) this.syncTicker.Stop() var lastErr error this.mu.Lock() for _, bFile := range this.bMap { err := bFile.Close() if err != nil { lastErr = err } } this.mu.Unlock() err := this.locker.Release() if err != nil { lastErr = err } return lastErr } func (this *FS) TestBMap() map[string]*BlocksFile { return this.bMap } func (this *FS) TestBList() *linkedlist.List[string] { return this.bList } func (this *FS) bPathForHash(hash string) (path string, bName string, err error) { err = CheckHashErr(hash) if err != nil { return "", "", err } return this.dir + "/" + hash[:2] + "/" + hash[2:4] + BFileExt, hash[:4], nil } func (this *FS) syncLoop() { if this.isClosed { return } if this.opt.SyncTimeout <= 0 { return } var maxSyncFiles = this.opt.MaxSyncFiles if maxSyncFiles <= 0 { maxSyncFiles = 32 } var bFiles []*BlocksFile this.mu.RLock() for _, bFile := range this.bMap { if time.Since(bFile.SyncAt()) > this.opt.SyncTimeout { bFiles = append(bFiles, bFile) maxSyncFiles-- if maxSyncFiles <= 0 { break } } } this.mu.RUnlock() for _, bFile := range bFiles { err := bFile.ForceSync() if err != nil { // TODO 可以在options自定义一个logger log.Println("BFS", "sync failed: "+err.Error()) } } } func (this *FS) openBFileForHashWriting(hash string) (*BlocksFile, error) { err := CheckHashErr(hash) if err != nil { return nil, err } bPath, bName, err := this.bPathForHash(hash) if err != nil { return nil, err } this.mu.RLock() bFile, ok := this.bMap[bName] this.mu.RUnlock() if ok { // 调整当前BFile所在位置 this.mu.Lock() item, itemOk := this.bItemMap[bName] if itemOk { this.bList.Remove(item) this.bList.Push(item) } this.mu.Unlock() return bFile, nil } return this.openBFile(bPath, bName) } func (this *FS) openBFileForHashReading(hash string) (*BlocksFile, error) { err := CheckHashErr(hash) if err != nil { return nil, err } bPath, bName, err := this.bPathForHash(hash) if err != nil { return nil, err } this.mu.RLock() bFile, ok := this.bMap[bName] this.mu.RUnlock() if ok { // 调整当前BFile所在位置 this.mu.Lock() item, itemOk := this.bItemMap[bName] if itemOk { this.bList.Remove(item) this.bList.Push(item) } this.mu.Unlock() return bFile, nil } return this.openBFile(bPath, bName) } func (this *FS) openBFile(bPath string, bName string) (*BlocksFile, error) { // check closing queue this.mu.RLock() _, isClosing := this.closingBMap[bPath] this.mu.RUnlock() if isClosing { var maxWaits = 30_000 for { this.mu.RLock() _, isClosing = this.closingBMap[bPath] this.mu.RUnlock() if !isClosing { break } time.Sleep(1 * time.Millisecond) maxWaits-- if maxWaits < 0 { return nil, errors.New("open blocks file timeout") } } } this.mu.Lock() defer this.mu.Unlock() // lookup again bFile, ok := this.bMap[bName] if ok { return bFile, nil } bFile, err := OpenBlocksFile(bPath, &BlockFileOptions{ BytesPerSync: this.opt.BytesPerSync, }) if err != nil { return nil, err } this.bMap[bName] = bFile // 加入到列表中 var item = linkedlist.NewItem(bName) this.bList.Push(item) this.bItemMap[bName] = item // 检查是否超出maxOpenFiles if this.bList.Len() > this.opt.MaxOpenFiles { var firstBName = this.bList.Shift().Value var firstBFile = this.bMap[firstBName] delete(this.bMap, firstBName) delete(this.bItemMap, firstBName) this.closingBMap[firstBFile.Filename()] = zero.Zero{} // MUST run in goroutine go func() { // 因为 closingBChan 可能已经关闭 defer func() { recover() }() this.closingBChan <- firstBFile }() } return bFile, nil } // 处理关闭中的 BFile 们 func (this *FS) processClosingBFiles() { if this.isClosed { return } var bFile = <-this.closingBChan if bFile == nil { return } _ = bFile.Close() this.mu.Lock() delete(this.closingBMap, bFile.Filename()) this.mu.Unlock() }