Initial commit (code only without large binaries)
This commit is contained in:
87
EdgeNode/internal/utils/bfs/DESIGN.md
Normal file
87
EdgeNode/internal/utils/bfs/DESIGN.md
Normal 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的时候记录空闲blocks:freeBlocks
|
||||
* 再次被使用的时候减去空闲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 ], ...
|
||||
~~~
|
||||
|
||||
15
EdgeNode/internal/utils/bfs/block_info.go
Normal file
15
EdgeNode/internal/utils/bfs/block_info.go
Normal 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
|
||||
}
|
||||
403
EdgeNode/internal/utils/bfs/blocks_file.go
Normal file
403
EdgeNode/internal/utils/bfs/blocks_file.go
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
17
EdgeNode/internal/utils/bfs/blocks_file_options.go
Normal file
17
EdgeNode/internal/utils/bfs/blocks_file_options.go
Normal 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,
|
||||
}
|
||||
86
EdgeNode/internal/utils/bfs/blocks_file_test.go
Normal file
86
EdgeNode/internal/utils/bfs/blocks_file_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
20
EdgeNode/internal/utils/bfs/errors.go
Normal file
20
EdgeNode/internal/utils/bfs/errors.go
Normal 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)
|
||||
}
|
||||
203
EdgeNode/internal/utils/bfs/file_header.go
Normal file
203
EdgeNode/internal/utils/bfs/file_header.go
Normal 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())
|
||||
}
|
||||
67
EdgeNode/internal/utils/bfs/file_header_lazy.go
Normal file
67
EdgeNode/internal/utils/bfs/file_header_lazy.go
Normal 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
|
||||
}
|
||||
87
EdgeNode/internal/utils/bfs/file_header_lazy_test.go
Normal file
87
EdgeNode/internal/utils/bfs/file_header_lazy_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
432
EdgeNode/internal/utils/bfs/file_header_test.go
Normal file
432
EdgeNode/internal/utils/bfs/file_header_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
88
EdgeNode/internal/utils/bfs/file_reader.go
Normal file
88
EdgeNode/internal/utils/bfs/file_reader.go
Normal 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()
|
||||
}
|
||||
237
EdgeNode/internal/utils/bfs/file_reader_test.go
Normal file
237
EdgeNode/internal/utils/bfs/file_reader_test.go
Normal 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()))
|
||||
}
|
||||
112
EdgeNode/internal/utils/bfs/file_writer.go
Normal file
112
EdgeNode/internal/utils/bfs/file_writer.go
Normal 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)
|
||||
}
|
||||
134
EdgeNode/internal/utils/bfs/file_writer_test.go
Normal file
134
EdgeNode/internal/utils/bfs/file_writer_test.go
Normal 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")
|
||||
}
|
||||
}
|
||||
442
EdgeNode/internal/utils/bfs/fs.go
Normal file
442
EdgeNode/internal/utils/bfs/fs.go
Normal 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
|
||||
}
|
||||
47
EdgeNode/internal/utils/bfs/fs_options.go
Normal file
47
EdgeNode/internal/utils/bfs/fs_options.go
Normal 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,
|
||||
}
|
||||
197
EdgeNode/internal/utils/bfs/fs_test.go
Normal file
197
EdgeNode/internal/utils/bfs/fs_test.go
Normal 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()
|
||||
}
|
||||
66
EdgeNode/internal/utils/bfs/gzip_reader_pool.go
Normal file
66
EdgeNode/internal/utils/bfs/gzip_reader_pool.go
Normal 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
|
||||
}
|
||||
63
EdgeNode/internal/utils/bfs/gzip_writer_pool.go
Normal file
63
EdgeNode/internal/utils/bfs/gzip_writer_pool.go
Normal 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
|
||||
}
|
||||
36
EdgeNode/internal/utils/bfs/hash.go
Normal file
36
EdgeNode/internal/utils/bfs/hash.go
Normal 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)
|
||||
}
|
||||
27
EdgeNode/internal/utils/bfs/hash_test.go
Normal file
27
EdgeNode/internal/utils/bfs/hash_test.go
Normal 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())))
|
||||
}
|
||||
}
|
||||
52
EdgeNode/internal/utils/bfs/meta_block.go
Normal file
52
EdgeNode/internal/utils/bfs/meta_block.go
Normal 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
|
||||
}
|
||||
52
EdgeNode/internal/utils/bfs/meta_block_test.go
Normal file
52
EdgeNode/internal/utils/bfs/meta_block_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
380
EdgeNode/internal/utils/bfs/meta_file.go
Normal file
380
EdgeNode/internal/utils/bfs/meta_file.go
Normal 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())
|
||||
}
|
||||
196
EdgeNode/internal/utils/bfs/meta_file_test.go
Normal file
196
EdgeNode/internal/utils/bfs/meta_file_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
25
EdgeNode/internal/utils/bfs/threads_limiter.go
Normal file
25
EdgeNode/internal/utils/bfs/threads_limiter.go
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user