mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-09-04 15:17:09 +00:00
* filer: cover the storage rule TTL on the object transaction write path An object written through ObjectTransaction used to land with ttlSec 0 even under an fs.configure TTL rule, while the same object written through CreateEntry got the rule's TTL. Guard the shared stamping so the two paths cannot drift apart again. * filer: apply the path's storage rule to an appended entry AppendToEntry resolved the storage option from the path - so its chunks land on a TTL volume under an fs.configure TTL rule - but never stamped the rule's TTL on the entry it creates, leaving an entry that outlives its data. Route it through applyStorageDefaultsToEntry, which now feeds the entry's own TTL into the option so the placement an existing entry's appended chunks get is unchanged. * filer: apply the path's storage rule to a completed TUS upload The PATCH path resolves the storage option from the target, so a TUS upload into an fs.configure TTL prefix writes its chunks to a TTL volume, but completion built the final entry with ttlSec 0 - the entry outlived the data it pointed at. Stamp it through applyStorageDefaultsToEntry, which also subsumes the hand-rolled read-only check and supplies the rule's name-length limit. * filer: apply the destination's storage option TTL to a copied entry The copy handler re-uploads the source's chunks under the destination's storage option, so a copy into an fs.configure TTL prefix already lands its data on a TTL volume. The entry, though, carried the source's ttlSec - 0 for a source outside the prefix, or the source's own TTL where the two rules differ - so it never expired with the data it pointed at. Take the TTL from the same option the chunks were placed with, after the data-only copy has restored the destination's metadata.
158 lines
5.5 KiB
Go
158 lines
5.5 KiB
Go
package weed_server
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
|
|
"github.com/seaweedfs/seaweedfs/weed/util"
|
|
)
|
|
|
|
// TestFilerServer_completeTusUpload_OverlappingRecords verifies a session whose
|
|
// chunk records overlap - a PATCH retried while its predecessor was still
|
|
// storing a sub-chunk - still completes: coverage is validated the way HEAD
|
|
// computes the offset, and only records extending coverage join the entry.
|
|
func TestFilerServer_completeTusUpload_OverlappingRecords(t *testing.T) {
|
|
fidA, fidB, fidDup := "3,01637037d6", "4,02637037d6", "5,03637037d6"
|
|
|
|
tests := []struct {
|
|
name string
|
|
chunks [][3]int64 // offset, size, fid index into fids
|
|
wantChunks [][2]int64 // offset, size expected on the entry
|
|
}{
|
|
{
|
|
name: "exact duplicate dropped",
|
|
chunks: [][3]int64{{0, 8, 0}, {0, 8, 2}, {8, 4, 1}},
|
|
wantChunks: [][2]int64{{0, 8}, {8, 4}},
|
|
},
|
|
{
|
|
name: "partial overlap kept",
|
|
chunks: [][3]int64{{0, 8, 0}, {6, 6, 2}},
|
|
wantChunks: [][2]int64{{0, 8}, {6, 6}},
|
|
},
|
|
{
|
|
// a covered record naming a file id the entry keeps must not free it
|
|
name: "duplicate sharing a kept file id",
|
|
chunks: [][3]int64{{0, 8, 0}, {0, 4, 0}, {8, 4, 1}},
|
|
wantChunks: [][2]int64{{0, 8}, {8, 4}},
|
|
},
|
|
}
|
|
|
|
fids := []string{fidA, fidB, fidDup}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
fs, store := newTusTestServer(t, nil)
|
|
targetPath := "/buckets/data/raced.bin"
|
|
size := int64(12)
|
|
seedTusSession(t, fs, store, TusSession{ID: tusTestUploadID, TargetPath: targetPath, Size: size})
|
|
for _, c := range tt.chunks {
|
|
seedTusChunk(t, fs, store, tusTestUploadID, c[0], c[1], fids[c[2]])
|
|
}
|
|
|
|
// a zero-length PATCH at the reported offset triggers completion
|
|
req := tusRequest(http.MethodPatch, "/.tus/.uploads/"+tusTestUploadID, map[string]string{
|
|
"Authorization": "Bearer " + signFilerToken(t, tusTestWriteKey, nil, nil),
|
|
"Content-Type": "application/offset+octet-stream",
|
|
"Upload-Offset": "12",
|
|
}, "")
|
|
rec := httptest.NewRecorder()
|
|
fs.tusHandler(rec, req)
|
|
|
|
if rec.Code != http.StatusNoContent {
|
|
t.Fatalf("PATCH = %d, want %d; body=%q", rec.Code, http.StatusNoContent, rec.Body.String())
|
|
}
|
|
entry, err := store.FindEntry(context.Background(), util.FullPath(targetPath))
|
|
if err != nil {
|
|
t.Fatalf("final entry not created: %v", err)
|
|
}
|
|
chunks := entry.GetChunks()
|
|
if len(chunks) != len(tt.wantChunks) {
|
|
t.Fatalf("entry chunks = %d, want %d", len(chunks), len(tt.wantChunks))
|
|
}
|
|
for i, want := range tt.wantChunks {
|
|
if chunks[i].Offset != want[0] || int64(chunks[i].Size) != want[1] {
|
|
t.Errorf("chunk[%d] = @%d+%d, want @%d+%d", i, chunks[i].Offset, chunks[i].Size, want[0], want[1])
|
|
}
|
|
}
|
|
|
|
var freed []string
|
|
fs.filer.FileIdDeletionQueue.Consume(func(fileIds []string) {
|
|
freed = append(freed, fileIds...)
|
|
})
|
|
for _, chunk := range chunks {
|
|
for _, fileId := range freed {
|
|
if fileId == chunk.FileId {
|
|
t.Errorf("file id %s is referenced by the entry but was freed", fileId)
|
|
}
|
|
}
|
|
}
|
|
if _, err := store.FindEntry(context.Background(), util.FullPath(fs.tusSessionInfoPath(tusTestUploadID))); err == nil {
|
|
t.Errorf("session still present after completion")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestFilerServer_completeTusUpload_GapRejected verifies records that do not
|
|
// cover the full size still fail completion.
|
|
func TestFilerServer_completeTusUpload_GapRejected(t *testing.T) {
|
|
fs, store := newTusTestServer(t, nil)
|
|
targetPath := "/buckets/data/gap.bin"
|
|
seedTusSession(t, fs, store, TusSession{ID: tusTestUploadID, TargetPath: targetPath, Size: 12})
|
|
|
|
session := &TusSession{
|
|
ID: tusTestUploadID,
|
|
TargetPath: targetPath,
|
|
Size: 12,
|
|
Offset: 12,
|
|
Chunks: []*TusChunkInfo{
|
|
{Offset: 0, Size: 8, FileId: "3,01637037d6"},
|
|
{Offset: 9, Size: 3, FileId: "4,02637037d6"},
|
|
},
|
|
}
|
|
err := fs.completeTusUpload(context.Background(), session)
|
|
if err == nil || !strings.Contains(err.Error(), "chunk gap") {
|
|
t.Fatalf("completeTusUpload err = %v, want a chunk gap error", err)
|
|
}
|
|
if _, findErr := store.FindEntry(context.Background(), util.FullPath(targetPath)); findErr == nil {
|
|
t.Fatalf("entry created despite gap")
|
|
}
|
|
}
|
|
|
|
// TestFilerServer_completeTusUpload_ReadOnlyTarget verifies a read-only prefix
|
|
// still refuses the completing upload, before the session's chunks are claimed.
|
|
func TestFilerServer_completeTusUpload_ReadOnlyTarget(t *testing.T) {
|
|
fs, store := newTusTestServer(t, nil)
|
|
if err := fs.filer.FilerConf.AddLocationConf(&filer_pb.FilerConf_PathConf{
|
|
LocationPrefix: "/buckets/frozen/",
|
|
ReadOnly: true,
|
|
}); err != nil {
|
|
t.Fatalf("AddLocationConf: %v", err)
|
|
}
|
|
|
|
targetPath := "/buckets/frozen/upload.bin"
|
|
session := &TusSession{
|
|
ID: tusTestUploadID,
|
|
TargetPath: targetPath,
|
|
Size: 8,
|
|
Offset: 8,
|
|
Chunks: []*TusChunkInfo{{Offset: 0, Size: 8, FileId: "3,01637037d6"}},
|
|
}
|
|
seedTusSession(t, fs, store, *session)
|
|
|
|
err := fs.completeTusUpload(context.Background(), session)
|
|
if !errors.Is(err, ErrReadOnly) {
|
|
t.Fatalf("completeTusUpload err = %v, want %v", err, ErrReadOnly)
|
|
}
|
|
if _, findErr := store.FindEntry(context.Background(), util.FullPath(targetPath)); findErr == nil {
|
|
t.Fatalf("entry created under a read-only prefix")
|
|
}
|
|
if consumed, checkErr := fs.isTusSessionConsumed(context.Background(), tusTestUploadID); checkErr != nil || consumed {
|
|
t.Fatalf("session consumed = %v (err %v), want false", consumed, checkErr)
|
|
}
|
|
}
|