Initial commit (code only without large binaries)

This commit is contained in:
robin
2026-02-15 18:58:44 +08:00
commit 35df75498f
9442 changed files with 1495866 additions and 0 deletions

View File

@@ -0,0 +1,87 @@
# BFS设计
## TODO
* block对指定内容可以压缩比如text/html, text/plain
* block offset信息可以自动合并
* 实现bfs/下的所有TODO
* 系统退出(quit/terminate/kill)时自动关闭文件
* compact的时候同时compact .b和.m两个文件
* 实现对可缓存文件尺寸的限制
* 提前为文件扩展出空间: mFile, bFile
* FileReader可以重用使用完之后放入Pool但要考虑到数据可能已经变更
* 读的时候不允许修改相应区域
* 在compact的时候尤其注意不修改正在Read的区域
* 记录写入和读取速度,然后下次启动的时候根据写入和读取速度调整相关参数
* 可以实现缓存数据加密功能
* 校验mFile可以在文件末尾写入一个特殊标记比如$$$END$$$,下次读取的时候检查此标记是否仍然存在?可能导致写入性能较低
* IMPORTANT
* 实现空余空间重复利用:需要保证此块区域没有正在被读
* 策略单个文件内容写入时先写入最大的Gap写满之后再写入到尾部防止太过零碎
* delete file的时候记录空闲blocksfreeBlocks
* 再次被使用的时候减去空闲blocks
* 实现bFile和mFile的compact、定时器
* bFile和mFile的corruption检测
* 增加End Block
* 增加 openWriter options
* 增加 opnReader options
* 在 MetaFile 中实现 HeaderBlocks和BodyBlocks 合并操作
* 考虑 BlocksFile.Close()中是否要sync()还是简单的close即可这需要corruption检测支持
* fs.BFilesMap分区管理减少锁
* 思考把打开BFile和关闭BFile移出锁
* 完全避免 check status failed: the file closed
* 增加重试功能?
* limiter使用fsutils.Limiter
## 参考文档
* (CockroachDB's Storage Layer)[https://www.cockroachlabs.com/docs/stable/architecture/storage-layer]
## 设计目标
1亿个文件20TiB文件内容。
## 目录结构
~~~
00/
a.b - 文件内容
a.m - 元数据
01/
...
~~~
## 数据结构
文件内容:
~~~
block1, block2, ...
~~~
元数据:
~~~
hash
modifiedAt
expiresAt
status
fileSize
headerSize
bodySize
[header blocks info]
[body blocks info]
~~~
元数据要点:
* 单个文件可以放入512个文件
block info:
~~~
[fromOffset1, toOffset1 ], [fromOffset2, toOffset2 ], ...
~~~

View File

@@ -0,0 +1,15 @@
// Copyright 2024 GoEdge CDN goedge.cdn@gmail.com. All rights reserved. Official site: https://goedge.cn .
package bfs
type BlockInfo struct {
OriginOffsetFrom int64 `json:"1,omitempty"`
OriginOffsetTo int64 `json:"2,omitempty"`
BFileOffsetFrom int64 `json:"3,omitempty"`
BFileOffsetTo int64 `json:"4,omitempty"`
}
func (this BlockInfo) Contains(offset int64) bool {
return this.OriginOffsetFrom <= offset && this.OriginOffsetTo > /** MUST be gt, NOT gte **/ offset
}

View File

@@ -0,0 +1,403 @@
// Copyright 2024 GoEdge CDN goedge.cdn@gmail.com. All rights reserved. Official site: https://goedge.cn .
package bfs
import (
"errors"
"fmt"
"github.com/TeaOSLab/EdgeNode/internal/utils/zero"
"io"
"os"
"path/filepath"
"strings"
"sync"
"sync/atomic"
"time"
)
const BFileExt = ".b"
type BlockType string
const (
BlockTypeHeader BlockType = "header"
BlockTypeBody BlockType = "body"
)
type BlocksFile struct {
opt *BlockFileOptions
fp *os.File
mFile *MetaFile
isClosing bool
isClosed bool
mu *sync.RWMutex
writtenBytes int64
writingFileMap map[string]zero.Zero // hash => Zero
syncAt time.Time
readerPool chan *FileReader
countRefs int32
}
func NewBlocksFileWithRawFile(fp *os.File, options *BlockFileOptions) (*BlocksFile, error) {
options.EnsureDefaults()
var bFilename = fp.Name()
if !strings.HasSuffix(bFilename, BFileExt) {
return nil, errors.New("filename '" + bFilename + "' must has a '" + BFileExt + "' extension")
}
var mu = &sync.RWMutex{}
var mFilename = strings.TrimSuffix(bFilename, BFileExt) + MFileExt
mFile, err := OpenMetaFile(mFilename, mu)
if err != nil {
_ = fp.Close()
return nil, fmt.Errorf("load '%s' failed: %w", mFilename, err)
}
AckReadThread()
_, err = fp.Seek(0, io.SeekEnd)
ReleaseReadThread()
if err != nil {
_ = fp.Close()
_ = mFile.Close()
return nil, err
}
return &BlocksFile{
fp: fp,
mFile: mFile,
mu: mu,
opt: options,
syncAt: time.Now(),
readerPool: make(chan *FileReader, 32),
writingFileMap: map[string]zero.Zero{},
}, nil
}
func OpenBlocksFile(filename string, options *BlockFileOptions) (*BlocksFile, error) {
// TODO 考虑是否使用flock锁定防止多进程写冲突
fp, err := os.OpenFile(filename, os.O_CREATE|os.O_WRONLY, 0666)
if err != nil {
if os.IsNotExist(err) {
var dir = filepath.Dir(filename)
_ = os.MkdirAll(dir, 0777)
// try again
fp, err = os.OpenFile(filename, os.O_CREATE|os.O_WRONLY, 0666)
}
if err != nil {
return nil, fmt.Errorf("open blocks file failed: %w", err)
}
}
return NewBlocksFileWithRawFile(fp, options)
}
func (this *BlocksFile) Filename() string {
return this.fp.Name()
}
func (this *BlocksFile) Write(hash string, blockType BlockType, b []byte, originOffset int64) (n int, err error) {
if len(b) == 0 {
return
}
this.mu.Lock()
defer this.mu.Unlock()
posBefore, err := this.currentPos()
if err != nil {
return 0, err
}
err = this.checkStatus()
if err != nil {
return
}
AckWriteThread()
n, err = this.fp.Write(b)
ReleaseWriteThread()
if err == nil {
if n > 0 {
this.writtenBytes += int64(n)
}
if blockType == BlockTypeHeader {
err = this.mFile.WriteHeaderBlockUnsafe(hash, posBefore, posBefore+int64(n))
} else if blockType == BlockTypeBody {
err = this.mFile.WriteBodyBlockUnsafe(hash, posBefore, posBefore+int64(n), originOffset, originOffset+int64(n))
} else {
err = errors.New("invalid block type '" + string(blockType) + "'")
}
}
return
}
func (this *BlocksFile) OpenFileWriter(fileHash string, bodySize int64, isPartial bool) (writer *FileWriter, err error) {
err = CheckHashErr(fileHash)
if err != nil {
return nil, err
}
this.mu.Lock()
defer this.mu.Unlock()
_, isWriting := this.writingFileMap[fileHash]
if isWriting {
err = ErrFileIsWriting
return
}
this.writingFileMap[fileHash] = zero.Zero{}
err = this.checkStatus()
if err != nil {
return
}
return NewFileWriter(this, fileHash, bodySize, isPartial)
}
func (this *BlocksFile) OpenFileReader(fileHash string, isPartial bool) (*FileReader, error) {
err := CheckHashErr(fileHash)
if err != nil {
return nil, err
}
this.mu.RLock()
err = this.checkStatus()
this.mu.RUnlock()
if err != nil {
return nil, err
}
// 是否存在
header, ok := this.mFile.CloneFileHeader(fileHash)
if !ok {
return nil, os.ErrNotExist
}
// TODO 对于partial content需要传入ranges用来判断是否有交集
if header.IsWriting {
return nil, ErrFileIsWriting
}
if !isPartial && !header.IsCompleted {
return nil, os.ErrNotExist
}
// 先尝试从Pool中获取
select {
case reader := <-this.readerPool:
if reader == nil {
return nil, ErrClosed
}
reader.Reset(header)
atomic.AddInt32(&this.countRefs, 1)
return reader, nil
default:
}
AckReadThread()
fp, err := os.Open(this.fp.Name())
ReleaseReadThread()
if err != nil {
return nil, err
}
atomic.AddInt32(&this.countRefs, 1)
return NewFileReader(this, fp, header), nil
}
func (this *BlocksFile) CloseFileReader(reader *FileReader) error {
defer atomic.AddInt32(&this.countRefs, -1)
select {
case this.readerPool <- reader:
return nil
default:
return reader.Free()
}
}
func (this *BlocksFile) ExistFile(fileHash string) bool {
err := CheckHashErr(fileHash)
if err != nil {
return false
}
return this.mFile.ExistFile(fileHash)
}
func (this *BlocksFile) RemoveFile(fileHash string) error {
err := CheckHashErr(fileHash)
if err != nil {
return err
}
return this.mFile.RemoveFile(fileHash)
}
func (this *BlocksFile) Sync() error {
this.mu.Lock()
defer this.mu.Unlock()
err := this.checkStatus()
if err != nil {
return err
}
return this.sync(false)
}
func (this *BlocksFile) ForceSync() error {
this.mu.Lock()
defer this.mu.Unlock()
err := this.checkStatus()
if err != nil {
return err
}
return this.sync(true)
}
func (this *BlocksFile) SyncAt() time.Time {
return this.syncAt
}
func (this *BlocksFile) Compact() error {
// TODO 需要实现
return nil
}
func (this *BlocksFile) RemoveAll() error {
this.mu.Lock()
defer this.mu.Unlock()
this.isClosed = true
_ = this.mFile.RemoveAll()
this.closeReaderPool()
_ = this.fp.Close()
return os.Remove(this.fp.Name())
}
// CanClose 检查是否可以关闭
func (this *BlocksFile) CanClose() bool {
this.mu.RLock()
defer this.mu.RUnlock()
if len(this.writingFileMap) > 0 || atomic.LoadInt32(&this.countRefs) > 0 {
return false
}
this.isClosing = true
return true
}
// Close 关闭当前文件
func (this *BlocksFile) Close() error {
this.mu.Lock()
defer this.mu.Unlock()
if this.isClosed {
return nil
}
// TODO 决定是否同步
//_ = this.sync(true)
this.isClosed = true
_ = this.mFile.Close()
this.closeReaderPool()
return this.fp.Close()
}
// IsClosing 判断当前文件是否正在关闭或者已关闭
func (this *BlocksFile) IsClosing() bool {
return this.isClosed || this.isClosing
}
func (this *BlocksFile) IncrRef() {
atomic.AddInt32(&this.countRefs, 1)
}
func (this *BlocksFile) DecrRef() {
atomic.AddInt32(&this.countRefs, -1)
}
func (this *BlocksFile) TestReaderPool() chan *FileReader {
return this.readerPool
}
func (this *BlocksFile) removeWritingFile(hash string) {
this.mu.Lock()
delete(this.writingFileMap, hash)
this.mu.Unlock()
}
func (this *BlocksFile) checkStatus() error {
if this.isClosed || this.isClosing {
return fmt.Errorf("check status failed: %w", ErrClosed)
}
return nil
}
func (this *BlocksFile) currentPos() (int64, error) {
return this.fp.Seek(0, io.SeekCurrent)
}
func (this *BlocksFile) sync(force bool) error {
if !force {
if this.writtenBytes < this.opt.BytesPerSync {
return nil
}
}
if this.writtenBytes > 0 {
AckWriteThread()
err := this.fp.Sync()
ReleaseWriteThread()
if err != nil {
return err
}
}
this.writtenBytes = 0
this.syncAt = time.Now()
if force {
return this.mFile.SyncUnsafe()
}
return nil
}
func (this *BlocksFile) closeReaderPool() {
for {
select {
case reader := <-this.readerPool:
if reader != nil {
_ = reader.Free()
}
default:
return
}
}
}

View File

@@ -0,0 +1,17 @@
// Copyright 2024 GoEdge CDN goedge.cdn@gmail.com. All rights reserved. Official site: https://goedge.cn .
package bfs
type BlockFileOptions struct {
BytesPerSync int64
}
func (this *BlockFileOptions) EnsureDefaults() {
if this.BytesPerSync <= 0 {
this.BytesPerSync = 1 << 20
}
}
var DefaultBlockFileOptions = &BlockFileOptions{
BytesPerSync: 1 << 20,
}

View File

@@ -0,0 +1,86 @@
// Copyright 2024 GoEdge CDN goedge.cdn@gmail.com. All rights reserved. Official site: https://goedge.cn .
package bfs_test
import (
"github.com/TeaOSLab/EdgeNode/internal/utils/bfs"
"github.com/iwind/TeaGo/assert"
"os"
"testing"
)
func TestBlocksFile_CanClose(t *testing.T) {
var a = assert.NewAssertion(t)
bFile, openErr := bfs.OpenBlocksFile("testdata/test.b", bfs.DefaultBlockFileOptions)
if openErr != nil {
if os.IsNotExist(openErr) {
return
}
t.Fatal(openErr)
}
reader, err := bFile.OpenFileReader(bfs.Hash("123456"), false)
if err != nil {
t.Fatal(err)
}
a.IsTrue(!bFile.CanClose())
err = reader.Close()
if err != nil {
t.Fatal(err)
}
// duplicated close
err = reader.Close()
if err != nil {
t.Fatal(err)
}
a.IsTrue(bFile.CanClose())
}
func TestBlocksFile_OpenFileWriter_SameHash(t *testing.T) {
bFile, openErr := bfs.OpenBlocksFile("testdata/test.b", bfs.DefaultBlockFileOptions)
if openErr != nil {
if os.IsNotExist(openErr) {
return
}
t.Fatal(openErr)
}
{
writer, err := bFile.OpenFileWriter(bfs.Hash("123456"), -1, false)
if err != nil {
t.Fatal(err)
}
_ = writer.Close()
}
{
writer, err := bFile.OpenFileWriter(bfs.Hash("123456"), -1, false)
if err != nil {
t.Fatal(err)
}
_ = writer.Close()
}
}
func TestBlocksFile_RemoveAll(t *testing.T) {
bFile, err := bfs.OpenBlocksFile("testdata/test.b", bfs.DefaultBlockFileOptions)
if err != nil {
if os.IsNotExist(err) {
return
}
t.Fatal(err)
}
defer func() {
_ = bFile.Close()
}()
err = bFile.RemoveAll()
if err != nil {
t.Fatal(err)
}
}

View File

@@ -0,0 +1,20 @@
// Copyright 2024 GoEdge CDN goedge.cdn@gmail.com. All rights reserved. Official site: https://goedge.cn .
package bfs
import (
"errors"
"os"
)
var ErrClosed = errors.New("the file closed")
var ErrInvalidHash = errors.New("invalid hash")
var ErrFileIsWriting = errors.New("the file is writing")
func IsWritingErr(err error) bool {
return err != nil && errors.Is(err, ErrFileIsWriting)
}
func IsNotExist(err error) bool {
return err != nil && os.IsNotExist(err)
}

View File

@@ -0,0 +1,203 @@
// Copyright 2024 GoEdge CDN goedge.cdn@gmail.com. All rights reserved. Official site: https://goedge.cn .
package bfs
import (
"encoding/json"
"github.com/TeaOSLab/EdgeNode/internal/utils"
"sort"
)
type FileHeader struct {
Version int `json:"1,omitempty"`
ModifiedAt int64 `json:"2,omitempty"`
ExpiresAt int64 `json:"3,omitempty"`
Status int `json:"4,omitempty"`
HeaderSize int64 `json:"5,omitempty"`
BodySize int64 `json:"6,omitempty"`
ExpiredBodySize int64 `json:"7,omitempty"`
HeaderBlocks []BlockInfo `json:"8,omitempty"`
BodyBlocks []BlockInfo `json:"9,omitempty"`
IsCompleted bool `json:"10,omitempty"`
IsWriting bool `json:"11,omitempty"`
}
func (this *FileHeader) BlockAt(offset int64) (blockInfo BlockInfo, ok bool) {
var l = len(this.BodyBlocks)
if l == 1 {
if this.BodyBlocks[0].Contains(offset) {
return this.BodyBlocks[0], true
}
return
}
sort.Search(l, func(i int) bool {
if this.BodyBlocks[i].Contains(offset) {
blockInfo = this.BodyBlocks[i]
ok = true
return true
}
return this.BodyBlocks[i].OriginOffsetFrom > offset
})
return
}
func (this *FileHeader) MaxOffset() int64 {
var l = len(this.BodyBlocks)
if l > 0 {
return this.BodyBlocks[l-1].OriginOffsetTo
}
return 0
}
// Compact blocks
func (this *FileHeader) Compact() {
this.compactHeader()
this.compactBody()
}
// compact header blocks
func (this *FileHeader) compactHeader() {
var l = len(this.HeaderBlocks)
if l > 1 {
// 合并
var newBlocks []BlockInfo
var newIndex int
for index, currentBlock := range this.HeaderBlocks {
if index == 0 {
newBlocks = append(newBlocks, currentBlock)
newIndex++
continue
}
var lastBlock = newBlocks[newIndex-1]
if currentBlock.OriginOffsetFrom >= lastBlock.OriginOffsetFrom &&
currentBlock.OriginOffsetFrom <= /* MUST gte */ lastBlock.OriginOffsetTo &&
currentBlock.OriginOffsetFrom-lastBlock.OriginOffsetFrom == currentBlock.BFileOffsetFrom-lastBlock.BFileOffsetFrom /* 两侧距离一致 */ {
if currentBlock.OriginOffsetTo > lastBlock.OriginOffsetTo {
lastBlock.OriginOffsetTo = currentBlock.OriginOffsetTo
lastBlock.BFileOffsetTo = currentBlock.BFileOffsetTo
newBlocks[newIndex-1] = lastBlock
}
} else {
newBlocks = append(newBlocks, currentBlock)
newIndex++
}
}
this.HeaderBlocks = newBlocks
}
}
// sort and compact body blocks
func (this *FileHeader) compactBody() {
var l = len(this.BodyBlocks)
if l > 0 {
if l > 1 {
// 排序
sort.Slice(this.BodyBlocks, func(i, j int) bool {
var block1 = this.BodyBlocks[i]
var block2 = this.BodyBlocks[j]
if block1.OriginOffsetFrom == block1.OriginOffsetFrom {
return block1.OriginOffsetTo < block2.OriginOffsetTo
}
return block1.OriginOffsetFrom < block2.OriginOffsetFrom
})
// 合并
var newBlocks []BlockInfo
var newIndex int
for index, currentBlock := range this.BodyBlocks {
if index == 0 {
newBlocks = append(newBlocks, currentBlock)
newIndex++
continue
}
var lastBlock = newBlocks[newIndex-1]
if currentBlock.OriginOffsetFrom >= lastBlock.OriginOffsetFrom &&
currentBlock.OriginOffsetFrom <= /* MUST gte */ lastBlock.OriginOffsetTo &&
currentBlock.OriginOffsetFrom-lastBlock.OriginOffsetFrom == currentBlock.BFileOffsetFrom-lastBlock.BFileOffsetFrom /* 两侧距离一致 */ {
if currentBlock.OriginOffsetTo > lastBlock.OriginOffsetTo {
lastBlock.OriginOffsetTo = currentBlock.OriginOffsetTo
lastBlock.BFileOffsetTo = currentBlock.BFileOffsetTo
newBlocks[newIndex-1] = lastBlock
}
} else {
newBlocks = append(newBlocks, currentBlock)
newIndex++
}
}
this.BodyBlocks = newBlocks
l = len(this.BodyBlocks)
}
// 检查是否已完成
var isCompleted = true
if this.BodyBlocks[0].OriginOffsetFrom != 0 || this.BodyBlocks[len(this.BodyBlocks)-1].OriginOffsetTo != this.BodySize {
isCompleted = false
} else {
for index, block := range this.BodyBlocks {
// 是否有不连续的
if index > 0 && block.OriginOffsetFrom > this.BodyBlocks[index-1].OriginOffsetTo {
isCompleted = false
break
}
}
}
this.IsCompleted = isCompleted
}
}
// Clone current header
func (this *FileHeader) Clone() *FileHeader {
return &FileHeader{
Version: this.Version,
ModifiedAt: this.ModifiedAt,
ExpiresAt: this.ExpiresAt,
Status: this.Status,
HeaderSize: this.HeaderSize,
BodySize: this.BodySize,
ExpiredBodySize: this.ExpiredBodySize,
HeaderBlocks: this.HeaderBlocks,
BodyBlocks: this.BodyBlocks,
IsCompleted: this.IsCompleted,
IsWriting: this.IsWriting,
}
}
func (this *FileHeader) Encode(hash string) ([]byte, error) {
headerJSON, err := json.Marshal(this)
if err != nil {
return nil, err
}
// we do not compress data which size is less than 100 bytes
if len(headerJSON) < 100 {
return EncodeMetaBlock(MetaActionNew, hash, append([]byte("json:"), headerJSON...))
}
var buf = utils.SharedBufferPool.Get()
defer utils.SharedBufferPool.Put(buf)
compressor, err := SharedCompressPool.Get(buf)
if err != nil {
return nil, err
}
_, err = compressor.Write(headerJSON)
if err != nil {
_ = compressor.Close()
SharedCompressPool.Put(compressor)
return nil, err
}
err = compressor.Close()
SharedCompressPool.Put(compressor)
if err != nil {
return nil, err
}
return EncodeMetaBlock(MetaActionNew, hash, buf.Bytes())
}

View File

@@ -0,0 +1,67 @@
// Copyright 2024 GoEdge CDN goedge.cdn@gmail.com. All rights reserved. Official site: https://goedge.cn .
package bfs
import (
"bytes"
"encoding/json"
)
// LazyFileHeader load file header lazily to save memory
type LazyFileHeader struct {
rawData []byte
fileHeader *FileHeader
}
func NewLazyFileHeaderFromData(rawData []byte) *LazyFileHeader {
return &LazyFileHeader{
rawData: rawData,
}
}
func NewLazyFileHeader(fileHeader *FileHeader) *LazyFileHeader {
return &LazyFileHeader{
fileHeader: fileHeader,
}
}
func (this *LazyFileHeader) FileHeaderUnsafe() (*FileHeader, error) {
if this.fileHeader != nil {
return this.fileHeader, nil
}
var jsonPrefix = []byte("json:")
var header = &FileHeader{}
// json
if bytes.HasPrefix(this.rawData, jsonPrefix) {
err := json.Unmarshal(this.rawData[len(jsonPrefix):], header)
if err != nil {
return nil, err
}
return header, nil
}
decompressor, err := SharedDecompressPool.Get(bytes.NewBuffer(this.rawData))
if err != nil {
return nil, err
}
defer func() {
_ = decompressor.Close()
SharedDecompressPool.Put(decompressor)
}()
err = json.NewDecoder(decompressor).Decode(header)
if err != nil {
return nil, err
}
header.IsWriting = false
this.fileHeader = header
this.rawData = nil
return header, nil
}

View File

@@ -0,0 +1,87 @@
// Copyright 2024 GoEdge CDN goedge.cdn@gmail.com. All rights reserved. Official site: https://goedge.cn .
package bfs_test
import (
"github.com/TeaOSLab/EdgeNode/internal/utils/bfs"
"runtime"
"testing"
)
func TestNewLazyFileHeaderFromData(t *testing.T) {
var header = &bfs.FileHeader{
Version: 1,
Status: 200,
BodyBlocks: []bfs.BlockInfo{
{
BFileOffsetFrom: 0,
BFileOffsetTo: 1 << 20,
},
},
}
blockBytes, err := header.Encode(bfs.Hash("123456"))
if err != nil {
t.Fatal(err)
}
_, _, rawData, err := bfs.DecodeMetaBlock(blockBytes)
if err != nil {
t.Fatal(err)
}
var lazyHeader = bfs.NewLazyFileHeaderFromData(rawData)
newHeader, err := lazyHeader.FileHeaderUnsafe()
if err != nil {
t.Fatal(err)
}
t.Log(newHeader)
}
func BenchmarkLazyFileHeader_Decode(b *testing.B) {
runtime.GOMAXPROCS(12)
var header = &bfs.FileHeader{
Version: 1,
Status: 200,
BodyBlocks: []bfs.BlockInfo{},
}
var offset int64
for {
var end = offset + 16<<10
if end > 1<<20 {
break
}
header.BodyBlocks = append(header.BodyBlocks, bfs.BlockInfo{
BFileOffsetFrom: offset,
BFileOffsetTo: end,
})
offset = end
}
var hash = bfs.Hash("123456")
blockBytes, err := header.Encode(hash)
if err != nil {
b.Fatal(err)
}
b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
_, _, rawData, decodeErr := bfs.DecodeMetaBlock(blockBytes)
if decodeErr != nil {
b.Fatal(decodeErr)
}
var lazyHeader = bfs.NewLazyFileHeaderFromData(rawData)
_, decodeErr = lazyHeader.FileHeaderUnsafe()
if decodeErr != nil {
b.Fatal(decodeErr)
}
}
})
}

