Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
83f581f7f0 | ||
|
|
03ce082926 | ||
|
|
ebafe458ff | ||
|
|
ff30b8d15c | ||
|
|
51dd778ca7 | ||
|
|
b5f706686c | ||
|
|
b67c2ec39c |
143
internal/caches/partial_ranges.go
Normal file
143
internal/caches/partial_ranges.go
Normal file
@@ -0,0 +1,143 @@
|
||||
// Copyright 2022 Liuxiangchao iwind.liu@gmail.com. All rights reserved.
|
||||
|
||||
package caches
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
)
|
||||
|
||||
// PartialRanges 内容分区范围定义
|
||||
type PartialRanges struct {
|
||||
ranges [][2]int64
|
||||
}
|
||||
|
||||
// NewPartialRanges 获取新对象
|
||||
func NewPartialRanges() *PartialRanges {
|
||||
return &PartialRanges{ranges: [][2]int64{}}
|
||||
}
|
||||
|
||||
// NewPartialRangesFromJSON 从JSON中解析范围
|
||||
func NewPartialRangesFromJSON(data []byte) (*PartialRanges, error) {
|
||||
var rs = [][2]int64{}
|
||||
err := json.Unmarshal(data, &rs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var r = NewPartialRanges()
|
||||
r.ranges = rs
|
||||
return r, nil
|
||||
}
|
||||
|
||||
// Add 添加新范围
|
||||
func (this *PartialRanges) Add(begin int64, end int64) {
|
||||
if begin > end {
|
||||
begin, end = end, begin
|
||||
}
|
||||
|
||||
var nr = [2]int64{begin, end}
|
||||
|
||||
var count = len(this.ranges)
|
||||
if count == 0 {
|
||||
this.ranges = [][2]int64{nr}
|
||||
return
|
||||
}
|
||||
|
||||
// insert
|
||||
// TODO 将来使用二分法改进
|
||||
var index = -1
|
||||
for i, r := range this.ranges {
|
||||
if r[0] > begin || (r[0] == begin && r[1] >= end) {
|
||||
index = i
|
||||
this.ranges = append(this.ranges, [2]int64{})
|
||||
copy(this.ranges[index+1:], this.ranges[index:])
|
||||
this.ranges[index] = nr
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if index == -1 {
|
||||
index = count
|
||||
this.ranges = append(this.ranges, nr)
|
||||
}
|
||||
|
||||
this.merge(index)
|
||||
}
|
||||
|
||||
// Ranges 获取所有范围
|
||||
func (this *PartialRanges) Ranges() [][2]int64 {
|
||||
return this.ranges
|
||||
}
|
||||
|
||||
// Contains 检查是否包含某个范围
|
||||
func (this *PartialRanges) Contains(begin int64, end int64) bool {
|
||||
if len(this.ranges) == 0 {
|
||||
return true
|
||||
}
|
||||
|
||||
// TODO 使用二分法查找改进性能
|
||||
for _, r2 := range this.ranges {
|
||||
if r2[0] <= begin && r2[1] >= end {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// AsJSON 转换为JSON
|
||||
func (this *PartialRanges) AsJSON() ([]byte, error) {
|
||||
return json.Marshal(this.ranges)
|
||||
}
|
||||
|
||||
func (this *PartialRanges) merge(index int) {
|
||||
// forward
|
||||
var lastIndex = index
|
||||
for i := index; i >= 1; i-- {
|
||||
var curr = this.ranges[i]
|
||||
var prev = this.ranges[i-1]
|
||||
var w1 = this.w(curr)
|
||||
var w2 = this.w(prev)
|
||||
if w1+w2 >= this.max(curr[1], prev[1])-this.min(curr[0], prev[0])-1 {
|
||||
prev = [2]int64{this.min(curr[0], prev[0]), this.max(curr[1], prev[1])}
|
||||
this.ranges[i-1] = prev
|
||||
this.ranges = append(this.ranges[:i], this.ranges[i+1:]...)
|
||||
lastIndex = i - 1
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// backward
|
||||
index = lastIndex
|
||||
for index < len(this.ranges)-1 {
|
||||
var curr = this.ranges[index]
|
||||
var next = this.ranges[index+1]
|
||||
var w1 = this.w(curr)
|
||||
var w2 = this.w(next)
|
||||
if w1+w2 >= this.max(curr[1], next[1])-this.min(curr[0], next[0])-1 {
|
||||
curr = [2]int64{this.min(curr[0], next[0]), this.max(curr[1], next[1])}
|
||||
this.ranges = append(this.ranges[:index], this.ranges[index+1:]...)
|
||||
this.ranges[index] = curr
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (this *PartialRanges) w(r [2]int64) int64 {
|
||||
return r[1] - r[0]
|
||||
}
|
||||
|
||||
func (this *PartialRanges) min(n1 int64, n2 int64) int64 {
|
||||
if n1 <= n2 {
|
||||
return n1
|
||||
}
|
||||
return n2
|
||||
}
|
||||
|
||||
func (this *PartialRanges) max(n1 int64, n2 int64) int64 {
|
||||
if n1 >= n2 {
|
||||
return n1
|
||||
}
|
||||
return n2
|
||||
}
|
||||
124
internal/caches/partial_ranges_test.go
Normal file
124
internal/caches/partial_ranges_test.go
Normal file
@@ -0,0 +1,124 @@
|
||||
// Copyright 2022 Liuxiangchao iwind.liu@gmail.com. All rights reserved.
|
||||
|
||||
package caches_test
|
||||
|
||||
import (
|
||||
"github.com/TeaOSLab/EdgeNode/internal/caches"
|
||||
"github.com/iwind/TeaGo/assert"
|
||||
"github.com/iwind/TeaGo/logs"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestNewPartialRanges(t *testing.T) {
|
||||
var r = caches.NewPartialRanges()
|
||||
r.Add(1, 100)
|
||||
r.Add(50, 300)
|
||||
|
||||
r.Add(30, 80)
|
||||
r.Add(30, 100)
|
||||
r.Add(30, 400)
|
||||
r.Add(1000, 10000)
|
||||
r.Add(200, 1000)
|
||||
r.Add(200, 10040)
|
||||
|
||||
logs.PrintAsJSON(r.Ranges())
|
||||
}
|
||||
|
||||
func TestNewPartialRanges1(t *testing.T) {
|
||||
var a = assert.NewAssertion(t)
|
||||
|
||||
var r = caches.NewPartialRanges()
|
||||
r.Add(1, 100)
|
||||
r.Add(1, 101)
|
||||
r.Add(1, 102)
|
||||
r.Add(2, 103)
|
||||
r.Add(200, 300)
|
||||
r.Add(1, 1000)
|
||||
|
||||
var rs = r.Ranges()
|
||||
logs.PrintAsJSON(rs, t)
|
||||
a.IsTrue(len(rs) == 1)
|
||||
if len(rs) == 1 {
|
||||
a.IsTrue(rs[0][0] == 1)
|
||||
a.IsTrue(rs[0][1] == 1000)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewPartialRanges2(t *testing.T) {
|
||||
// low -> high
|
||||
var r = caches.NewPartialRanges()
|
||||
r.Add(1, 100)
|
||||
r.Add(1, 101)
|
||||
r.Add(1, 102)
|
||||
r.Add(2, 103)
|
||||
r.Add(200, 300)
|
||||
r.Add(301, 302)
|
||||
r.Add(303, 304)
|
||||
r.Add(250, 400)
|
||||
|
||||
var rs = r.Ranges()
|
||||
logs.PrintAsJSON(rs, t)
|
||||
}
|
||||
|
||||
func TestNewPartialRanges3(t *testing.T) {
|
||||
// high -> low
|
||||
var r = caches.NewPartialRanges()
|
||||
r.Add(301, 302)
|
||||
r.Add(303, 304)
|
||||
r.Add(200, 300)
|
||||
r.Add(250, 400)
|
||||
|
||||
var rs = r.Ranges()
|
||||
logs.PrintAsJSON(rs, t)
|
||||
}
|
||||
|
||||
func TestNewPartialRanges4(t *testing.T) {
|
||||
// nearby
|
||||
var r = caches.NewPartialRanges()
|
||||
r.Add(301, 302)
|
||||
r.Add(303, 304)
|
||||
r.Add(305, 306)
|
||||
|
||||
r.Add(417, 417)
|
||||
r.Add(410, 415)
|
||||
r.Add(400, 409)
|
||||
|
||||
var rs = r.Ranges()
|
||||
logs.PrintAsJSON(rs, t)
|
||||
t.Log(r.Contains(400, 416))
|
||||
}
|
||||
|
||||
func TestNewPartialRanges5(t *testing.T) {
|
||||
var r = caches.NewPartialRanges()
|
||||
for j := 0; j < 1000; j++ {
|
||||
r.Add(int64(j), int64(j+100))
|
||||
}
|
||||
logs.PrintAsJSON(r.Ranges(), t)
|
||||
}
|
||||
|
||||
func TestNewPartialRanges_AsJSON(t *testing.T) {
|
||||
var r = caches.NewPartialRanges()
|
||||
for j := 0; j < 1000; j++ {
|
||||
r.Add(int64(j), int64(j+100))
|
||||
}
|
||||
data, err := r.AsJSON()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Log(string(data))
|
||||
|
||||
r2, err := caches.NewPartialRangesFromJSON(data)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Log(r2.Ranges())
|
||||
}
|
||||
|
||||
func BenchmarkNewPartialRanges(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
var r = caches.NewPartialRanges()
|
||||
for j := 0; j < 1000; j++ {
|
||||
r.Add(int64(j), int64(j+100))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -32,6 +32,10 @@ func NewFileReader(fp *os.File) *FileReader {
|
||||
}
|
||||
|
||||
func (this *FileReader) Init() error {
|
||||
return this.InitAutoDiscard(true)
|
||||
}
|
||||
|
||||
func (this *FileReader) InitAutoDiscard(autoDiscard bool) error {
|
||||
if this.openFile != nil {
|
||||
this.meta = this.openFile.meta
|
||||
this.header = this.openFile.header
|
||||
@@ -39,11 +43,13 @@ func (this *FileReader) Init() error {
|
||||
|
||||
isOk := false
|
||||
|
||||
defer func() {
|
||||
if !isOk {
|
||||
_ = this.discard()
|
||||
}
|
||||
}()
|
||||
if autoDiscard {
|
||||
defer func() {
|
||||
if !isOk {
|
||||
_ = this.discard()
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
var buf = this.meta
|
||||
if len(buf) == 0 {
|
||||
@@ -78,13 +84,13 @@ func (this *FileReader) Init() error {
|
||||
this.headerOffset = int64(SizeMeta) + int64(urlLength)
|
||||
|
||||
// body
|
||||
this.bodyOffset = this.headerOffset + int64(headerSize)
|
||||
bodySize := int(binary.BigEndian.Uint64(buf[SizeExpiresAt+SizeStatus+SizeURLLength+SizeHeaderLength : SizeExpiresAt+SizeStatus+SizeURLLength+SizeHeaderLength+SizeBodyLength]))
|
||||
if bodySize == 0 {
|
||||
isOk = true
|
||||
return nil
|
||||
}
|
||||
this.bodySize = int64(bodySize)
|
||||
this.bodyOffset = this.headerOffset + int64(headerSize)
|
||||
|
||||
// read header
|
||||
if this.openFileCache != nil && len(this.header) == 0 {
|
||||
|
||||
@@ -54,6 +54,30 @@ func TestFileReader(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestFileReader_ReadHeader(t *testing.T) {
|
||||
var path = "/Users/WorkSpace/EdgeProject/EdgeCache/p43/12/6b/126bbed90fc80f2bdfb19558948b0d49.cache"
|
||||
fp, err := os.Open(path)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer func() {
|
||||
_ = fp.Close()
|
||||
}()
|
||||
var reader = NewFileReader(fp)
|
||||
err = reader.Init()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var buf = make([]byte, 16*1024)
|
||||
err = reader.ReadHeader(buf, func(n int) (goNext bool, err error) {
|
||||
t.Log("header:", string(buf[:n]))
|
||||
return
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFileReader_Range(t *testing.T) {
|
||||
storage := NewFileStorage(&serverconfigs.HTTPCachePolicy{
|
||||
Id: 1,
|
||||
|
||||
@@ -325,11 +325,11 @@ func (this *FileStorage) openReader(key string, allowMemory bool, useStale bool)
|
||||
}
|
||||
|
||||
// OpenWriter 打开缓存文件等待写入
|
||||
func (this *FileStorage) OpenWriter(key string, expiredAt int64, status int, size int64) (Writer, error) {
|
||||
func (this *FileStorage) OpenWriter(key string, expiredAt int64, status int, size int64, isPartial bool) (Writer, error) {
|
||||
// 先尝试内存缓存
|
||||
// 我们限定仅小文件优先存在内存中
|
||||
if this.memoryStorage != nil && size > 0 && size < 32*1024*1024 {
|
||||
writer, err := this.memoryStorage.OpenWriter(key, expiredAt, status, size)
|
||||
if !isPartial && this.memoryStorage != nil && size > 0 && size < 32*1024*1024 {
|
||||
writer, err := this.memoryStorage.OpenWriter(key, expiredAt, status, size, false)
|
||||
if err == nil {
|
||||
return writer, nil
|
||||
}
|
||||
@@ -361,13 +361,13 @@ func (this *FileStorage) OpenWriter(key string, expiredAt int64, status int, siz
|
||||
if this.policy.MaxKeys > 0 && count > this.policy.MaxKeys {
|
||||
return nil, NewCapacityError("write file cache failed: too many keys in cache storage")
|
||||
}
|
||||
capacityBytes := this.diskCapacityBytes()
|
||||
var capacityBytes = this.diskCapacityBytes()
|
||||
if capacityBytes > 0 && capacityBytes <= this.totalSize {
|
||||
return nil, NewCapacityError("write file cache failed: over disk size, current total size: " + strconv.FormatInt(this.totalSize, 10) + " bytes, capacity: " + strconv.FormatInt(capacityBytes, 10))
|
||||
}
|
||||
|
||||
hash := stringutil.Md5(key)
|
||||
dir := this.cacheConfig.Dir + "/p" + strconv.FormatInt(this.policy.Id, 10) + "/" + hash[:2] + "/" + hash[2:4]
|
||||
var hash = stringutil.Md5(key)
|
||||
var dir = this.cacheConfig.Dir + "/p" + strconv.FormatInt(this.policy.Id, 10) + "/" + hash[:2] + "/" + hash[2:4]
|
||||
_, err = os.Stat(dir)
|
||||
if err != nil {
|
||||
if !os.IsNotExist(err) {
|
||||
@@ -387,6 +387,9 @@ func (this *FileStorage) OpenWriter(key string, expiredAt int64, status int, siz
|
||||
return nil, ErrFileIsWriting
|
||||
}
|
||||
var tmpPath = cachePath + ".tmp"
|
||||
if isPartial {
|
||||
tmpPath = cachePath
|
||||
}
|
||||
|
||||
// 先删除
|
||||
err = this.list.Remove(hash)
|
||||
@@ -421,70 +424,96 @@ func (this *FileStorage) OpenWriter(key string, expiredAt int64, status int, siz
|
||||
return nil, ErrFileIsWriting
|
||||
}
|
||||
|
||||
err = writer.Truncate(0)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 写入过期时间
|
||||
bytes4 := make([]byte, 4)
|
||||
{
|
||||
binary.BigEndian.PutUint32(bytes4, uint32(expiredAt))
|
||||
_, err = writer.Write(bytes4)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
// 是否已经有内容
|
||||
var isNewCreated = true
|
||||
var partialBodyOffset int64
|
||||
if isPartial {
|
||||
partialFP, err := os.OpenFile(tmpPath, os.O_RDONLY, 0444)
|
||||
if err == nil {
|
||||
var partialReader = NewFileReader(partialFP)
|
||||
err = partialReader.InitAutoDiscard(false)
|
||||
if err == nil && partialReader.bodyOffset > 0 {
|
||||
isNewCreated = false
|
||||
partialBodyOffset = partialReader.bodyOffset
|
||||
}
|
||||
_ = partialReader.Close()
|
||||
}
|
||||
}
|
||||
|
||||
// 写入状态码
|
||||
if status > 999 || status < 100 {
|
||||
status = 200
|
||||
}
|
||||
_, err = writer.WriteString(strconv.Itoa(status))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 写入URL长度
|
||||
{
|
||||
binary.BigEndian.PutUint32(bytes4, uint32(len(key)))
|
||||
_, err = writer.Write(bytes4)
|
||||
if isNewCreated {
|
||||
err = writer.Truncate(0)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// 写入Header Length
|
||||
{
|
||||
binary.BigEndian.PutUint32(bytes4, uint32(0))
|
||||
_, err = writer.Write(bytes4)
|
||||
// 写入过期时间
|
||||
bytes4 := make([]byte, 4)
|
||||
{
|
||||
binary.BigEndian.PutUint32(bytes4, uint32(expiredAt))
|
||||
_, err = writer.Write(bytes4)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// 写入状态码
|
||||
if status > 999 || status < 100 {
|
||||
status = 200
|
||||
}
|
||||
_, err = writer.WriteString(strconv.Itoa(status))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// 写入Body Length
|
||||
{
|
||||
b := make([]byte, SizeBodyLength)
|
||||
binary.BigEndian.PutUint64(b, uint64(0))
|
||||
_, err = writer.Write(b)
|
||||
// 写入URL长度
|
||||
{
|
||||
binary.BigEndian.PutUint32(bytes4, uint32(len(key)))
|
||||
_, err = writer.Write(bytes4)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// 写入Header Length
|
||||
{
|
||||
binary.BigEndian.PutUint32(bytes4, uint32(0))
|
||||
_, err = writer.Write(bytes4)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// 写入Body Length
|
||||
{
|
||||
b := make([]byte, SizeBodyLength)
|
||||
binary.BigEndian.PutUint64(b, uint64(0))
|
||||
_, err = writer.Write(b)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// 写入URL
|
||||
_, err = writer.WriteString(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// 写入URL
|
||||
_, err = writer.WriteString(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
isOk = true
|
||||
return NewFileWriter(writer, key, expiredAt, func() {
|
||||
sharedWritingFileKeyLocker.Lock()
|
||||
delete(sharedWritingFileKeyMap, key)
|
||||
sharedWritingFileKeyLocker.Unlock()
|
||||
}), nil
|
||||
if isPartial {
|
||||
return NewPartialFileWriter(writer, key, expiredAt, isNewCreated, isPartial, partialBodyOffset, func() {
|
||||
sharedWritingFileKeyLocker.Lock()
|
||||
delete(sharedWritingFileKeyMap, key)
|
||||
sharedWritingFileKeyLocker.Unlock()
|
||||
}), nil
|
||||
} else {
|
||||
return NewFileWriter(writer, key, expiredAt, func() {
|
||||
sharedWritingFileKeyLocker.Lock()
|
||||
delete(sharedWritingFileKeyMap, key)
|
||||
sharedWritingFileKeyLocker.Unlock()
|
||||
}), nil
|
||||
}
|
||||
}
|
||||
|
||||
// AddToList 添加到List
|
||||
@@ -930,6 +959,9 @@ func (this *FileStorage) hotLoop() {
|
||||
|
||||
err = reader.ReadBody(buf, func(n int) (goNext bool, err error) {
|
||||
_, err = writer.Write(buf[:n])
|
||||
if err == nil {
|
||||
goNext = true
|
||||
}
|
||||
return
|
||||
})
|
||||
if err != nil {
|
||||
|
||||
@@ -62,7 +62,7 @@ func TestFileStorage_OpenWriter(t *testing.T) {
|
||||
|
||||
header := []byte("Header")
|
||||
body := []byte("This is Body")
|
||||
writer, err := storage.OpenWriter("my-key", time.Now().Unix()+86400, 200, -1)
|
||||
writer, err := storage.OpenWriter("my-key", time.Now().Unix()+86400, 200, -1, false)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -87,6 +87,41 @@ func TestFileStorage_OpenWriter(t *testing.T) {
|
||||
t.Log("ok")
|
||||
}
|
||||
|
||||
func TestFileStorage_OpenWriter_Partial(t *testing.T) {
|
||||
var storage = NewFileStorage(&serverconfigs.HTTPCachePolicy{
|
||||
Id: 2,
|
||||
IsOn: true,
|
||||
Options: map[string]interface{}{
|
||||
"dir": Tea.Root + "/caches",
|
||||
},
|
||||
})
|
||||
err := storage.Init()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
writer, err := storage.OpenWriter("my-key", time.Now().Unix()+86400, 200, -1, true)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
_, err = writer.WriteHeader([]byte("Content-Type:text/html; charset=utf-8"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
err = writer.WriteAt([]byte("Hello, World"), 0)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
err = writer.Close()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Log(writer)
|
||||
}
|
||||
|
||||
func TestFileStorage_OpenWriter_HTTP(t *testing.T) {
|
||||
storage := NewFileStorage(&serverconfigs.HTTPCachePolicy{
|
||||
Id: 1,
|
||||
@@ -104,7 +139,7 @@ func TestFileStorage_OpenWriter_HTTP(t *testing.T) {
|
||||
t.Log(time.Since(now).Seconds()*1000, "ms")
|
||||
}()
|
||||
|
||||
writer, err := storage.OpenWriter("my-http-response", time.Now().Unix()+86400, 200, -1)
|
||||
writer, err := storage.OpenWriter("my-http-response", time.Now().Unix()+86400, 200, -1, false)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -177,10 +212,11 @@ func TestFileStorage_Concurrent_Open_DifferentFile(t *testing.T) {
|
||||
go func(i int) {
|
||||
defer wg.Done()
|
||||
|
||||
writer, err := storage.OpenWriter("abc"+strconv.Itoa(i), time.Now().Unix()+3600, 200, -1)
|
||||
writer, err := storage.OpenWriter("abc"+strconv.Itoa(i), time.Now().Unix()+3600, 200, -1, false)
|
||||
if err != nil {
|
||||
if err != ErrFileIsWriting {
|
||||
t.Fatal(err)
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -188,7 +224,8 @@ func TestFileStorage_Concurrent_Open_DifferentFile(t *testing.T) {
|
||||
|
||||
_, err = writer.Write([]byte("Hello,World"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
// 故意造成慢速写入
|
||||
@@ -196,7 +233,8 @@ func TestFileStorage_Concurrent_Open_DifferentFile(t *testing.T) {
|
||||
|
||||
err = writer.Close()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
}(i)
|
||||
}
|
||||
@@ -229,10 +267,11 @@ func TestFileStorage_Concurrent_Open_SameFile(t *testing.T) {
|
||||
go func(i int) {
|
||||
defer wg.Done()
|
||||
|
||||
writer, err := storage.OpenWriter("abc"+strconv.Itoa(0), time.Now().Unix()+3600, 200, -1)
|
||||
writer, err := storage.OpenWriter("abc"+strconv.Itoa(0), time.Now().Unix()+3600, 200, -1, false)
|
||||
if err != nil {
|
||||
if err != ErrFileIsWriting {
|
||||
t.Fatal(err)
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -241,7 +280,8 @@ func TestFileStorage_Concurrent_Open_SameFile(t *testing.T) {
|
||||
t.Log("writing")
|
||||
_, err = writer.Write([]byte("Hello,World"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
// 故意造成慢速写入
|
||||
@@ -249,7 +289,8 @@ func TestFileStorage_Concurrent_Open_SameFile(t *testing.T) {
|
||||
|
||||
err = writer.Close()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
}(i)
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ type StorageInterface interface {
|
||||
OpenReader(key string, useStale bool) (reader Reader, err error)
|
||||
|
||||
// OpenWriter 打开缓存写入器等待写入
|
||||
OpenWriter(key string, expiredAt int64, status int, size int64) (Writer, error)
|
||||
OpenWriter(key string, expiredAt int64, status int, size int64, isPartial bool) (Writer, error)
|
||||
|
||||
// Delete 删除某个键值对应的缓存
|
||||
Delete(key string) error
|
||||
|
||||
@@ -145,7 +145,11 @@ func (this *MemoryStorage) OpenReader(key string, useStale bool) (Reader, error)
|
||||
}
|
||||
|
||||
// OpenWriter 打开缓存写入器等待写入
|
||||
func (this *MemoryStorage) OpenWriter(key string, expiredAt int64, status int, size int64) (Writer, error) {
|
||||
func (this *MemoryStorage) OpenWriter(key string, expiredAt int64, status int, size int64, isPartial bool) (Writer, error) {
|
||||
// TODO 内存缓存暂时不支持分块内容存储
|
||||
if isPartial {
|
||||
return nil, ErrFileIsWriting
|
||||
}
|
||||
return this.openWriter(key, expiredAt, status, size, true)
|
||||
}
|
||||
|
||||
@@ -387,7 +391,7 @@ func (this *MemoryStorage) flushItem(key string) {
|
||||
return
|
||||
}
|
||||
|
||||
writer, err := this.parentStorage.OpenWriter(key, item.ExpiredAt, item.Status, -1)
|
||||
writer, err := this.parentStorage.OpenWriter(key, item.ExpiredAt, item.Status, -1, false)
|
||||
if err != nil {
|
||||
if !CanIgnoreErr(err) {
|
||||
remotelogs.Error("CACHE", "flush items failed: open writer failed: "+err.Error())
|
||||
|
||||
@@ -15,7 +15,7 @@ import (
|
||||
func TestMemoryStorage_OpenWriter(t *testing.T) {
|
||||
storage := NewMemoryStorage(&serverconfigs.HTTPCachePolicy{}, nil)
|
||||
|
||||
writer, err := storage.OpenWriter("abc", time.Now().Unix()+60, 200, -1)
|
||||
writer, err := storage.OpenWriter("abc", time.Now().Unix()+60, 200, -1, false)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -62,7 +62,7 @@ func TestMemoryStorage_OpenWriter(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
writer, err = storage.OpenWriter("abc", time.Now().Unix()+60, 200, -1)
|
||||
writer, err = storage.OpenWriter("abc", time.Now().Unix()+60, 200, -1, false)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -103,7 +103,7 @@ func TestMemoryStorage_OpenReaderLock(t *testing.T) {
|
||||
func TestMemoryStorage_Delete(t *testing.T) {
|
||||
storage := NewMemoryStorage(&serverconfigs.HTTPCachePolicy{}, nil)
|
||||
{
|
||||
writer, err := storage.OpenWriter("abc", time.Now().Unix()+60, 200, -1)
|
||||
writer, err := storage.OpenWriter("abc", time.Now().Unix()+60, 200, -1, false)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -111,7 +111,7 @@ func TestMemoryStorage_Delete(t *testing.T) {
|
||||
t.Log(len(storage.valuesMap))
|
||||
}
|
||||
{
|
||||
writer, err := storage.OpenWriter("abc1", time.Now().Unix()+60, 200, -1)
|
||||
writer, err := storage.OpenWriter("abc1", time.Now().Unix()+60, 200, -1, false)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -126,7 +126,7 @@ func TestMemoryStorage_Stat(t *testing.T) {
|
||||
storage := NewMemoryStorage(&serverconfigs.HTTPCachePolicy{}, nil)
|
||||
expiredAt := time.Now().Unix() + 60
|
||||
{
|
||||
writer, err := storage.OpenWriter("abc", expiredAt, 200, -1)
|
||||
writer, err := storage.OpenWriter("abc", expiredAt, 200, -1, false)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -139,7 +139,7 @@ func TestMemoryStorage_Stat(t *testing.T) {
|
||||
})
|
||||
}
|
||||
{
|
||||
writer, err := storage.OpenWriter("abc1", expiredAt, 200, -1)
|
||||
writer, err := storage.OpenWriter("abc1", expiredAt, 200, -1, false)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -163,7 +163,7 @@ func TestMemoryStorage_CleanAll(t *testing.T) {
|
||||
storage := NewMemoryStorage(&serverconfigs.HTTPCachePolicy{}, nil)
|
||||
expiredAt := time.Now().Unix() + 60
|
||||
{
|
||||
writer, err := storage.OpenWriter("abc", expiredAt, 200, -1)
|
||||
writer, err := storage.OpenWriter("abc", expiredAt, 200, -1, false)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -175,7 +175,7 @@ func TestMemoryStorage_CleanAll(t *testing.T) {
|
||||
})
|
||||
}
|
||||
{
|
||||
writer, err := storage.OpenWriter("abc1", expiredAt, 200, -1)
|
||||
writer, err := storage.OpenWriter("abc1", expiredAt, 200, -1, false)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -198,7 +198,7 @@ func TestMemoryStorage_Purge(t *testing.T) {
|
||||
storage := NewMemoryStorage(&serverconfigs.HTTPCachePolicy{}, nil)
|
||||
expiredAt := time.Now().Unix() + 60
|
||||
{
|
||||
writer, err := storage.OpenWriter("abc", expiredAt, 200, -1)
|
||||
writer, err := storage.OpenWriter("abc", expiredAt, 200, -1, false)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -210,7 +210,7 @@ func TestMemoryStorage_Purge(t *testing.T) {
|
||||
})
|
||||
}
|
||||
{
|
||||
writer, err := storage.OpenWriter("abc1", expiredAt, 200, -1)
|
||||
writer, err := storage.OpenWriter("abc1", expiredAt, 200, -1, false)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -241,7 +241,7 @@ func TestMemoryStorage_Expire(t *testing.T) {
|
||||
for i := 0; i < 1000; i++ {
|
||||
expiredAt := time.Now().Unix() + int64(rands.Int(0, 60))
|
||||
key := "abc" + strconv.Itoa(i)
|
||||
writer, err := storage.OpenWriter(key, expiredAt, 200, -1)
|
||||
writer, err := storage.OpenWriter(key, expiredAt, 200, -1, false)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -8,6 +8,9 @@ type Writer interface {
|
||||
// Write 写入Body数据
|
||||
Write(data []byte) (n int, err error)
|
||||
|
||||
// WriteAt 在指定位置写入数据
|
||||
WriteAt(data []byte, offset int64) error
|
||||
|
||||
// HeaderSize 写入的Header数据大小
|
||||
HeaderSize() int64
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ package caches
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"github.com/iwind/TeaGo/types"
|
||||
"io"
|
||||
"os"
|
||||
@@ -65,6 +66,13 @@ func (this *FileWriter) Write(data []byte) (n int, err error) {
|
||||
return
|
||||
}
|
||||
|
||||
// WriteAt 在指定位置写入数据
|
||||
func (this *FileWriter) WriteAt(data []byte, offset int64) error {
|
||||
_ = data
|
||||
_ = offset
|
||||
return errors.New("not supported")
|
||||
}
|
||||
|
||||
// WriteBodyLength 写入Body长度数据
|
||||
func (this *FileWriter) WriteBodyLength(bodyLength int64) error {
|
||||
bytes8 := make([]byte, 8)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package caches
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"github.com/cespare/xxhash"
|
||||
"sync"
|
||||
"time"
|
||||
@@ -55,6 +56,13 @@ func (this *MemoryWriter) Write(data []byte) (n int, err error) {
|
||||
return len(data), nil
|
||||
}
|
||||
|
||||
// WriteAt 在指定位置写入数据
|
||||
func (this *MemoryWriter) WriteAt(b []byte, offset int64) error {
|
||||
_ = b
|
||||
_ = offset
|
||||
return errors.New("not supported")
|
||||
}
|
||||
|
||||
// HeaderSize 数据尺寸
|
||||
func (this *MemoryWriter) HeaderSize() int64 {
|
||||
return this.headerSize
|
||||
|
||||
173
internal/caches/writer_partial_file.go
Normal file
173
internal/caches/writer_partial_file.go
Normal file
@@ -0,0 +1,173 @@
|
||||
// Copyright 2022 Liuxiangchao iwind.liu@gmail.com. All rights reserved.
|
||||
|
||||
package caches
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"github.com/iwind/TeaGo/types"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type PartialFileWriter struct {
|
||||
rawWriter *os.File
|
||||
key string
|
||||
headerSize int64
|
||||
bodySize int64
|
||||
expiredAt int64
|
||||
endFunc func()
|
||||
once sync.Once
|
||||
|
||||
isNew bool
|
||||
isPartial bool
|
||||
bodyOffset int64
|
||||
}
|
||||
|
||||
func NewPartialFileWriter(rawWriter *os.File, key string, expiredAt int64, isNew bool, isPartial bool, bodyOffset int64, endFunc func()) *PartialFileWriter {
|
||||
return &PartialFileWriter{
|
||||
key: key,
|
||||
rawWriter: rawWriter,
|
||||
expiredAt: expiredAt,
|
||||
endFunc: endFunc,
|
||||
isNew: isNew,
|
||||
isPartial: isPartial,
|
||||
bodyOffset: bodyOffset,
|
||||
}
|
||||
}
|
||||
|
||||
// WriteHeader 写入数据
|
||||
func (this *PartialFileWriter) WriteHeader(data []byte) (n int, err error) {
|
||||
if !this.isNew {
|
||||
return
|
||||
}
|
||||
n, err = this.rawWriter.Write(data)
|
||||
this.headerSize += int64(n)
|
||||
if err != nil {
|
||||
_ = this.Discard()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// WriteHeaderLength 写入Header长度数据
|
||||
func (this *PartialFileWriter) WriteHeaderLength(headerLength int) error {
|
||||
bytes4 := make([]byte, 4)
|
||||
binary.BigEndian.PutUint32(bytes4, uint32(headerLength))
|
||||
_, err := this.rawWriter.Seek(SizeExpiresAt+SizeStatus+SizeURLLength, io.SeekStart)
|
||||
if err != nil {
|
||||
_ = this.Discard()
|
||||
return err
|
||||
}
|
||||
_, err = this.rawWriter.Write(bytes4)
|
||||
if err != nil {
|
||||
_ = this.Discard()
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Write 写入数据
|
||||
func (this *PartialFileWriter) Write(data []byte) (n int, err error) {
|
||||
n, err = this.rawWriter.Write(data)
|
||||
this.bodySize += int64(n)
|
||||
if err != nil {
|
||||
_ = this.Discard()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// WriteAt 在指定位置写入数据
|
||||
func (this *PartialFileWriter) WriteAt(data []byte, offset int64) error {
|
||||
if this.bodyOffset == 0 {
|
||||
this.bodyOffset = SizeMeta + int64(len(this.key)) + this.headerSize
|
||||
}
|
||||
_, err := this.rawWriter.WriteAt(data, this.bodyOffset+offset)
|
||||
return err
|
||||
}
|
||||
|
||||
// WriteBodyLength 写入Body长度数据
|
||||
func (this *PartialFileWriter) WriteBodyLength(bodyLength int64) error {
|
||||
bytes8 := make([]byte, 8)
|
||||
binary.BigEndian.PutUint64(bytes8, uint64(bodyLength))
|
||||
_, err := this.rawWriter.Seek(SizeExpiresAt+SizeStatus+SizeURLLength+SizeHeaderLength, io.SeekStart)
|
||||
if err != nil {
|
||||
_ = this.Discard()
|
||||
return err
|
||||
}
|
||||
_, err = this.rawWriter.Write(bytes8)
|
||||
if err != nil {
|
||||
_ = this.Discard()
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Close 关闭
|
||||
func (this *PartialFileWriter) Close() error {
|
||||
defer this.once.Do(func() {
|
||||
this.endFunc()
|
||||
})
|
||||
|
||||
var path = this.rawWriter.Name()
|
||||
|
||||
if this.isNew {
|
||||
err := this.WriteHeaderLength(types.Int(this.headerSize))
|
||||
if err != nil {
|
||||
_ = this.rawWriter.Close()
|
||||
_ = os.Remove(path)
|
||||
return err
|
||||
}
|
||||
err = this.WriteBodyLength(this.bodySize)
|
||||
if err != nil {
|
||||
_ = this.rawWriter.Close()
|
||||
_ = os.Remove(path)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
err := this.rawWriter.Close()
|
||||
if err != nil {
|
||||
_ = os.Remove(path)
|
||||
} else if !this.isPartial {
|
||||
err = os.Rename(path, strings.Replace(path, ".tmp", "", 1))
|
||||
if err != nil {
|
||||
_ = os.Remove(path)
|
||||
}
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// Discard 丢弃
|
||||
func (this *PartialFileWriter) Discard() error {
|
||||
defer this.once.Do(func() {
|
||||
this.endFunc()
|
||||
})
|
||||
|
||||
_ = this.rawWriter.Close()
|
||||
|
||||
err := os.Remove(this.rawWriter.Name())
|
||||
return err
|
||||
}
|
||||
|
||||
func (this *PartialFileWriter) HeaderSize() int64 {
|
||||
return this.headerSize
|
||||
}
|
||||
|
||||
func (this *PartialFileWriter) BodySize() int64 {
|
||||
return this.bodySize
|
||||
}
|
||||
|
||||
func (this *PartialFileWriter) ExpiredAt() int64 {
|
||||
return this.expiredAt
|
||||
}
|
||||
|
||||
func (this *PartialFileWriter) Key() string {
|
||||
return this.key
|
||||
}
|
||||
|
||||
// ItemType 获取内容类型
|
||||
func (this *PartialFileWriter) ItemType() ItemType {
|
||||
return ItemTypeFile
|
||||
}
|
||||
50
internal/caches/writer_partial_file_test.go
Normal file
50
internal/caches/writer_partial_file_test.go
Normal file
@@ -0,0 +1,50 @@
|
||||
// Copyright 2022 Liuxiangchao iwind.liu@gmail.com. All rights reserved.
|
||||
|
||||
package caches_test
|
||||
|
||||
import (
|
||||
"github.com/TeaOSLab/EdgeNode/internal/caches"
|
||||
"github.com/iwind/TeaGo/types"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestPartialFileWriter_SeekOffset(t *testing.T) {
|
||||
var path = "/tmp/test@partial.cache"
|
||||
_ = os.Remove(path)
|
||||
|
||||
var reader = func() {
|
||||
data, err := ioutil.ReadFile(path)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Log("["+types.String(len(data))+"]", string(data))
|
||||
}
|
||||
|
||||
fp, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY, 0666)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var writer = caches.NewPartialFileWriter(fp, "test", time.Now().Unix()+86500, true, true, 0, func() {
|
||||
t.Log("end")
|
||||
})
|
||||
_, err = writer.WriteHeader([]byte("header"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// 移动位置
|
||||
err = writer.WriteAt([]byte("HELLO"), 100)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
err = writer.Close()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
reader()
|
||||
}
|
||||
@@ -18,6 +18,11 @@ const (
|
||||
|
||||
var ErrNotSupportedContentEncoding = errors.New("not supported content encoding")
|
||||
|
||||
// AllEncodings 当前支持的所有编码
|
||||
func AllEncodings() []ContentEncoding {
|
||||
return []ContentEncoding{ContentEncodingBr, ContentEncodingGzip, ContentEncodingDeflate}
|
||||
}
|
||||
|
||||
// NewReader 获取Reader
|
||||
func NewReader(reader io.Reader, contentEncoding ContentEncoding) (Reader, error) {
|
||||
switch contentEncoding {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package teaconst
|
||||
|
||||
const (
|
||||
Version = "0.4.1"
|
||||
Version = "0.4.3"
|
||||
|
||||
ProductName = "Edge Node"
|
||||
ProcessName = "edge-node"
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
|
||||
"github.com/TeaOSLab/EdgeCommon/pkg/serverconfigs"
|
||||
"github.com/TeaOSLab/EdgeNode/internal/caches"
|
||||
"github.com/TeaOSLab/EdgeNode/internal/compressions"
|
||||
"github.com/TeaOSLab/EdgeNode/internal/configs"
|
||||
teaconst "github.com/TeaOSLab/EdgeNode/internal/const"
|
||||
"github.com/TeaOSLab/EdgeNode/internal/errors"
|
||||
@@ -182,7 +183,7 @@ func (this *APIStream) handleWriteCache(message *pb.NodeStreamMessage) error {
|
||||
}
|
||||
|
||||
expiredAt := time.Now().Unix() + msg.LifeSeconds
|
||||
writer, err := storage.OpenWriter(msg.Key, expiredAt, 200, int64(len(msg.Value)))
|
||||
writer, err := storage.OpenWriter(msg.Key, expiredAt, 200, int64(len(msg.Value)), false)
|
||||
if err != nil {
|
||||
this.replyFail(message.RequestId, "prepare writing failed: "+err.Error())
|
||||
return err
|
||||
@@ -350,7 +351,12 @@ func (this *APIStream) handlePurgeCache(message *pb.NodeStreamMessage) error {
|
||||
if msg.Type == "file" {
|
||||
var keys = msg.Keys
|
||||
for _, key := range keys {
|
||||
keys = append(keys, key+webpSuffix)
|
||||
keys = append(keys, key+webpCacheSuffix, key+cacheMethodSuffix+"HEAD")
|
||||
// TODO 根据实际缓存的内容进行组合
|
||||
for _, encoding := range compressions.AllEncodings() {
|
||||
keys = append(keys, key+compressionCacheSuffix+encoding)
|
||||
keys = append(keys, key+webpCacheSuffix+compressionCacheSuffix+encoding)
|
||||
}
|
||||
}
|
||||
msg.Keys = keys
|
||||
}
|
||||
@@ -462,7 +468,7 @@ func (this *APIStream) handlePreheatCache(message *pb.NodeStreamMessage) error {
|
||||
}
|
||||
|
||||
expiredAt := time.Now().Unix() + 8600
|
||||
writer, err := storage.OpenWriter(key, expiredAt, 200, resp.ContentLength) // TODO 可以设置缓存过期时间
|
||||
writer, err := storage.OpenWriter(key, expiredAt, 200, resp.ContentLength, false) // TODO 可以设置缓存过期时间
|
||||
if err != nil {
|
||||
locker.Lock()
|
||||
errorMessages = append(errorMessages, "open cache writer failed: "+key+": "+err.Error())
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"errors"
|
||||
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
|
||||
"github.com/TeaOSLab/EdgeNode/internal/caches"
|
||||
"github.com/TeaOSLab/EdgeNode/internal/compressions"
|
||||
"github.com/TeaOSLab/EdgeNode/internal/goman"
|
||||
"github.com/TeaOSLab/EdgeNode/internal/remotelogs"
|
||||
"github.com/TeaOSLab/EdgeNode/internal/rpc"
|
||||
@@ -106,12 +107,20 @@ func (this *HTTPRequest) doCacheRead(useStale bool) (shouldStop bool) {
|
||||
|
||||
// TODO 支持Vary Header
|
||||
|
||||
// 缓存标签
|
||||
var tags = []string{}
|
||||
|
||||
// 检查是否有缓存
|
||||
key := this.Format(this.cacheRef.Key)
|
||||
var key = this.Format(this.cacheRef.Key)
|
||||
if len(key) == 0 {
|
||||
this.cacheRef = nil
|
||||
return
|
||||
}
|
||||
var method = this.Method()
|
||||
if method != http.MethodGet && method != http.MethodPost {
|
||||
key += cacheMethodSuffix + method
|
||||
tags = append(tags, strings.ToLower(method))
|
||||
}
|
||||
|
||||
this.cacheKey = key
|
||||
this.varMapping["cache.key"] = key
|
||||
@@ -122,16 +131,27 @@ func (this *HTTPRequest) doCacheRead(useStale bool) (shouldStop bool) {
|
||||
this.cacheRef = nil
|
||||
return
|
||||
}
|
||||
this.writer.cacheStorage = storage
|
||||
|
||||
// 判断是否在Purge
|
||||
if this.web.Cache.PurgeIsOn && strings.ToUpper(this.RawReq.Method) == "PURGE" && this.RawReq.Header.Get("X-Edge-Purge-Key") == this.web.Cache.PurgeKey {
|
||||
this.varMapping["cache.status"] = "PURGE"
|
||||
|
||||
err := storage.Delete(key)
|
||||
if err != nil {
|
||||
remotelogs.Error("HTTP_REQUEST_CACHE", "purge failed: "+err.Error())
|
||||
var subKeys = []string{key, key + cacheMethodSuffix + "HEAD"}
|
||||
// TODO 根据实际缓存的内容进行组合
|
||||
for _, encoding := range compressions.AllEncodings() {
|
||||
subKeys = append(subKeys, key+compressionCacheSuffix+encoding)
|
||||
subKeys = append(subKeys, key+webpCacheSuffix+compressionCacheSuffix+encoding)
|
||||
}
|
||||
for _, subKey := range subKeys {
|
||||
err := storage.Delete(subKey)
|
||||
if err != nil {
|
||||
remotelogs.Error("HTTP_REQUEST_CACHE", "purge failed: "+err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
// 通过API节点清除别节点上的的Key
|
||||
// TODO 改为队列,不需要每个请求都使用goroutine
|
||||
goman.New(func() {
|
||||
rpcClient, err := rpc.SharedRPC()
|
||||
if err == nil {
|
||||
@@ -160,15 +180,47 @@ func (this *HTTPRequest) doCacheRead(useStale bool) (shouldStop bool) {
|
||||
var reader caches.Reader
|
||||
var err error
|
||||
|
||||
// 是否优先检查WebP
|
||||
var isWebP = false
|
||||
if this.web.WebP != nil &&
|
||||
// 检查是否支持WebP
|
||||
var webPIsEnabled = false
|
||||
var isHeadMethod = method == http.MethodHead
|
||||
if !isHeadMethod &&
|
||||
this.web.WebP != nil &&
|
||||
this.web.WebP.IsOn &&
|
||||
this.web.WebP.MatchRequest(filepath.Ext(this.Path()), this.Format) &&
|
||||
this.web.WebP.MatchAccept(this.requestHeader("Accept")) {
|
||||
reader, _ = storage.OpenReader(key+webpSuffix, useStale)
|
||||
this.web.WebP.MatchAccept(this.RawReq.Header.Get("Accept")) {
|
||||
webPIsEnabled = true
|
||||
}
|
||||
|
||||
// 检查压缩缓存
|
||||
if !isHeadMethod && reader == nil {
|
||||
if this.web.Compression != nil && this.web.Compression.IsOn {
|
||||
_, encoding, ok := this.web.Compression.MatchAcceptEncoding(this.RawReq.Header.Get("Accept-Encoding"))
|
||||
if ok {
|
||||
// 检查支持WebP的压缩缓存
|
||||
if webPIsEnabled {
|
||||
reader, _ = storage.OpenReader(key+webpCacheSuffix+compressionCacheSuffix+encoding, useStale)
|
||||
if reader != nil {
|
||||
tags = append(tags, "webp", encoding)
|
||||
}
|
||||
}
|
||||
|
||||
// 检查普通缓存
|
||||
if reader == nil {
|
||||
reader, _ = storage.OpenReader(key+compressionCacheSuffix+encoding, useStale)
|
||||
if reader != nil {
|
||||
tags = append(tags, encoding)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 检查WebP
|
||||
if !isHeadMethod && reader == nil && webPIsEnabled {
|
||||
reader, _ = storage.OpenReader(key+webpCacheSuffix, useStale)
|
||||
if reader != nil {
|
||||
isWebP = true
|
||||
this.writer.cacheReaderSuffix = webpCacheSuffix
|
||||
tags = append(tags, "webp")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -252,6 +304,8 @@ func (this *HTTPRequest) doCacheRead(useStale bool) (shouldStop bool) {
|
||||
} else {
|
||||
this.writer.Header().Set("X-Cache", "HIT, "+refType+", "+reader.TypeName())
|
||||
}
|
||||
} else {
|
||||
this.writer.Header().Del("X-Cache")
|
||||
}
|
||||
if this.web.Cache.AddAgeHeader {
|
||||
this.writer.Header().Set("Age", age)
|
||||
@@ -263,8 +317,8 @@ func (this *HTTPRequest) doCacheRead(useStale bool) (shouldStop bool) {
|
||||
var eTag = ""
|
||||
var lastModifiedAt = reader.LastModified()
|
||||
if lastModifiedAt > 0 {
|
||||
if isWebP {
|
||||
eTag = "\"" + strconv.FormatInt(lastModifiedAt, 10) + "_webp" + "\""
|
||||
if len(tags) > 0 {
|
||||
eTag = "\"" + strconv.FormatInt(lastModifiedAt, 10) + "_" + strings.Join(tags, "_") + "\""
|
||||
} else {
|
||||
eTag = "\"" + strconv.FormatInt(lastModifiedAt, 10) + "\""
|
||||
}
|
||||
|
||||
@@ -156,6 +156,9 @@ func httpRequestNextId() string {
|
||||
|
||||
// 检查是否可以接受某个编码
|
||||
func httpAcceptEncoding(acceptEncodings string, encoding string) bool {
|
||||
if len(acceptEncodings) == 0 {
|
||||
return false
|
||||
}
|
||||
var pieces = strings.Split(acceptEncodings, ",")
|
||||
for _, piece := range pieces {
|
||||
var qualityIndex = strings.Index(piece, ";")
|
||||
@@ -169,3 +172,20 @@ func httpAcceptEncoding(acceptEncodings string, encoding string) bool {
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// 分隔编码
|
||||
func httpAcceptEncodings(acceptEncodings string) (encodings []string) {
|
||||
if len(acceptEncodings) == 0 {
|
||||
return
|
||||
}
|
||||
var pieces = strings.Split(acceptEncodings, ",")
|
||||
for _, piece := range pieces {
|
||||
var qualityIndex = strings.Index(piece, ";")
|
||||
if qualityIndex >= 0 {
|
||||
piece = piece[:qualityIndex]
|
||||
}
|
||||
|
||||
encodings = append(encodings, strings.TrimSpace(piece))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@@ -35,11 +35,17 @@ import (
|
||||
)
|
||||
|
||||
// webp相关配置
|
||||
const webpSuffix = "@GOEDGE_WEBP"
|
||||
const webpCacheSuffix = "@GOEDGE_WEBP"
|
||||
|
||||
var webpMaxBufferSize int64 = 1_000_000_000
|
||||
var webpTotalBufferSize int64 = 0
|
||||
|
||||
// 压缩相关配置
|
||||
const compressionCacheSuffix = "@GOEDGE_"
|
||||
|
||||
// 缓存相关配置
|
||||
const cacheMethodSuffix = "@GOEDGE_"
|
||||
|
||||
func init() {
|
||||
var systemMemory = utils.SystemMemoryGB() / 8
|
||||
if systemMemory > 0 {
|
||||
@@ -66,17 +72,24 @@ type HTTPWriter struct {
|
||||
isOk bool // 是否完全成功
|
||||
isFinished bool // 是否已完成
|
||||
|
||||
// Partial
|
||||
isPartial bool
|
||||
|
||||
// WebP
|
||||
webpIsEncoding bool
|
||||
webpOriginContentType string
|
||||
|
||||
// Compression
|
||||
compressionConfig *serverconfigs.HTTPCompressionConfig
|
||||
compressionConfig *serverconfigs.HTTPCompressionConfig
|
||||
compressionCacheWriter caches.Writer
|
||||
|
||||
// Cache
|
||||
cacheStorage caches.StorageInterface
|
||||
cacheWriter caches.Writer
|
||||
cacheIsFinished bool
|
||||
|
||||
cacheReader caches.Reader
|
||||
cacheReaderSuffix string
|
||||
}
|
||||
|
||||
// NewHTTPWriter 包装对象
|
||||
@@ -95,13 +108,22 @@ func (this *HTTPWriter) Prepare(resp *http.Response, size int64, status int, ena
|
||||
this.size = size
|
||||
this.statusCode = status
|
||||
|
||||
if resp != nil {
|
||||
this.isPartial = status == http.StatusPartialContent
|
||||
|
||||
if resp != nil && resp.Body != nil {
|
||||
cacheReader, ok := resp.Body.(caches.Reader)
|
||||
if ok {
|
||||
this.cacheReader = cacheReader
|
||||
}
|
||||
|
||||
this.rawReader = resp.Body
|
||||
|
||||
if enableCache {
|
||||
this.PrepareCache(resp, size)
|
||||
}
|
||||
this.PrepareWebP(resp, size)
|
||||
if !this.isPartial {
|
||||
this.PrepareWebP(resp, size)
|
||||
}
|
||||
this.PrepareCompression(resp, size)
|
||||
}
|
||||
|
||||
@@ -135,7 +157,7 @@ func (this *HTTPWriter) PrepareCache(resp *http.Response, size int64) {
|
||||
var addStatusHeader = this.req.web != nil && this.req.web.Cache != nil && this.req.web.Cache.AddStatusHeader
|
||||
|
||||
// 不支持Range
|
||||
if len(this.Header().Get("Content-Range")) > 0 {
|
||||
if this.StatusCode() == http.StatusPartialContent || len(this.Header().Get("Content-Range")) > 0 {
|
||||
this.req.varMapping["cache.status"] = "BYPASS"
|
||||
if addStatusHeader {
|
||||
this.Header().Set("X-Cache", "BYPASS, not supported Content-Range")
|
||||
@@ -243,7 +265,7 @@ func (this *HTTPWriter) PrepareCache(resp *http.Response, size int64) {
|
||||
|
||||
var expiredAt = utils.UnixTime() + life
|
||||
var cacheKey = this.req.cacheKey
|
||||
cacheWriter, err := storage.OpenWriter(cacheKey, expiredAt, this.StatusCode(), size)
|
||||
cacheWriter, err := storage.OpenWriter(cacheKey, expiredAt, this.StatusCode(), size, false)
|
||||
if err != nil {
|
||||
if !caches.CanIgnoreErr(err) {
|
||||
remotelogs.Error("HTTP_WRITER", "write cache failed: "+err.Error())
|
||||
@@ -302,7 +324,7 @@ func (this *HTTPWriter) PrepareWebP(resp *http.Response, size int64) {
|
||||
return
|
||||
}
|
||||
|
||||
var contentEncoding = resp.Header.Get("Content-Encoding")
|
||||
var contentEncoding = this.Header().Get("Content-Encoding")
|
||||
switch contentEncoding {
|
||||
case "gzip", "deflate", "br":
|
||||
reader, err := compressions.NewReader(resp.Body, contentEncoding)
|
||||
@@ -310,6 +332,7 @@ func (this *HTTPWriter) PrepareWebP(resp *http.Response, size int64) {
|
||||
return
|
||||
}
|
||||
this.Header().Del("Content-Encoding")
|
||||
this.Header().Del("Content-Length")
|
||||
this.rawReader = reader
|
||||
case "": // 空
|
||||
default:
|
||||
@@ -328,6 +351,15 @@ func (this *HTTPWriter) PrepareWebP(resp *http.Response, size int64) {
|
||||
|
||||
// PrepareCompression 准备压缩
|
||||
func (this *HTTPWriter) PrepareCompression(resp *http.Response, size int64) {
|
||||
var method = this.req.Method()
|
||||
if method == http.MethodHead {
|
||||
return
|
||||
}
|
||||
|
||||
if this.StatusCode() == http.StatusNoContent {
|
||||
return
|
||||
}
|
||||
|
||||
var acceptEncodings = this.req.RawReq.Header.Get("Accept-Encoding")
|
||||
var contentEncoding = this.Header().Get("Content-Encoding")
|
||||
|
||||
@@ -388,19 +420,70 @@ func (this *HTTPWriter) PrepareCompression(resp *http.Response, size int64) {
|
||||
resp.Body = reader
|
||||
}
|
||||
|
||||
// 需要放在compression cache writer之前
|
||||
var header = this.rawWriter.Header()
|
||||
header.Set("Content-Encoding", compressionEncoding)
|
||||
header.Set("Vary", "Accept-Encoding")
|
||||
header.Del("Content-Length")
|
||||
|
||||
// compression cache writer
|
||||
if !this.isPartial && this.cacheStorage != nil && (this.cacheReader != nil || this.cacheWriter != nil) && !this.webpIsEncoding {
|
||||
var cacheKey = ""
|
||||
var expiredAt int64 = 0
|
||||
|
||||
if this.cacheReader != nil {
|
||||
cacheKey = this.req.cacheKey
|
||||
expiredAt = this.cacheReader.ExpiresAt()
|
||||
} else if this.cacheWriter != nil {
|
||||
cacheKey = this.cacheWriter.Key()
|
||||
expiredAt = this.cacheWriter.ExpiredAt()
|
||||
}
|
||||
|
||||
if len(this.cacheReaderSuffix) > 0 {
|
||||
cacheKey += this.cacheReaderSuffix
|
||||
}
|
||||
|
||||
compressionCacheWriter, err := this.cacheStorage.OpenWriter(cacheKey+compressionCacheSuffix+compressionEncoding, expiredAt, this.StatusCode(), -1, false)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// 写入Header
|
||||
for k, v := range this.Header() {
|
||||
for _, v1 := range v {
|
||||
_, err = compressionCacheWriter.WriteHeader([]byte(k + ":" + v1 + "\n"))
|
||||
if err != nil {
|
||||
remotelogs.Error("HTTP_WRITER", "write compression cache failed: "+err.Error())
|
||||
_ = compressionCacheWriter.Discard()
|
||||
compressionCacheWriter = nil
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if compressionCacheWriter != nil {
|
||||
this.compressionCacheWriter = compressionCacheWriter
|
||||
var teeWriter = writers.NewTeeWriterCloser(this.writer, compressionCacheWriter)
|
||||
teeWriter.OnFail(func(err error) {
|
||||
_ = compressionCacheWriter.Discard()
|
||||
this.compressionCacheWriter = nil
|
||||
})
|
||||
this.writer = teeWriter
|
||||
}
|
||||
}
|
||||
|
||||
// compression writer
|
||||
var err error = nil
|
||||
compressionWriter, err := compressions.NewWriter(this.writer, compressionType, int(this.compressionConfig.Level))
|
||||
if err != nil {
|
||||
remotelogs.Error("HTTP_WRITER", err.Error())
|
||||
header.Del("Content-Encoding")
|
||||
if this.compressionCacheWriter != nil {
|
||||
_ = this.compressionCacheWriter.Discard()
|
||||
}
|
||||
return
|
||||
}
|
||||
this.writer = compressionWriter
|
||||
|
||||
header := this.rawWriter.Header()
|
||||
header.Set("Content-Encoding", compressionEncoding)
|
||||
header.Set("Vary", "Accept-Encoding")
|
||||
header.Del("Content-Length")
|
||||
}
|
||||
|
||||
// SetCompression 设置内容压缩配置
|
||||
@@ -556,13 +639,26 @@ func (this *HTTPWriter) Close() {
|
||||
var webpCacheWriter caches.Writer
|
||||
|
||||
// 准备WebP Cache
|
||||
if this.cacheWriter != nil {
|
||||
var cacheKey = this.cacheWriter.Key() + webpSuffix
|
||||
if this.cacheReader != nil || this.cacheWriter != nil {
|
||||
var cacheKey = ""
|
||||
var expiredAt int64 = 0
|
||||
|
||||
webpCacheWriter, _ = this.cacheStorage.OpenWriter(cacheKey, this.cacheWriter.ExpiredAt(), this.StatusCode(), -1)
|
||||
if this.cacheReader != nil {
|
||||
cacheKey = this.req.cacheKey + webpCacheSuffix
|
||||
expiredAt = this.cacheReader.ExpiresAt()
|
||||
} else if this.cacheWriter != nil {
|
||||
cacheKey = this.cacheWriter.Key() + webpCacheSuffix
|
||||
expiredAt = this.cacheWriter.ExpiredAt()
|
||||
}
|
||||
|
||||
webpCacheWriter, _ = this.cacheStorage.OpenWriter(cacheKey, expiredAt, this.StatusCode(), -1, false)
|
||||
if webpCacheWriter != nil {
|
||||
// 写入Header
|
||||
for k, v := range this.Header() {
|
||||
// 这里是原始的数据,不需要内容编码
|
||||
if k == "Content-Encoding" || k == "Transfer-Encoding" {
|
||||
continue
|
||||
}
|
||||
for _, v1 := range v {
|
||||
_, err := webpCacheWriter.WriteHeader([]byte(k + ":" + v1 + "\n"))
|
||||
if err != nil {
|
||||
@@ -679,13 +775,16 @@ func (this *HTTPWriter) Close() {
|
||||
// 缓存
|
||||
if this.cacheWriter != nil {
|
||||
if this.isOk && this.cacheIsFinished {
|
||||
// 对比Content-Length
|
||||
var contentLengthString = this.Header().Get("Content-Length")
|
||||
if len(contentLengthString) > 0 {
|
||||
var contentLength = types.Int64(contentLengthString)
|
||||
if contentLength != this.cacheWriter.BodySize() {
|
||||
this.isOk = false
|
||||
_ = this.cacheWriter.Discard()
|
||||
// 对比缓存前后的Content-Length
|
||||
var method = this.req.Method()
|
||||
if method != http.MethodHead && this.StatusCode() != http.StatusNoContent {
|
||||
var contentLengthString = this.Header().Get("Content-Length")
|
||||
if len(contentLengthString) > 0 {
|
||||
var contentLength = types.Int64(contentLengthString)
|
||||
if contentLength != this.cacheWriter.BodySize() {
|
||||
this.isOk = false
|
||||
_ = this.cacheWriter.Discard()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -710,6 +809,27 @@ func (this *HTTPWriter) Close() {
|
||||
}
|
||||
}
|
||||
|
||||
if this.compressionCacheWriter != nil {
|
||||
if this.isOk {
|
||||
err := this.compressionCacheWriter.Close()
|
||||
if err == nil {
|
||||
var expiredAt = this.compressionCacheWriter.ExpiredAt()
|
||||
this.cacheStorage.AddToList(&caches.Item{
|
||||
Type: this.compressionCacheWriter.ItemType(),
|
||||
Key: this.compressionCacheWriter.Key(),
|
||||
ExpiredAt: expiredAt,
|
||||
StaleAt: expiredAt + int64(this.calculateStaleLife()),
|
||||
HeaderSize: this.compressionCacheWriter.HeaderSize(),
|
||||
BodySize: this.compressionCacheWriter.BodySize(),
|
||||
Host: this.req.ReqHost,
|
||||
ServerId: this.req.ReqServer.Id,
|
||||
})
|
||||
}
|
||||
} else {
|
||||
_ = this.compressionCacheWriter.Discard()
|
||||
}
|
||||
}
|
||||
|
||||
this.sentBodyBytes = this.counterWriter.TotalBytes()
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user