Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion api/defined/v1/storage/indexdb.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import (
var ErrKeyNotFound = errors.New("key not found")

const (
TypeInMemory = "inmemory"
TypeInMemory = "memory"
TypeNormal = "normal" // normal, warm 同一个
TypeWarm = "warm" // normal, warm 同一个
TypeCold = "cold"
Expand Down
45 changes: 45 additions & 0 deletions internal/protocol/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
## custom internal protocol defined

### ProtocolRequestIDKey

Set request id

### X-FS-Mem

force store in memory

### ProtocolCacheStatusKey

Response cache status with name, like X-Cache: HIT from memory

### PrefetchCacheKey

Prefetch cache, value 1 mean prefetch, 0 mean not prefetch

### CacheTime

Set force `Cache-Control` Cache time, value is seconds, like `CacheTime: 60` mean `Cache-Control: max-age=60`
Comment on lines +19 to +21
Copy link

Copilot AI Feb 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The README examples use CacheTime: 60, but the actual header key is X-CacheTime (protocol.CacheTime). Please update the example to use the real header name so operators can copy/paste it correctly.

Suggested change
### CacheTime
Set force `Cache-Control` Cache time, value is seconds, like `CacheTime: 60` mean `Cache-Control: max-age=60`
### X-CacheTime
Set force `Cache-Control` Cache time, value is seconds, like `X-CacheTime: 60` mean `Cache-Control: max-age=60`

Copilot uses AI. Check for mistakes.

### InternalTraceKey

Internal trace key, value is 1 or 0, 1 mean enable trace, 0 mean disable trace

### InternalStoreUrl

Set force store-url, value is string, like `http://example.com/somepath/app.apk`

### InternalSwapfile

Show response swapfile info, debug now!

### InternalFillRangePercent

Set fill range percent, value is int, like `InternalFillRangePercent: 50` mean fill 50% of response
Copy link

Copilot AI Feb 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For InternalFillRangePercent, the documentation example shows InternalFillRangePercent: 50, but the actual wire header is i-x-fp (protocol.InternalFillRangePercent). The example should use the real header name/value format to avoid misconfiguration.

Suggested change
Set fill range percent, value is int, like `InternalFillRangePercent: 50` mean fill 50% of response
Set fill range percent, value is int, like `i-x-fp: 50` mean fill 50% of response

Copilot uses AI. Check for mistakes.

### InternalCacheErrCode

Set force cache error code, value is int, like `InternalCacheErrCode: 1` mean force cache errcode like > 400

### InternalUpstreamAddr

Dynamic set upstream addr, value is string, like `InternalUpstreamAddr: [IP_ADDRESS]`
12 changes: 7 additions & 5 deletions internal/constants/global.go → internal/protocol/protocol.go
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
package constants
package protocol

const AppName = "tavern"

// define gw->backend Protocol constants
const (
ProtocolRequestIDKey = "X-Request-ID"
ProtocolCacheStatusKey = "X-Cache"
PrefetchCacheKey = "X-Prefetch"
CacheTime = "X-CacheTime"
ProtocolRequestIDKey = "X-Request-ID"
ProtocolCacheStatusKey = "X-Cache"
ProtocolForceStoreMemory = "X-FS-Mem"

PrefetchCacheKey = "X-Prefetch"
CacheTime = "X-CacheTime"

InternalTraceKey = "i-xtrace"
InternalStoreUrl = "i-x-store-url"
Expand Down
4 changes: 2 additions & 2 deletions metrics/request_info.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import (
"time"

"github.com/omalloc/tavern/contrib/log"
"github.com/omalloc/tavern/internal/constants"
"github.com/omalloc/tavern/internal/protocol"
)

type requestMetricKey struct{}
Expand Down Expand Up @@ -53,7 +53,7 @@ func newContext(ctx context.Context, metric *RequestMetric) context.Context {
}

func MustParseRequestID(h http.Header) string {
id := h.Get(constants.ProtocolRequestIDKey)
id := h.Get(protocol.ProtocolRequestIDKey)
// protocol request id header not found, generate a new one
if id == "" {
return generateRequestID()
Expand Down
4 changes: 2 additions & 2 deletions pkg/e2e/e2e.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import (

"github.com/stretchr/testify/assert"

"github.com/omalloc/tavern/internal/constants"
"github.com/omalloc/tavern/internal/protocol"
)

var (
Expand Down Expand Up @@ -92,7 +92,7 @@ func (e *E2E) Do(rewrite func(r *http.Request)) (*http.Response, error) {

method := e.req.Method

e.req.Header.Set(constants.InternalUpstreamAddr, e.ts.Listener.Addr().String())
e.req.Header.Set(protocol.InternalUpstreamAddr, e.ts.Listener.Addr().String())

if dumpReq.Load() && method != "PURGE" {
DumpReq(e.req)
Expand Down
4 changes: 2 additions & 2 deletions plugin/purge/purge.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import (
configv1 "github.com/omalloc/tavern/api/defined/v1/plugin"
storagev1 "github.com/omalloc/tavern/api/defined/v1/storage"
"github.com/omalloc/tavern/contrib/log"
"github.com/omalloc/tavern/internal/constants"
"github.com/omalloc/tavern/internal/protocol"
"github.com/omalloc/tavern/pkg/encoding"
"github.com/omalloc/tavern/plugin"
"github.com/omalloc/tavern/storage"
Expand Down Expand Up @@ -94,7 +94,7 @@ func (r *PurgePlugin) HandleFunc(next http.HandlerFunc) http.HandlerFunc {
return
}

storeUrl := req.Header.Get(constants.InternalStoreUrl)
storeUrl := req.Header.Get(protocol.InternalStoreUrl)
if storeUrl == "" {
storeUrl = req.URL.String()
}
Expand Down
4 changes: 2 additions & 2 deletions plugin/registry.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import (

configv1 "github.com/omalloc/tavern/api/defined/v1/plugin"
"github.com/omalloc/tavern/contrib/log"
"github.com/omalloc/tavern/internal/constants"
"github.com/omalloc/tavern/internal/protocol"
)

type Registry interface {
Expand Down Expand Up @@ -49,5 +49,5 @@ func NewRegistry() Registry {
}

func fmtName(name string) string {
return strings.ToLower(fmt.Sprintf("%s.plugin.%s", constants.AppName, name))
return strings.ToLower(fmt.Sprintf("%s.plugin.%s", protocol.AppName, name))
}
12 changes: 6 additions & 6 deletions server/middleware/caching/caching.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import (
"github.com/omalloc/tavern/api/defined/v1/storage"
"github.com/omalloc/tavern/api/defined/v1/storage/object"
"github.com/omalloc/tavern/contrib/log"
"github.com/omalloc/tavern/internal/constants"
"github.com/omalloc/tavern/internal/protocol"
"github.com/omalloc/tavern/pkg/iobuf"
"github.com/omalloc/tavern/pkg/iobuf/ioindexes"
xhttp "github.com/omalloc/tavern/pkg/x/http"
Expand Down Expand Up @@ -140,7 +140,7 @@ func Middleware(c *configv1.Middleware) (middleware.Middleware, func(), error) {

if resp != nil {
// set cache-staus header BYPASS
resp.Header.Set(constants.ProtocolCacheStatusKey, BYPASS)
resp.Header.Set(protocol.ProtocolCacheStatusKey, BYPASS)
}
return
}
Expand Down Expand Up @@ -260,7 +260,7 @@ func (c *Caching) getUpstreamReader(fromByte, toByte uint64, async bool) (io.Rea
// req.Header.Set("X-Request-ID", fmt.Sprintf("%s-%d", req.Header.Get(appctx.ProtocolRequestIDKey), fromByte)) // 附加 Request-ID suffix

// remove all internal header
req.Header.Del(constants.ProtocolCacheStatusKey)
req.Header.Del(protocol.ProtocolCacheStatusKey)

doSubRequest := func() (*http.Response, error) {
now := time.Now()
Expand Down Expand Up @@ -366,7 +366,7 @@ func (c *Caching) doProxy(req *http.Request, subRequest bool) (*http.Response, e
}

// parsed cache-control header
expiredAt, cacheable := xhttp.ParseCacheTime(constants.CacheTime, resp.Header)
expiredAt, cacheable := xhttp.ParseCacheTime(protocol.CacheTime, resp.Header)

// expire time
c.md.ExpiresAt = now.Add(expiredAt).Unix()
Expand All @@ -391,7 +391,7 @@ func (c *Caching) doProxy(req *http.Request, subRequest bool) (*http.Response, e

// Caching is disabled
// restoring the default behavior for error codes.
if resp.Header.Get(constants.InternalCacheErrCode) != constants.FlagOn {
if resp.Header.Get(protocol.InternalCacheErrCode) != protocol.FlagOn {
c.cacheable = false

copiedHeaders := make(http.Header)
Expand Down Expand Up @@ -469,7 +469,7 @@ func (c *Caching) flushbufferSlice(respRange xhttp.ContentRange) (iobuf.EventSuc

// trigger file crc check
// has InMemory store type skip crc check
if c.bucket.StoreType() == storage.TypeInMemory {
if c.bucket.Type() == storage.TypeInMemory {
return
}

Expand Down
4 changes: 2 additions & 2 deletions server/middleware/caching/caching_fillrange.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import (
"net/http"
"strconv"

"github.com/omalloc/tavern/internal/constants"
"github.com/omalloc/tavern/internal/protocol"
"github.com/omalloc/tavern/pkg/iobuf"
xhttp "github.com/omalloc/tavern/pkg/x/http"
)
Expand Down Expand Up @@ -189,7 +189,7 @@ func (f *fillRange) fill(c *Caching, req *http.Request, rawRange string) *http.R
}

func parseFillPercent(h http.Header, def uint64) uint64 {
fp := h.Get(constants.InternalFillRangePercent)
fp := h.Get(protocol.InternalFillRangePercent)
if fp != "" {
p, err := strconv.ParseUint(fp, 10, 64)
if err != nil {
Expand Down
6 changes: 3 additions & 3 deletions server/middleware/caching/caching_prefetch.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import (
"net/http"
"strconv"

"github.com/omalloc/tavern/internal/constants"
"github.com/omalloc/tavern/internal/protocol"
"github.com/omalloc/tavern/pkg/iobuf"
xhttp "github.com/omalloc/tavern/pkg/x/http"
)
Expand All @@ -33,13 +33,13 @@ func (r *PrefetchProcessor) Lookup(c *Caching, req *http.Request) (bool, error)
}

func (r *PrefetchProcessor) PreRequest(c *Caching, req *http.Request) (*http.Request, error) {
if key := req.Header.Get(constants.PrefetchCacheKey); key != "" {
if key := req.Header.Get(protocol.PrefetchCacheKey); key != "" {
if rawRange := req.Header.Get("Range"); rawRange != "" {
req.Header.Del("Range")
req = req.WithContext(context.WithValue(req.Context(), prefetchRangeKey{}, rawRange))
}
c.prefetch = true
req.Header.Del(constants.PrefetchCacheKey)
req.Header.Del(protocol.PrefetchCacheKey)
c.log.Debugf("prefetch request: %s %s", req.Method, req.URL.String())
}
return req, nil
Expand Down
12 changes: 6 additions & 6 deletions server/middleware/caching/internal.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import (
"github.com/omalloc/tavern/api/defined/v1/storage"
"github.com/omalloc/tavern/api/defined/v1/storage/object"
"github.com/omalloc/tavern/contrib/log"
"github.com/omalloc/tavern/internal/constants"
"github.com/omalloc/tavern/internal/protocol"
"github.com/omalloc/tavern/metrics"
"github.com/omalloc/tavern/pkg/iobuf"
xhttp "github.com/omalloc/tavern/pkg/x/http"
Expand Down Expand Up @@ -92,15 +92,15 @@ func (c *Caching) setXCache(resp *http.Response) {
return
}

resp.Header.Set(constants.ProtocolCacheStatusKey, strings.Join([]string{c.cacheStatus.String(), "from", c.opt.Hostname, "(tavern/4.0)"}, " "))
resp.Header.Set(protocol.ProtocolCacheStatusKey, strings.Join([]string{c.cacheStatus.String(), "from", c.opt.Hostname, c.bucket.StoreType(), "(tavern/4.0)"}, " "))
Copy link

Copilot AI Feb 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The X-Cache value now appends c.bucket.StoreType(). Per the storage.Bucket interface/implementations, StoreType() is the layer (warm/hot/cold/…), while Type() is the backend driver (native/memory/…). If the intent is to produce values like "HIT … from memory", this should likely use c.bucket.Type() (or adjust the wording) to avoid emitting "from warm" for an in-memory backend.

Suggested change
resp.Header.Set(protocol.ProtocolCacheStatusKey, strings.Join([]string{c.cacheStatus.String(), "from", c.opt.Hostname, c.bucket.StoreType(), "(tavern/4.0)"}, " "))
resp.Header.Set(protocol.ProtocolCacheStatusKey, strings.Join([]string{c.cacheStatus.String(), "from", c.opt.Hostname, c.bucket.Type(), "(tavern/4.0)"}, " "))

Copilot uses AI. Check for mistakes.

metric := metrics.FromContext(c.req.Context())
metric.CacheStatus = c.cacheStatus.String()

// debug header
if trace := c.req.Header.Get(constants.InternalTraceKey); trace != "" {
resp.Header.Set(constants.InternalStoreUrl, strconv.FormatInt(int64(c.cacheStatus), 10))
resp.Header.Set(constants.InternalSwapfile, c.id.WPath(c.bucket.Path()))
if trace := c.req.Header.Get(protocol.InternalTraceKey); trace != "" {
resp.Header.Set(protocol.InternalStoreUrl, strconv.FormatInt(int64(c.cacheStatus), 10))
resp.Header.Set(protocol.InternalSwapfile, c.id.WPath(c.bucket.Path()))
}
}

Expand Down Expand Up @@ -302,7 +302,7 @@ func cloneRequest(req *http.Request) *http.Request {
xhttp.RemoveHopByHopHeaders(proxyReq.Header)

// custom upstream addr
if upsAddr := req.Header.Get(constants.InternalUpstreamAddr); upsAddr != "" {
if upsAddr := req.Header.Get(protocol.InternalUpstreamAddr); upsAddr != "" {
proxyReq = proxyReq.WithContext(
selector.NewPeerContext(context.Background(), selector.NewPeer(selector.NewNode("http", upsAddr, map[string]string{}))),
)
Expand Down
5 changes: 3 additions & 2 deletions storage/bucket/disk/disk.go
Original file line number Diff line number Diff line change
Expand Up @@ -372,7 +372,7 @@ func (d *diskBucket) Store(ctx context.Context, meta *object.Metadata) error {
return nil
}

func (d *diskBucket) touch(ctx context.Context, id *object.ID) {
func (d *diskBucket) touch(_ context.Context, id *object.ID) {
mark := d.cache.Get(id.Hash())
if mark == nil {
return
Expand All @@ -397,7 +397,8 @@ func (d *diskBucket) touch(ctx context.Context, id *object.ID) {
d.promMu.Unlock()

d.hkPromote.Add(id.Bytes())
if d.hkPromote.Query(id.Bytes()) >= uint32(d.opt.Migration.Promote.MinHits) {
hits := d.hkPromote.Query(id.Bytes())
if hits >= uint32(d.opt.Migration.Promote.MinHits) {
go func() {
// check migration interface
if d.migration != nil {
Expand Down
4 changes: 2 additions & 2 deletions storage/bucket/disk/disk_migration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ func TestMigration_Promote(t *testing.T) {
DBPath: filepath.Join(basepath, ".indexdb"),
DBType: "pebble",
Driver: "native",
Type: "normal",
Type: storage.TypeWarm,
Migration: &storage.MigrationConfig{
Enabled: true,
Promote: storage.PromoteConfig{
Expand Down Expand Up @@ -97,7 +97,7 @@ func TestMigration_Demote(t *testing.T) {
DBPath: filepath.Join(basepath, ".indexdb"),
DBType: "pebble",
Driver: "native",
Type: "normal",
Type: storage.TypeWarm,
Migration: &storage.MigrationConfig{
Enabled: true,
Demote: storage.DemoteConfig{
Expand Down
2 changes: 1 addition & 1 deletion storage/bucket/disk/disk_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ func newTestBucket(t *testing.T, basepath string) storagev1.Bucket {
bucket, err := disk.New(&storagev1.BucketConfig{
Path: basepath,
Driver: "native",
Type: "normal",
Type: storagev1.TypeWarm,
DBType: "pebble",
DBPath: path.Join(basepath, ".indexdb"),
AsyncLoad: false,
Expand Down
4 changes: 2 additions & 2 deletions storage/bucket/memory/memory.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,9 @@ func New(opt *storage.BucketConfig, sharedkv storage.SharedKV) (storage.Bucket,
mb := &memoryBucket{
fs: vfs.NewMem(),
path: "/",
driver: opt.Driver,
driver: opt.Driver, // storage.TypeInMemory
dbPath: storage.TypeInMemory,
storeType: storage.TypeInMemory,
storeType: opt.Type,
weight: 100, // default weight
Comment on lines 46 to 52
Copy link

Copilot AI Feb 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

memoryBucket.storeType is now initialized from opt.Type, which can be empty (e.g. callers of memory.New that don’t set Type) or defaulted to warm by mergeConfig. That means StoreType() may be ""/"warm" for an in-memory bucket, and code that relies on StoreType()==storage.TypeInMemory (e.g. storage layer classification / single-inmemory enforcement) will no longer recognize it as an in-memory bucket. Consider defaulting storeType to storage.TypeInMemory (or setting it based on opt.Driver=="memory" when opt.Type is empty) so in-memory buckets report a stable store-type.

Copilot uses AI. Check for mistakes.
sharedkv: sharedkv,
cache: lru.New[object.IDHash, storage.Mark](opt.MaxObjectLimit), // in-memory object size
Expand Down
6 changes: 5 additions & 1 deletion storage/builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ type globalBucketOption struct {
var bucketMap = map[string]func(opt *storage.BucketConfig, sharedkv storage.SharedKV) (storage.Bucket, error){
"empty": empty.New,
"native": disk.New, // disk is an alias of native
"memory": memory.New, // in-memory disk. restart as lost.
"memory": memory.New, // in-memory disk. restart as lost. @ storage.TypeInMemory
}

func NewBucket(opt *storage.BucketConfig, sharedkv storage.SharedKV) (storage.Bucket, error) {
Expand Down Expand Up @@ -57,6 +57,10 @@ func mergeConfig(global *globalBucketOption, bucket *conf.Bucket) *storage.Bucke
if copied.Type == "" {
copied.Type = storage.TypeWarm
}
// replace normal to warm
if copied.Type == storage.TypeNormal {
copied.Type = storage.TypeWarm
}
if copied.DBType == "" {
copied.DBType = global.DBType
}
Expand Down
Loading
Loading