View File

@@ -0,0 +1,432 @@
// Copyright 2024 GoEdge CDN goedge.cdn@gmail.com. All rights reserved. Official site: https://goedge.cn .
package bfs_test
import (
"encoding/json"
"github.com/TeaOSLab/EdgeNode/internal/utils/bfs"
"github.com/TeaOSLab/EdgeNode/internal/utils/fasttime"
"github.com/iwind/TeaGo/assert"
"github.com/iwind/TeaGo/logs"
"math/rand"
"runtime"
"testing"
)
func TestFileHeader_Compact(t *testing.T) {
var a = assert.NewAssertion(t)
{
var header = &bfs.FileHeader{
Version: 1,
Status: 200,
BodySize: 100,
BodyBlocks: []bfs.BlockInfo{
{
OriginOffsetFrom: 0,
OriginOffsetTo: 100,
},
},
}
header.Compact()
a.IsTrue(header.IsCompleted)
}
{
var header = &bfs.FileHeader{
Version: 1,
Status: 200,
BodySize: 200,
BodyBlocks: []bfs.BlockInfo{
{
OriginOffsetFrom: 100,
OriginOffsetTo: 200,
},
{
OriginOffsetFrom: 0,
OriginOffsetTo: 100,
},
},
}
header.Compact()
a.IsTrue(header.IsCompleted)
}
{
var header = &bfs.FileHeader{
Version: 1,
Status: 200,
BodySize: 200,
BodyBlocks: []bfs.BlockInfo{
{
OriginOffsetFrom: 10,
OriginOffsetTo: 99,
},
{
OriginOffsetFrom: 110,
OriginOffsetTo: 200,
},
{
OriginOffsetFrom: 88,
OriginOffsetTo: 120,
},
{
OriginOffsetFrom: 0,
OriginOffsetTo: 100,
},
},
}
header.Compact()
a.IsTrue(header.IsCompleted)
}
{
var header = &bfs.FileHeader{
Version: 1,
Status: 200,
BodySize: 100,
BodyBlocks: []bfs.BlockInfo{
{
OriginOffsetFrom: 10,
OriginOffsetTo: 100,
},
{
OriginOffsetFrom: 100,
OriginOffsetTo: 200,
},
},
}
header.Compact()
a.IsFalse(header.IsCompleted)
}
{
var header = &bfs.FileHeader{
Version: 1,
Status: 200,
BodySize: 200,
BodyBlocks: []bfs.BlockInfo{
{
OriginOffsetFrom: 0,
OriginOffsetTo: 100,
},
{
OriginOffsetFrom: 100,
OriginOffsetTo: 199,
},
},
}
header.Compact()
a.IsFalse(header.IsCompleted)
}
{
var header = &bfs.FileHeader{
Version: 1,
Status: 200,
BodySize: 200,
BodyBlocks: []bfs.BlockInfo{
{
OriginOffsetFrom: 0,
OriginOffsetTo: 100,
},
{
OriginOffsetFrom: 101,
OriginOffsetTo: 200,
},
},
}
header.Compact()
a.IsFalse(header.IsCompleted)
}
}
func TestFileHeader_Compact_Merge(t *testing.T) {
var a = assert.NewAssertion(t)
var header = &bfs.FileHeader{
Version: 1,
Status: 200,
HeaderBlocks: []bfs.BlockInfo{
{
BFileOffsetFrom: 1000,
BFileOffsetTo: 1100,
OriginOffsetFrom: 1200,
OriginOffsetTo: 1300,
},
{
BFileOffsetFrom: 1100,
BFileOffsetTo: 1200,
OriginOffsetFrom: 1300,
OriginOffsetTo: 1400,
},
},
BodyBlocks: []bfs.BlockInfo{
{
BFileOffsetFrom: 0,
BFileOffsetTo: 100,
OriginOffsetFrom: 200,
OriginOffsetTo: 300,
},
{
BFileOffsetFrom: 100,
BFileOffsetTo: 200,
OriginOffsetFrom: 300,
OriginOffsetTo: 400,
},
{
BFileOffsetFrom: 200,
BFileOffsetTo: 300,
OriginOffsetFrom: 400,
OriginOffsetTo: 500,
},
},
}
header.Compact()
logs.PrintAsJSON(header.HeaderBlocks)
logs.PrintAsJSON(header.BodyBlocks)
a.IsTrue(len(header.HeaderBlocks) == 1)
a.IsTrue(len(header.BodyBlocks) == 1)
}
func TestFileHeader_Compact_Merge2(t *testing.T) {
var header = &bfs.FileHeader{
Version: 1,
Status: 200,
BodyBlocks: []bfs.BlockInfo{
{
BFileOffsetFrom: 0,
BFileOffsetTo: 100,
OriginOffsetFrom: 200,
OriginOffsetTo: 300,
},
{
BFileOffsetFrom: 101,
BFileOffsetTo: 200,
OriginOffsetFrom: 301,
OriginOffsetTo: 400,
},
{
BFileOffsetFrom: 200,
BFileOffsetTo: 300,
OriginOffsetFrom: 400,
OriginOffsetTo: 500,
},
},
}
header.Compact()
logs.PrintAsJSON(header.BodyBlocks)
}
func TestFileHeader_Clone(t *testing.T) {
var a = assert.NewAssertion(t)
var header = &bfs.FileHeader{
Version: 1,
Status: 200,
BodyBlocks: []bfs.BlockInfo{
{
BFileOffsetFrom: 0,
BFileOffsetTo: 100,
},
},
}
var clonedHeader = header.Clone()
t.Log("=== cloned header ===")
logs.PrintAsJSON(clonedHeader, t)
a.IsTrue(len(clonedHeader.BodyBlocks) == 1)
header.BodyBlocks = append(header.BodyBlocks, bfs.BlockInfo{
BFileOffsetFrom: 100,
BFileOffsetTo: 200,
})
header.BodyBlocks = append(header.BodyBlocks, bfs.BlockInfo{
BFileOffsetFrom: 300,
BFileOffsetTo: 400,
})
clonedHeader.BodyBlocks[0].OriginOffsetFrom = 100000000
t.Log("=== after changed ===")
logs.PrintAsJSON(clonedHeader, t)
a.IsTrue(len(clonedHeader.BodyBlocks) == 1)
t.Log("=== original header ===")
logs.PrintAsJSON(header, t)
a.IsTrue(header.BodyBlocks[0].OriginOffsetFrom != clonedHeader.BodyBlocks[0].OriginOffsetFrom)
}
func TestFileHeader_Encode(t *testing.T) {
{
var header = &bfs.FileHeader{
Version: 1,
Status: 200,
ModifiedAt: fasttime.Now().Unix(),
ExpiresAt: fasttime.Now().Unix() + 3600,
BodySize: 1 << 20,
HeaderSize: 1 << 10,
BodyBlocks: []bfs.BlockInfo{
{
BFileOffsetFrom: 1 << 10,
BFileOffsetTo: 1 << 20,
},
},
}
data, err := header.Encode(bfs.Hash("123456"))
if err != nil {
t.Fatal(err)
}
jsonBytes, _ := json.Marshal(header)
t.Log(len(header.BodyBlocks), "blocks", len(data), "bytes", "json:", len(jsonBytes), "bytes")
_, _, _, err = bfs.DecodeMetaBlock(data)
if err != nil {
t.Fatal(err)
}
}
{
var header = &bfs.FileHeader{
Version: 1,
Status: 200,
BodyBlocks: []bfs.BlockInfo{},
}
var offset int64
for {
var end = offset + 16<<10
if end > 256<<10 {
break
}
header.BodyBlocks = append(header.BodyBlocks, bfs.BlockInfo{
BFileOffsetFrom: offset,
BFileOffsetTo: end,
})
offset = end
}
data, err := header.Encode(bfs.Hash("123456"))
if err != nil {
t.Fatal(err)
}
jsonBytes, _ := json.Marshal(header)
t.Log(len(header.BodyBlocks), "blocks", len(data), "bytes", "json:", len(jsonBytes), "bytes")
}
{
var header = &bfs.FileHeader{
Version: 1,
Status: 200,
BodyBlocks: []bfs.BlockInfo{},
}
var offset int64
for {
var end = offset + 16<<10
if end > 512<<10 {
break
}
header.BodyBlocks = append(header.BodyBlocks, bfs.BlockInfo{
BFileOffsetFrom: offset,
BFileOffsetTo: end,
})
offset = end
}
data, err := header.Encode(bfs.Hash("123456"))
if err != nil {
t.Fatal(err)
}
jsonBytes, _ := json.Marshal(header)
t.Log(len(header.BodyBlocks), "blocks", len(data), "bytes", "json:", len(jsonBytes), "bytes")
}
{
var header = &bfs.FileHeader{
Version: 1,
Status: 200,
BodyBlocks: []bfs.BlockInfo{},
}
var offset int64
for {
var end = offset + 16<<10
if end > 1<<20 {
break
}
header.BodyBlocks = append(header.BodyBlocks, bfs.BlockInfo{
BFileOffsetFrom: offset,
BFileOffsetTo: end,
})
offset = end
}
data, err := header.Encode(bfs.Hash("123456"))
if err != nil {
t.Fatal(err)
}
jsonBytes, _ := json.Marshal(header)
t.Log(len(header.BodyBlocks), "blocks", len(data), "bytes", "json:", len(jsonBytes), "bytes")
}
}
func BenchmarkFileHeader_Compact(b *testing.B) {
for i := 0; i < b.N; i++ {
var header = &bfs.FileHeader{
Version: 1,
Status: 200,
BodySize: 200,
BodyBlocks: nil,
}
for j := 0; j < 100; j++ {
header.BodyBlocks = append(header.BodyBlocks, bfs.BlockInfo{
OriginOffsetFrom: int64(j * 100),
OriginOffsetTo: int64(j * 200),
BFileOffsetFrom: 0,
BFileOffsetTo: 0,
})
}
header.Compact()
}
}
func BenchmarkFileHeader_Encode(b *testing.B) {
runtime.GOMAXPROCS(12)
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
var header = &bfs.FileHeader{
Version: 1,
Status: 200,
ModifiedAt: rand.Int63(),
BodySize: rand.Int63(),
BodyBlocks: []bfs.BlockInfo{},
}
var offset int64
for {
var end = offset + 16<<10
if end > 2<<20 {
break
}
header.BodyBlocks = append(header.BodyBlocks, bfs.BlockInfo{
BFileOffsetFrom: offset + int64(rand.Int()%1000000),
BFileOffsetTo: end + int64(rand.Int()%1000000),
})
offset = end
}
var hash = bfs.Hash("123456")
_, err := header.Encode(hash)
if err != nil {
b.Fatal(err)
}
}
})
}

View File

@@ -0,0 +1,88 @@
// Copyright 2024 GoEdge CDN goedge.cdn@gmail.com. All rights reserved. Official site: https://goedge.cn .
package bfs
import (
"errors"
"github.com/iwind/TeaGo/types"
"io"
"os"
)
type FileReader struct {
bFile *BlocksFile
fp *os.File
fileHeader *FileHeader
pos int64
isClosed bool
}
func NewFileReader(bFile *BlocksFile, fp *os.File, fileHeader *FileHeader) *FileReader {
return &FileReader{
bFile: bFile,
fp: fp,
fileHeader: fileHeader,
}
}
func (this *FileReader) FileHeader() *FileHeader {
return this.fileHeader
}
func (this *FileReader) Read(b []byte) (n int, err error) {
n, err = this.ReadAt(b, this.pos)
this.pos += int64(n)
return
}
func (this *FileReader) ReadAt(b []byte, offset int64) (n int, err error) {
if offset >= this.fileHeader.MaxOffset() {
err = io.EOF
return
}
blockInfo, ok := this.fileHeader.BlockAt(offset)
if !ok {
err = errors.New("could not find block at '" + types.String(offset) + "'")
return
}
var delta = offset - blockInfo.OriginOffsetFrom
var bFrom = blockInfo.BFileOffsetFrom + delta
var bTo = blockInfo.BFileOffsetTo
if bFrom > bTo {
err = errors.New("invalid block information")
return
}
var bufLen = len(b)
if int64(bufLen) > bTo-bFrom {
bufLen = int(bTo - bFrom)
}
AckReadThread()
n, err = this.fp.ReadAt(b[:bufLen], bFrom)
ReleaseReadThread()
return
}
func (this *FileReader) Reset(fileHeader *FileHeader) {
this.fileHeader = fileHeader
this.pos = 0
}
func (this *FileReader) Close() error {
if this.isClosed {
return nil
}
this.isClosed = true
return this.bFile.CloseFileReader(this)
}
func (this *FileReader) Free() error {
return this.fp.Close()
}

View File

@@ -0,0 +1,237 @@
// Copyright 2024 GoEdge CDN goedge.cdn@gmail.com. All rights reserved. Official site: https://goedge.cn .
package bfs_test
import (
"fmt"
"github.com/TeaOSLab/EdgeNode/internal/utils/bfs"
"io"
"os"
"testing"
"time"
)
func TestFileReader_Read_SmallBuf(t *testing.T) {
bFile, err := bfs.OpenBlocksFile("testdata/test.b", bfs.DefaultBlockFileOptions)
if err != nil {
t.Fatal(err)
}
reader, err := bFile.OpenFileReader(bfs.Hash("123456"), false)
if err != nil {
if os.IsNotExist(err) {
t.Log(err)
return
}
t.Fatal(err)
}
defer func() {
_ = reader.Close()
}()
var buf = make([]byte, 3)
for {
n, readErr := reader.Read(buf)
if n > 0 {
t.Log(string(buf[:n]))
}
if readErr != nil {
if readErr == io.EOF {
break
}
t.Fatal(readErr)
}
}
}
func TestFileReader_Read_LargeBuff(t *testing.T) {
bFile, err := bfs.OpenBlocksFile("testdata/test.b", bfs.DefaultBlockFileOptions)
if err != nil {
if os.IsNotExist(err) {
t.Log(err)
return
}
t.Fatal(err)
}
reader, err := bFile.OpenFileReader(bfs.Hash("123456"), false)
if err != nil {
if os.IsNotExist(err) {
t.Log(err)
return
}
t.Fatal(err)
}
defer func() {
_ = reader.Close()
}()
var buf = make([]byte, 128)
for {
n, readErr := reader.Read(buf)
if n > 0 {
t.Log(string(buf[:n]))
}
if readErr != nil {
if readErr == io.EOF {
break
}
t.Fatal(readErr)
}
}
}
func TestFileReader_Read_LargeFile(t *testing.T) {
bFile, err := bfs.OpenBlocksFile("testdata/test.b", bfs.DefaultBlockFileOptions)
if err != nil {
if os.IsNotExist(err) {
t.Log(err)
return
}
t.Fatal(err)
}
reader, err := bFile.OpenFileReader(bfs.Hash("123456@LARGE"), false)
if err != nil {
if os.IsNotExist(err) {
t.Log(err)
return
}
t.Fatal(err)
}
defer func() {
_ = reader.Close()
}()
var buf = make([]byte, 16<<10)
var totalSize int64
var before = time.Now()
for {
n, readErr := reader.Read(buf)
if n > 0 {
totalSize += int64(n)
}
if readErr != nil {
if readErr == io.EOF {
break
}
t.Fatal(readErr)
}
}
t.Log("totalSize:", totalSize>>20, "MiB", "cost:", fmt.Sprintf("%.4fms", time.Since(before).Seconds()*1000))
}
func TestFileReader_ReadAt(t *testing.T) {
bFile, err := bfs.OpenBlocksFile("testdata/test.b", bfs.DefaultBlockFileOptions)
if err != nil {
if os.IsNotExist(err) {
t.Log(err)
return
}
t.Fatal(err)
}
reader, err := bFile.OpenFileReader(bfs.Hash("123456"), false)
if err != nil {
if os.IsNotExist(err) {
t.Log(err)
return
}
t.Fatal(err)
}
defer func() {
_ = reader.Close()
}()
{
var buf = make([]byte, 3)
n, readErr := reader.ReadAt(buf, 0)
if n > 0 {
t.Log(string(buf[:n]))
}
if readErr != nil && readErr != io.EOF {
t.Fatal(readErr)
}
}
{
var buf = make([]byte, 3)
n, readErr := reader.ReadAt(buf, 3)
if n > 0 {
t.Log(string(buf[:n]))
}
if readErr != nil && readErr != io.EOF {
t.Fatal(readErr)
}
}
{
var buf = make([]byte, 11)
n, readErr := reader.ReadAt(buf, 3)
if n > 0 {
t.Log(string(buf[:n]))
}
if readErr != nil && readErr != io.EOF {
t.Fatal(readErr)
}
}
{
var buf = make([]byte, 3)
n, readErr := reader.ReadAt(buf, 11)
if n > 0 {
t.Log(string(buf[:n]))
}
if readErr != nil && readErr != io.EOF {
t.Fatal(readErr)
}
}
{
var buf = make([]byte, 3)
n, readErr := reader.ReadAt(buf, 1000)
if n > 0 {
t.Log(string(buf[:n]))
} else {
t.Log("EOF")
}
if readErr != nil && readErr != io.EOF {
t.Fatal(readErr)
}
}
}
func TestFileReader_Pool(t *testing.T) {
bFile, openErr := bfs.OpenBlocksFile("testdata/test.b", bfs.DefaultBlockFileOptions)
if openErr != nil {
if os.IsNotExist(openErr) {
t.Log(openErr)
return
}
t.Fatal(openErr)
}
for i := 0; i < 10; i++ {
reader, err := bFile.OpenFileReader(bfs.Hash("123456"), false)
if err != nil {
if os.IsNotExist(err) {
continue
}
t.Fatal(err)
}
go func() {
err = reader.Close()
if err != nil {
t.Log(err)
}
}()
}
time.Sleep(100 * time.Millisecond)
t.Log(len(bFile.TestReaderPool()))
}

View File

@@ -0,0 +1,112 @@
// Copyright 2024 GoEdge CDN goedge.cdn@gmail.com. All rights reserved. Official site: https://goedge.cn .
package bfs
import "errors"
// FileWriter file writer
// not thread-safe
type FileWriter struct {
bFile *BlocksFile
hasMeta bool
hash string
bodySize int64
originOffset int64
realHeaderSize int64
realBodySize int64
isPartial bool
}
func NewFileWriter(bFile *BlocksFile, hash string, bodySize int64, isPartial bool) (*FileWriter, error) {
if isPartial && bodySize <= 0 {
return nil, errors.New("invalid body size for partial content")
}
return &FileWriter{
bFile: bFile,
hash: hash,
bodySize: bodySize,
isPartial: isPartial,
}, nil
}
func (this *FileWriter) WriteMeta(status int, expiresAt int64, expectedFileSize int64) error {
this.hasMeta = true
return this.bFile.mFile.WriteMeta(this.hash, status, expiresAt, expectedFileSize)
}
func (this *FileWriter) WriteHeader(b []byte) (n int, err error) {
if !this.isPartial && !this.hasMeta {
err = errors.New("no meta found")
return
}
n, err = this.bFile.Write(this.hash, BlockTypeHeader, b, -1)
this.realHeaderSize += int64(n)
return
}
func (this *FileWriter) WriteBody(b []byte) (n int, err error) {
if !this.isPartial && !this.hasMeta {
err = errors.New("no meta found")
return
}
n, err = this.bFile.Write(this.hash, BlockTypeBody, b, this.originOffset)
this.originOffset += int64(n)
this.realBodySize += int64(n)
return
}
func (this *FileWriter) WriteBodyAt(b []byte, offset int64) (n int, err error) {
if !this.hasMeta {
err = errors.New("no meta found")
return
}
if !this.isPartial {
err = errors.New("can not write body at specified offset: it is not a partial file")
return
}
// still 'Write()' NOT 'WriteAt()'
this.originOffset = offset
n, err = this.bFile.Write(this.hash, BlockTypeBody, b, offset)
this.originOffset += int64(n)
return
}
func (this *FileWriter) Close() error {
defer func() {
this.bFile.removeWritingFile(this.hash)
}()
if !this.isPartial && !this.hasMeta {
return errors.New("no meta found")
}
if this.isPartial {
if this.originOffset > this.bodySize {
return errors.New("unexpected body size")
}
this.realBodySize = this.bodySize
} else {
if this.bodySize > 0 && this.bodySize != this.realBodySize {
return errors.New("unexpected body size")
}
}
err := this.bFile.mFile.WriteClose(this.hash, this.realHeaderSize, this.realBodySize)
if err != nil {
return err
}
return this.bFile.Sync()
}
func (this *FileWriter) Discard() error {
// TODO 需要测试
return this.bFile.mFile.RemoveFile(this.hash)
}

View File

@@ -0,0 +1,134 @@
// Copyright 2024 GoEdge CDN goedge.cdn@gmail.com. All rights reserved. Official site: https://goedge.cn .
package bfs_test
import (
"bytes"
"github.com/TeaOSLab/EdgeNode/internal/utils/bfs"
"github.com/TeaOSLab/EdgeNode/internal/utils/fasttime"
"github.com/TeaOSLab/EdgeNode/internal/utils/testutils"
"github.com/iwind/TeaGo/logs"
"net/http"
"testing"
"time"
)
func TestNewFileWriter(t *testing.T) {
bFile, err := bfs.OpenBlocksFile("testdata/test.b", bfs.DefaultBlockFileOptions)
if err != nil {
t.Fatal(err)
}
defer func() {
if !testutils.IsSingleTesting() {
_ = bFile.RemoveAll()
} else {
_ = bFile.Close()
}
}()
writer, err := bFile.OpenFileWriter(bfs.Hash("123456"), -1, false)
if err != nil {
t.Fatal(err)
}
err = writer.WriteMeta(http.StatusOK, fasttime.Now().Unix()+3600, -1)
if err != nil {
t.Fatal(err)
}
_, err = writer.WriteHeader([]byte("Content-Type: text/html; charset=utf-8"))
if err != nil {
t.Fatal(err)
}
for i := 0; i < 3; i++ {
n, writeErr := writer.WriteBody([]byte("Hello,World"))
if writeErr != nil {
t.Fatal(writeErr)
}
t.Log("wrote:", n, "bytes")
}
err = writer.Close()
if err != nil {
t.Fatal(err)
}
}
func TestNewFileWriter_LargeFile(t *testing.T) {
bFile, err := bfs.OpenBlocksFile("testdata/test.b", bfs.DefaultBlockFileOptions)
if err != nil {
t.Fatal(err)
}
defer func() {
if !testutils.IsSingleTesting() {
_ = bFile.RemoveAll()
} else {
_ = bFile.Close()
}
}()
writer, err := bFile.OpenFileWriter(bfs.Hash("123456@LARGE"), -1, false)
if err != nil {
t.Fatal(err)
}
err = writer.WriteMeta(http.StatusOK, fasttime.Now().Unix()+86400, -1)
if err != nil {
t.Fatal(err)
}
var countBlocks = 1 << 10
if !testutils.IsSingleTesting() {
countBlocks = 2
}
var data = bytes.Repeat([]byte{'A'}, 16<<10)
var before = time.Now()
for i := 0; i < countBlocks; i++ {
_, err = writer.WriteBody(data)
if err != nil {
t.Fatal(err)
}
}
err = writer.Close()
if err != nil {
t.Fatal(err)
}
logs.Println("cost:", time.Since(before).Seconds()*1000, "ms")
}
func TestFileWriter_WriteBodyAt(t *testing.T) {
bFile, err := bfs.OpenBlocksFile("testdata/test.b", bfs.DefaultBlockFileOptions)
if err != nil {
t.Fatal(err)
}
defer func() {
if !testutils.IsSingleTesting() {
_ = bFile.RemoveAll()
} else {
_ = bFile.Close()
}
}()
writer, err := bFile.OpenFileWriter(bfs.Hash("123456"), 1<<20, true)
if err != nil {
t.Fatal(err)
}
{
n, writeErr := writer.WriteBodyAt([]byte("Hello,World"), 1024)
if writeErr != nil {
t.Fatal(writeErr)
}
t.Log("wrote:", n, "bytes")
}
}

View File

@@ -0,0 +1,442 @@
// 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/goman"
"github.com/TeaOSLab/EdgeNode/internal/utils/linkedlist"
"github.com/TeaOSLab/EdgeNode/internal/utils/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()
if len(this.bMap) > 0 {
var g = goman.NewTaskGroup()
for _, bFile := range this.bMap {
var bFileCopy = bFile
g.Run(func() {
err := bFileCopy.Close()
if err != nil {
lastErr = err
}
})
}
g.Wait()
}
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 {
if bFile.IsClosing() {
continue
}
err := bFile.ForceSync()
if err != nil {
// check again
if bFile.IsClosing() {
continue
}
// 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()
if bFile.IsClosing() {
// TODO 需要重新等待打开
}
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
}
err = this.waitBFile(bPath)
if err != nil {
return nil, err
}
this.mu.Lock()
bFile, ok := this.bMap[bName]
if ok {
// 调整当前BFile所在位置
item, itemOk := this.bItemMap[bName]
if itemOk {
this.bList.Remove(item)
this.bList.Push(item)
}
this.mu.Unlock()
return bFile, nil
}
this.mu.Unlock()
return this.openBFile(bPath, bName)
}
func (this *FS) openBFile(bPath string, bName string) (*BlocksFile, error) {
// check closing queue
err := this.waitBFile(bPath)
if err != nil {
return nil, err
}
this.mu.Lock()
defer this.mu.Unlock()
// lookup again
bFile, ok := this.bMap[bName]
if ok {
return bFile, nil
}
// TODO 不要把 OpenBlocksFile 放入到 mu 中?
bFile, err = OpenBlocksFile(bPath, &BlockFileOptions{
BytesPerSync: this.opt.BytesPerSync,
})
if err != nil {
return nil, err
}
// 防止被关闭
bFile.IncrRef()
defer bFile.DecrRef()
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 {
this.shiftOpenFiles()
}
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()
}
// 弹出超出BFile数量限制的BFile
func (this *FS) shiftOpenFiles() {
var l = this.bList.Len()
var count = l - this.opt.MaxOpenFiles
if count <= 0 {
return
}
var bNames []string
var searchCount int
this.bList.Range(func(item *linkedlist.Item[string]) (goNext bool) {
searchCount++
var bName = item.Value
var bFile = this.bMap[bName]
if bFile.CanClose() {
bNames = append(bNames, bName)
count--
}
return count > 0 && searchCount < 8 && searchCount < l-8
})
for _, bName := range bNames {
var bFile = this.bMap[bName]
var item = this.bItemMap[bName]
// clean
delete(this.bMap, bName)
delete(this.bItemMap, bName)
this.bList.Remove(item)
// add to closing queue
this.closingBMap[bFile.Filename()] = zero.Zero{}
// MUST run in goroutine
go func(bFile *BlocksFile) {
// 因为 closingBChan 可能已经关闭
defer func() {
recover()
}()
this.closingBChan <- bFile
}(bFile)
}
}
func (this *FS) waitBFile(bPath string) error {
this.mu.RLock()
_, isClosing := this.closingBMap[bPath]
this.mu.RUnlock()
if !isClosing {
return nil
}
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 errors.New("open blocks file timeout")
}
}
return nil
}

View File

@@ -0,0 +1,47 @@
// Copyright 2024 GoEdge CDN goedge.cdn@gmail.com. All rights reserved. Official site: https://goedge.cn .
package bfs
import (
fsutils "github.com/TeaOSLab/EdgeNode/internal/utils/fs"
memutils "github.com/TeaOSLab/EdgeNode/internal/utils/mem"
"time"
)
type FSOptions struct {
MaxOpenFiles int
BytesPerSync int64
SyncTimeout time.Duration
MaxSyncFiles int
}
func (this *FSOptions) EnsureDefaults() {
if this.MaxOpenFiles <= 0 {
// 根据内存计算最大打开文件数
var maxOpenFiles = memutils.SystemMemoryGB() * 128
if maxOpenFiles > (8 << 10) {
maxOpenFiles = 8 << 10
}
this.MaxOpenFiles = maxOpenFiles
}
if this.BytesPerSync <= 0 {
if fsutils.DiskIsFast() {
this.BytesPerSync = 1 << 20 // TODO 根据硬盘实际写入速度进行调整
} else {
this.BytesPerSync = 512 << 10
}
}
if this.SyncTimeout <= 0 {
this.SyncTimeout = 1 * time.Second
}
if this.MaxSyncFiles <= 0 {
this.MaxSyncFiles = 32
}
}
var DefaultFSOptions = &FSOptions{
MaxOpenFiles: 1 << 10,
BytesPerSync: 512 << 10,
SyncTimeout: 1 * time.Second,
MaxSyncFiles: 32,
}

View File

@@ -0,0 +1,197 @@
// Copyright 2024 GoEdge CDN goedge.cdn@gmail.com. All rights reserved. Official site: https://goedge.cn .
package bfs_test
import (
"github.com/TeaOSLab/EdgeNode/internal/utils/bfs"
"github.com/TeaOSLab/EdgeNode/internal/utils/fasttime"
"github.com/TeaOSLab/EdgeNode/internal/utils/linkedlist"
"github.com/TeaOSLab/EdgeNode/internal/utils/testutils"
"github.com/iwind/TeaGo/Tea"
_ "github.com/iwind/TeaGo/bootstrap"
"github.com/iwind/TeaGo/logs"
"github.com/iwind/TeaGo/types"
"io"
"os"
"testing"
)
func TestFS_OpenFileWriter(t *testing.T) {
fs, openErr := bfs.OpenFS(Tea.Root+"/data/bfs/test", bfs.DefaultFSOptions)
if openErr != nil {
t.Fatal(openErr)
}
defer func() {
_ = fs.Close()
}()
{
writer, err := fs.OpenFileWriter(bfs.Hash("123456"), -1, false)
if err != nil {
t.Fatal(err)
}
err = writer.WriteMeta(200, fasttime.Now().Unix()+3600, -1)
if err != nil {
t.Fatal(err)
}
_, err = writer.WriteBody([]byte("Hello, World"))
if err != nil {
t.Fatal(err)
}
err = writer.Close()
if err != nil {
t.Fatal(err)
}
}
{
writer, err := fs.OpenFileWriter(bfs.Hash("654321"), 100, true)
if err != nil {
t.Fatal(err)
}
_, err = writer.WriteBody([]byte("Hello, World"))
if err != nil {
t.Fatal(err)
}
}
}
func TestFS_OpenFileReader(t *testing.T) {
fs, openErr := bfs.OpenFS(Tea.Root+"/data/bfs/test", bfs.DefaultFSOptions)
if openErr != nil {
t.Fatal(openErr)
}
defer func() {
_ = fs.Close()
}()
reader, err := fs.OpenFileReader(bfs.Hash("123456"), false)
if err != nil {
if bfs.IsNotExist(err) {
t.Log(err)
return
}
t.Fatal(err)
}
data, err := io.ReadAll(reader)
if err != nil {
t.Fatal(err)
}
t.Log(string(data))
logs.PrintAsJSON(reader.FileHeader(), t)
}
func TestFS_ExistFile(t *testing.T) {
fs, openErr := bfs.OpenFS(Tea.Root+"/data/bfs/test", bfs.DefaultFSOptions)
if openErr != nil {
t.Fatal(openErr)
}
defer func() {
_ = fs.Close()
}()
exist, err := fs.ExistFile(bfs.Hash("123456"))
if err != nil {
t.Fatal(err)
}
t.Log("exist:", exist)
}
func TestFS_RemoveFile(t *testing.T) {
fs, openErr := bfs.OpenFS(Tea.Root+"/data/bfs/test", bfs.DefaultFSOptions)
if openErr != nil {
t.Fatal(openErr)
}
defer func() {
_ = fs.Close()
}()
var hash = bfs.Hash("123456")
err := fs.RemoveFile(hash)
if err != nil {
t.Fatal(err)
}
exist, err := fs.ExistFile(bfs.Hash("123456"))
if err != nil {
t.Fatal(err)
}
t.Log("exist:", exist)
}
func TestFS_OpenFileWriter_Close(t *testing.T) {
if !testutils.IsSingleTesting() {
return
}
fs, openErr := bfs.OpenFS(Tea.Root+"/data/bfs/test", &bfs.FSOptions{
MaxOpenFiles: 99,
})
if openErr != nil {
t.Fatal(openErr)
}
defer func() {
_ = fs.Close()
}()
var count = 2
if testutils.IsSingleTesting() {
count = 100
}
for i := 0; i < count; i++ {
//t.Log("open", i)
writer, err := fs.OpenFileWriter(bfs.Hash(types.String(i)), -1, false)
if err != nil {
t.Fatal(err)
}
_ = writer.Close()
}
t.Log(len(fs.TestBMap()), "block files, pid:", os.Getpid())
var p = func() {
var bNames []string
fs.TestBList().Range(func(item *linkedlist.Item[string]) (goNext bool) {
bNames = append(bNames, item.Value)
return true
})
if len(bNames) != len(fs.TestBMap()) {
t.Fatal("len(bNames)!=len(bMap)")
}
if len(bNames) < 10 {
t.Log("["+types.String(len(bNames))+"]", bNames)
} else {
t.Log("["+types.String(len(bNames))+"]", bNames[:10], "...")
}
}
p()
{
writer, err := fs.OpenFileWriter(bfs.Hash(types.String(10)), -1, false)
if err != nil {
t.Fatal(err)
}
_ = writer.Close()
}
p()
// testing closing
for i := 0; i < 3; i++ {
writer, err := fs.OpenFileWriter(bfs.Hash(types.String(0)), -1, false)
if err != nil {
t.Fatal(err)
}
_ = writer.Close()
}
p()
}

View File

@@ -0,0 +1,66 @@
// Copyright 2024 GoEdge CDN goedge.cdn@gmail.com. All rights reserved. Official site: https://goedge.cn .
package bfs
import (
"github.com/TeaOSLab/EdgeNode/internal/utils/percpu"
"github.com/klauspost/compress/gzip"
"io"
"runtime"
)
var SharedDecompressPool = NewGzipReaderPool()
type GzipReaderPool struct {
c chan *gzip.Reader
cList []chan *gzip.Reader
}
func NewGzipReaderPool() *GzipReaderPool {
const poolSize = 16
var countProcs = runtime.GOMAXPROCS(0)
if countProcs <= 0 {
countProcs = runtime.NumCPU()
}
countProcs *= 4
var cList []chan *gzip.Reader
for i := 0; i < countProcs; i++ {
cList = append(cList, make(chan *gzip.Reader, poolSize))
}
return &GzipReaderPool{
c: make(chan *gzip.Reader, poolSize),
cList: cList,
}
}
func (this *GzipReaderPool) Get(rawReader io.Reader) (*gzip.Reader, error) {
select {
case w := <-this.getC():
err := w.Reset(rawReader)
if err != nil {
return nil, err
}
return w, nil
default:
return gzip.NewReader(rawReader)
}
}
func (this *GzipReaderPool) Put(reader *gzip.Reader) {
select {
case this.getC() <- reader:
default:
// 不需要close因为已经在使用的时候调用了
}
}
func (this *GzipReaderPool) getC() chan *gzip.Reader {
var procId = percpu.GetProcId()
if procId < len(this.cList) {
return this.cList[procId]
}
return this.c
}

View File

@@ -0,0 +1,63 @@
// Copyright 2024 GoEdge CDN goedge.cdn@gmail.com. All rights reserved. Official site: https://goedge.cn .
package bfs
import (
"github.com/TeaOSLab/EdgeNode/internal/utils/percpu"
"github.com/klauspost/compress/gzip"
"io"
"runtime"
)
var SharedCompressPool = NewGzipWriterPool()
type GzipWriterPool struct {
c chan *gzip.Writer
cList []chan *gzip.Writer
}
func NewGzipWriterPool() *GzipWriterPool {
const poolSize = 16
var countProcs = runtime.GOMAXPROCS(0)
if countProcs <= 0 {
countProcs = runtime.NumCPU()
}
countProcs *= 4
var cList []chan *gzip.Writer
for i := 0; i < countProcs; i++ {
cList = append(cList, make(chan *gzip.Writer, poolSize))
}
return &GzipWriterPool{
c: make(chan *gzip.Writer, poolSize),
cList: cList,
}
}
func (this *GzipWriterPool) Get(rawWriter io.Writer) (*gzip.Writer, error) {
select {
case w := <-this.getC():
w.Reset(rawWriter)
return w, nil
default:
return gzip.NewWriterLevel(rawWriter, gzip.BestSpeed)
}
}
func (this *GzipWriterPool) Put(writer *gzip.Writer) {
select {
case this.getC() <- writer:
default:
// 不需要close因为已经在使用的时候调用了
}
}
func (this *GzipWriterPool) getC() chan *gzip.Writer {
var procId = percpu.GetProcId()
if procId < len(this.cList) {
return this.cList[procId]
}
return this.c
}

View File

@@ -0,0 +1,36 @@
// Copyright 2024 GoEdge CDN goedge.cdn@gmail.com. All rights reserved. Official site: https://goedge.cn .
package bfs
import (
"fmt"
stringutil "github.com/iwind/TeaGo/utils/string"
)
var HashLen = 32
// CheckHash check hash string format
func CheckHash(hash string) bool {
if len(hash) != HashLen {
return false
}
for _, b := range hash {
if !((b >= '0' && b <= '9') || (b >= 'a' && b <= 'f')) {
return false
}
}
return true
}
func CheckHashErr(hash string) error {
if CheckHash(hash) {
return nil
}
return fmt.Errorf("check hash '%s' failed: %w", hash, ErrInvalidHash)
}
func Hash(s string) string {
return stringutil.Md5(s)
}

View File

@@ -0,0 +1,27 @@
// Copyright 2024 GoEdge CDN goedge.cdn@gmail.com. All rights reserved. Official site: https://goedge.cn .
package bfs_test
import (
"github.com/TeaOSLab/EdgeNode/internal/utils/bfs"
"github.com/iwind/TeaGo/assert"
"math/rand"
"strconv"
"strings"
"testing"
)
func TestCheckHash(t *testing.T) {
var a = assert.NewAssertion(t)
a.IsFalse(bfs.CheckHash("123456"))
a.IsFalse(bfs.CheckHash(strings.Repeat("A", 32)))
a.IsTrue(bfs.CheckHash(strings.Repeat("a", 32)))
a.IsTrue(bfs.CheckHash(bfs.Hash("123456")))
}
func BenchmarkCheckHashErr(b *testing.B) {
for i := 0; i < b.N; i++ {
_ = bfs.CheckHash(bfs.Hash(strconv.Itoa(rand.Int())))
}
}

View File

@@ -0,0 +1,52 @@
// Copyright 2024 GoEdge CDN goedge.cdn@gmail.com. All rights reserved. Official site: https://goedge.cn .
package bfs
import (
"encoding/binary"
"errors"
)
type MetaAction = byte
const (
MetaActionNew MetaAction = '+'
MetaActionRemove MetaAction = '-'
)
func EncodeMetaBlock(action MetaAction, hash string, data []byte) ([]byte, error) {
var hl = len(hash)
if hl != HashLen {
return nil, errors.New("invalid hash length")
}
var l = 1 /** Action **/ + hl /** Hash **/ + len(data)
var b = make([]byte, 4 /** Len **/ +l)
binary.BigEndian.PutUint32(b, uint32(l))
b[4] = action
copy(b[5:], hash)
copy(b[5+hl:], data)
return b, nil
}
func DecodeMetaBlock(blockBytes []byte) (action MetaAction, hash string, data []byte, err error) {
var dataOffset = 4 /** Len **/ + HashLen + 1 /** Action **/
if len(blockBytes) < dataOffset {
err = errors.New("decode failed: invalid block data")
return
}
action = blockBytes[4]
hash = string(blockBytes[5 : 5+HashLen])
if action == MetaActionNew {
var rawData = blockBytes[dataOffset:]
if len(rawData) > 0 {
data = make([]byte, len(rawData))
copy(data, rawData)
}
}
return
}

View File

@@ -0,0 +1,52 @@
// Copyright 2024 GoEdge CDN goedge.cdn@gmail.com. All rights reserved. Official site: https://goedge.cn .
package bfs_test
import (
"bytes"
"github.com/TeaOSLab/EdgeNode/internal/utils/bfs"
"github.com/iwind/TeaGo/assert"
"testing"
)
func TestMetaBlock(t *testing.T) {
var a = assert.NewAssertion(t)
{
var srcHash = bfs.Hash("a")
b, err := bfs.EncodeMetaBlock(bfs.MetaActionNew, srcHash, []byte{1, 2, 3})
if err != nil {
t.Fatal(err)
}
t.Log(b)
{
action, hash, data, decodeErr := bfs.DecodeMetaBlock(b)
if decodeErr != nil {
t.Fatal(err)
}
a.IsTrue(action == bfs.MetaActionNew)
a.IsTrue(hash == srcHash)
a.IsTrue(bytes.Equal(data, []byte{1, 2, 3}))
}
}
{
var srcHash = bfs.Hash("bcd")
b, err := bfs.EncodeMetaBlock(bfs.MetaActionRemove, srcHash, []byte{1, 2, 3})
if err != nil {
t.Fatal(err)
}
t.Log(b)
{
action, hash, data, decodeErr := bfs.DecodeMetaBlock(b)
if decodeErr != nil {
t.Fatal(err)
}
a.IsTrue(action == bfs.MetaActionRemove)
a.IsTrue(hash == srcHash)
a.IsTrue(len(data) == 0)
}
}
}

View File

@@ -0,0 +1,380 @@
// Copyright 2024 GoEdge CDN goedge.cdn@gmail.com. All rights reserved. Official site: https://goedge.cn .
package bfs
import (
"bytes"
"encoding/binary"
"github.com/TeaOSLab/EdgeNode/internal/utils/fasttime"
"github.com/TeaOSLab/EdgeNode/internal/utils/zero"
"io"
"os"
"sync"
)
const MFileExt = ".m"
const Version1 = 1
type MetaFile struct {
fp *os.File
filename string
headerMap map[string]*LazyFileHeader // hash => *LazyFileHeader
mu *sync.RWMutex // TODO 考虑单独一个不要和bFile共享
isModified bool
modifiedHashMap map[string]zero.Zero // hash => Zero
}
func OpenMetaFile(filename string, mu *sync.RWMutex) (*MetaFile, error) {
fp, err := os.OpenFile(filename, os.O_CREATE|os.O_RDWR, 0666)
if err != nil {
return nil, err
}
var mFile = &MetaFile{
filename: filename,
fp: fp,
headerMap: map[string]*LazyFileHeader{},
mu: mu,
modifiedHashMap: map[string]zero.Zero{},
}
// 从文件中加载已有的文件头信息
err = mFile.load()
if err != nil {
return nil, err
}
return mFile, nil
}
func (this *MetaFile) load() error {
AckReadThread()
_, err := this.fp.Seek(0, io.SeekStart)
ReleaseReadThread()
if err != nil {
return err
}
// TODO 检查文件是否完整
var buf = make([]byte, 4<<10)
var blockBytes []byte
for {
AckReadThread()
n, readErr := this.fp.Read(buf)
ReleaseReadThread()
if n > 0 {
blockBytes = append(blockBytes, buf[:n]...)
for len(blockBytes) > 4 {
var l = int(binary.BigEndian.Uint32(blockBytes[:4])) + 4 /* Len **/
if len(blockBytes) < l {
break
}
action, hash, data, decodeErr := DecodeMetaBlock(blockBytes[:l])
if decodeErr != nil {
return decodeErr
}
switch action {
case MetaActionNew:
this.headerMap[hash] = NewLazyFileHeaderFromData(data)
case MetaActionRemove:
delete(this.headerMap, hash)
}
blockBytes = blockBytes[l:]
}
}
if readErr != nil {
if readErr == io.EOF {
break
}
return readErr
}
}
return nil
}
func (this *MetaFile) WriteMeta(hash string, status int, expiresAt int64, expectedFileSize int64) error {
this.mu.Lock()
defer this.mu.Unlock()
this.headerMap[hash] = NewLazyFileHeader(&FileHeader{
Version: Version1,
ExpiresAt: expiresAt,
Status: status,
ExpiredBodySize: expectedFileSize,
IsWriting: true,
})
this.modifiedHashMap[hash] = zero.Zero{}
return nil
}
func (this *MetaFile) WriteHeaderBlockUnsafe(hash string, bOffsetFrom int64, bOffsetTo int64) error {
lazyHeader, ok := this.headerMap[hash]
if !ok {
return nil
}
header, err := lazyHeader.FileHeaderUnsafe()
if err != nil {
return err
}
// TODO 合并相邻block
header.HeaderBlocks = append(header.HeaderBlocks, BlockInfo{
BFileOffsetFrom: bOffsetFrom,
BFileOffsetTo: bOffsetTo,
})
this.modifiedHashMap[hash] = zero.Zero{}
return nil
}
func (this *MetaFile) WriteBodyBlockUnsafe(hash string, bOffsetFrom int64, bOffsetTo int64, originOffsetFrom int64, originOffsetTo int64) error {
lazyHeader, ok := this.headerMap[hash]
if !ok {
return nil
}
header, err := lazyHeader.FileHeaderUnsafe()
if err != nil {
return err
}
// TODO 合并相邻block
header.BodyBlocks = append(header.BodyBlocks, BlockInfo{
OriginOffsetFrom: originOffsetFrom,
OriginOffsetTo: originOffsetTo,
BFileOffsetFrom: bOffsetFrom,
BFileOffsetTo: bOffsetTo,
})
this.modifiedHashMap[hash] = zero.Zero{}
return nil
}
func (this *MetaFile) WriteClose(hash string, headerSize int64, bodySize int64) error {
// TODO 考虑单个hash多次重复调用的情况
this.mu.Lock()
lazyHeader, ok := this.headerMap[hash]
if !ok {
this.mu.Unlock()
return nil
}
header, err := lazyHeader.FileHeaderUnsafe()
if err != nil {
return err
}
this.mu.Unlock()
// TODO 检查bodySize和expectedBodySize是否一致如果不一致则从headerMap中删除
header.ModifiedAt = fasttime.Now().Unix()
header.HeaderSize = headerSize
header.BodySize = bodySize
header.Compact()
blockBytes, err := header.Encode(hash)
if err != nil {
return err
}
this.mu.Lock()
defer this.mu.Unlock()
AckReadThread()
_, err = this.fp.Seek(0, io.SeekEnd)
ReleaseReadThread()
if err != nil {
return err
}
AckWriteThread()
_, err = this.fp.Write(blockBytes)
ReleaseWriteThread()
this.isModified = true
return err
}
func (this *MetaFile) RemoveFile(hash string) error {
this.mu.Lock()
defer this.mu.Unlock()
_, ok := this.headerMap[hash]
if ok {
delete(this.headerMap, hash)
}
if ok {
blockBytes, err := EncodeMetaBlock(MetaActionRemove, hash, nil)
if err != nil {
return err
}
AckWriteThread()
_, err = this.fp.Write(blockBytes)
ReleaseWriteThread()
if err != nil {
return err
}
this.isModified = true
}
return nil
}
func (this *MetaFile) FileHeader(hash string) (header *FileHeader, ok bool) {
this.mu.RLock()
defer this.mu.RUnlock()
lazyHeader, ok := this.headerMap[hash]
if ok {
var err error
header, err = lazyHeader.FileHeaderUnsafe()
if err != nil {
ok = false
}
}
return
}
func (this *MetaFile) FileHeaderUnsafe(hash string) (header *FileHeader, ok bool) {
lazyHeader, ok := this.headerMap[hash]
if ok {
var err error
header, err = lazyHeader.FileHeaderUnsafe()
if err != nil {
ok = false
}
}
return
}
func (this *MetaFile) CloneFileHeader(hash string) (header *FileHeader, ok bool) {
this.mu.RLock()
defer this.mu.RUnlock()
lazyHeader, ok := this.headerMap[hash]
if !ok {
return
}
var err error
header, err = lazyHeader.FileHeaderUnsafe()
if err != nil {
ok = false
return
}
header = header.Clone()
return
}
func (this *MetaFile) FileHeaders() map[string]*LazyFileHeader {
this.mu.RLock()
defer this.mu.RUnlock()
return this.headerMap
}
func (this *MetaFile) ExistFile(hash string) bool {
this.mu.RLock()
defer this.mu.RUnlock()
_, ok := this.headerMap[hash]
return ok
}
// Compact the meta file
// TODO 考虑自动Compact的时机脏数据比例
func (this *MetaFile) Compact() error {
this.mu.Lock()
defer this.mu.Unlock()
var buf = bytes.NewBuffer(nil)
for hash, lazyHeader := range this.headerMap {
header, err := lazyHeader.FileHeaderUnsafe()
if err != nil {
return err
}
blockBytes, err := header.Encode(hash)
if err != nil {
return err
}
buf.Write(blockBytes)
}
AckWriteThread()
err := this.fp.Truncate(int64(buf.Len()))
ReleaseWriteThread()
if err != nil {
return err
}
AckReadThread()
_, err = this.fp.Seek(0, io.SeekStart)
ReleaseReadThread()
if err != nil {
return err
}
AckWriteThread()
_, err = this.fp.Write(buf.Bytes())
ReleaseWriteThread()
this.isModified = true
return err
}
func (this *MetaFile) SyncUnsafe() error {
if !this.isModified {
return nil
}
AckWriteThread()
err := this.fp.Sync()
ReleaseWriteThread()
if err != nil {
return err
}
for hash := range this.modifiedHashMap {
lazyHeader, ok := this.headerMap[hash]
if ok {
header, decodeErr := lazyHeader.FileHeaderUnsafe()
if decodeErr != nil {
return decodeErr
}
header.IsWriting = false
}
}
this.isModified = false
this.modifiedHashMap = map[string]zero.Zero{}
return nil
}
// Close 关闭当前文件
func (this *MetaFile) Close() error {
return this.fp.Close()
}
// RemoveAll 删除所有数据
func (this *MetaFile) RemoveAll() error {
_ = this.fp.Close()
return os.Remove(this.fp.Name())
}

View File

@@ -0,0 +1,196 @@
// Copyright 2024 GoEdge CDN goedge.cdn@gmail.com. All rights reserved. Official site: https://goedge.cn .
package bfs_test
import (
"github.com/TeaOSLab/EdgeNode/internal/utils/bfs"
"github.com/TeaOSLab/EdgeNode/internal/utils/fasttime"
"github.com/TeaOSLab/EdgeNode/internal/utils/testutils"
"github.com/iwind/TeaGo/logs"
"sync"
"testing"
"time"
)
func TestNewMetaFile(t *testing.T) {
mFile, err := bfs.OpenMetaFile("testdata/test.m", &sync.RWMutex{})
if err != nil {
t.Fatal(err)
}
defer func() {
_ = mFile.Close()
}()
var header, _ = mFile.FileHeader(bfs.Hash("123456"))
logs.PrintAsJSON(header, t)
//logs.PrintAsJSON(mFile.Headers(), t)
}
func TestNewMetaFile_Large(t *testing.T) {
var count = 2
if testutils.IsSingleTesting() {
count = 100
}
var before = time.Now()
for i := 0; i < count; i++ {
mFile, err := bfs.OpenMetaFile("testdata/test2.m", &sync.RWMutex{})
if err != nil {
if bfs.IsNotExist(err) {
continue
}
t.Fatal(err)
}
_ = mFile.Close()
}
var costMs = time.Since(before).Seconds() * 1000
t.Logf("cost: %.2fms, qps: %.2fms/file", costMs, costMs/float64(count))
}
func TestNewMetaFile_Memory(t *testing.T) {
var count = 2
if testutils.IsSingleTesting() {
count = 100
}
var stat1 = testutils.ReadMemoryStat()
var mFiles []*bfs.MetaFile
for i := 0; i < count; i++ {
mFile, err := bfs.OpenMetaFile("testdata/test2.m", &sync.RWMutex{})
if err != nil {
if bfs.IsNotExist(err) {
continue
}
t.Fatal(err)
}
_ = mFile.Close()
mFiles = append(mFiles, mFile)
}
var stat2 = testutils.ReadMemoryStat()
t.Log((stat2.HeapInuse-stat1.HeapInuse)>>20, "MiB")
}
func TestMetaFile_FileHeaders(t *testing.T) {
mFile, openErr := bfs.OpenMetaFile("testdata/test2.m", &sync.RWMutex{})
if openErr != nil {
if bfs.IsNotExist(openErr) {
return
}
t.Fatal(openErr)
}
_ = mFile.Close()
for hash, lazyHeader := range mFile.FileHeaders() {
header, err := lazyHeader.FileHeaderUnsafe()
if err != nil {
t.Fatal(err)
}
t.Log(hash, header.ModifiedAt, header.BodySize)
}
}
func TestMetaFile_WriteMeta(t *testing.T) {
mFile, err := bfs.OpenMetaFile("testdata/test.m", &sync.RWMutex{})
if err != nil {
t.Fatal(err)
}
defer func() {
_ = mFile.Close()
}()
var hash = bfs.Hash("123456")
err = mFile.WriteMeta(hash, 200, fasttime.Now().Unix()+3600, -1)
if err != nil {
t.Fatal(err)
}
err = mFile.WriteHeaderBlockUnsafe(hash, 123, 223)
if err != nil {
t.Fatal(err)
}
err = mFile.WriteBodyBlockUnsafe(hash, 223, 323, 0, 100)
if err != nil {
t.Fatal(err)
}
err = mFile.WriteBodyBlockUnsafe(hash, 323, 423, 100, 200)
if err != nil {
t.Fatal(err)
}
err = mFile.WriteClose(hash, 100, 200)
if err != nil {
t.Fatal(err)
}
//logs.PrintAsJSON(mFile.Header(hash), t)
}
func TestMetaFile_Write(t *testing.T) {
mFile, err := bfs.OpenMetaFile("testdata/test.m", &sync.RWMutex{})
if err != nil {
t.Fatal(err)
}
defer func() {
_ = mFile.Close()
}()
var hash = bfs.Hash("123456")
err = mFile.WriteBodyBlockUnsafe(hash, 0, 100, 0, 100)
if err != nil {
t.Fatal(err)
}
err = mFile.WriteClose(hash, 0, 100)
if err != nil {
t.Fatal(err)
}
}
func TestMetaFile_RemoveFile(t *testing.T) {
mFile, err := bfs.OpenMetaFile("testdata/test.m", &sync.RWMutex{})
if err != nil {
t.Fatal(err)
}
defer func() {
_ = mFile.Close()
}()
err = mFile.RemoveFile(bfs.Hash("123456"))
if err != nil {
t.Fatal(err)
}
}
func TestMetaFile_Compact(t *testing.T) {
mFile, err := bfs.OpenMetaFile("testdata/test.m", &sync.RWMutex{})
if err != nil {
t.Fatal(err)
}
defer func() {
_ = mFile.Close()
}()
err = mFile.Compact()
if err != nil {
t.Fatal(err)
}
}
func TestMetaFile_RemoveAll(t *testing.T) {
mFile, err := bfs.OpenMetaFile("testdata/test.m", &sync.RWMutex{})
if err != nil {
t.Fatal(err)
}
err = mFile.RemoveAll()
if err != nil {
t.Fatal(err)
}
}

View File

@@ -0,0 +1,25 @@
// Copyright 2024 GoEdge CDN goedge.cdn@gmail.com. All rights reserved. Official site: https://goedge.cn .
package bfs
import "github.com/TeaOSLab/EdgeNode/internal/utils/zero"
// TODO 线程数可以根据硬盘数量动态调整?
var readThreadsLimiter = make(chan zero.Zero, 8)
var writeThreadsLimiter = make(chan zero.Zero, 8)
func AckReadThread() {
readThreadsLimiter <- zero.Zero{}
}
func ReleaseReadThread() {
<-readThreadsLimiter
}
func AckWriteThread() {
writeThreadsLimiter <- zero.Zero{}
}
func ReleaseWriteThread() {
<-writeThreadsLimiter
}