Browse Source
Fix live volume move tail timestamp (#8440)
Fix live volume move tail timestamp (#8440)
* Improve move tail timestamp * Add move tail timestamp integration test * Simulate traffic during movepull/8372/merge
committed by
GitHub
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 360 additions and 11 deletions
-
4test/volume_server/framework/cluster_dual.go
-
279test/volume_server/grpc/move_tail_timestamp_test.go
-
68weed/server/volume_grpc_copy.go
-
2weed/shell/command_volume_copy.go
-
18weed/shell/command_volume_move.go
@ -0,0 +1,279 @@ |
|||||
|
package volume_server_grpc_test |
||||
|
|
||||
|
import ( |
||||
|
"bytes" |
||||
|
"context" |
||||
|
"fmt" |
||||
|
"io" |
||||
|
"net/http" |
||||
|
"os" |
||||
|
"path/filepath" |
||||
|
"strings" |
||||
|
"sync" |
||||
|
"testing" |
||||
|
"time" |
||||
|
|
||||
|
"github.com/seaweedfs/seaweedfs/test/volume_server/framework" |
||||
|
"github.com/seaweedfs/seaweedfs/test/volume_server/matrix" |
||||
|
"github.com/seaweedfs/seaweedfs/weed/pb/volume_server_pb" |
||||
|
"github.com/seaweedfs/seaweedfs/weed/storage" |
||||
|
"github.com/seaweedfs/seaweedfs/weed/storage/backend" |
||||
|
"github.com/seaweedfs/seaweedfs/weed/storage/idx" |
||||
|
"github.com/seaweedfs/seaweedfs/weed/storage/needle" |
||||
|
"github.com/seaweedfs/seaweedfs/weed/storage/types" |
||||
|
"github.com/seaweedfs/seaweedfs/weed/util" |
||||
|
) |
||||
|
|
||||
|
func TestVolumeCopyReturnsPreciseLastAppendTimestamp(t *testing.T) { |
||||
|
if testing.Short() { |
||||
|
t.Skip("skipping integration test in short mode") |
||||
|
} |
||||
|
|
||||
|
cluster := framework.StartDualVolumeCluster(t, matrix.P1()) |
||||
|
sourceConn, sourceClient := framework.DialVolumeServer(t, cluster.VolumeGRPCAddress(0)) |
||||
|
defer sourceConn.Close() |
||||
|
destConn, destClient := framework.DialVolumeServer(t, cluster.VolumeGRPCAddress(1)) |
||||
|
defer destConn.Close() |
||||
|
|
||||
|
const volumeID = uint32(999) |
||||
|
framework.AllocateVolume(t, sourceClient, volumeID, "") |
||||
|
|
||||
|
httpClient := framework.NewHTTPClient() |
||||
|
fid := framework.NewFileID(volumeID, 1, 0x42) |
||||
|
payload := []byte("move-tail-timestamp-payload") |
||||
|
uploadResp := framework.UploadBytes(t, httpClient, cluster.VolumeAdminURL(0), fid, payload) |
||||
|
_ = framework.ReadAllAndClose(t, uploadResp) |
||||
|
if uploadResp.StatusCode != http.StatusCreated { |
||||
|
t.Fatalf("source upload failed: status %d", uploadResp.StatusCode) |
||||
|
} |
||||
|
|
||||
|
sourceDir := filepath.Join(cluster.BaseDir(), "volume0") |
||||
|
datPath := storage.VolumeFileName(sourceDir, "", int(volumeID)) + ".dat" |
||||
|
futureTime := time.Now().Add(2 * time.Hour) |
||||
|
if err := os.Chtimes(datPath, futureTime, futureTime); err != nil { |
||||
|
t.Fatalf("set future dat timestamp: %v", err) |
||||
|
} |
||||
|
|
||||
|
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) |
||||
|
defer cancel() |
||||
|
|
||||
|
sourceDataNode := cluster.VolumeAdminAddress(0) + "." + strings.Split(cluster.VolumeGRPCAddress(0), ":")[1] |
||||
|
copyStream, err := destClient.VolumeCopy(ctx, &volume_server_pb.VolumeCopyRequest{ |
||||
|
VolumeId: volumeID, |
||||
|
SourceDataNode: sourceDataNode, |
||||
|
}) |
||||
|
if err != nil { |
||||
|
t.Fatalf("VolumeCopy start failed: %v", err) |
||||
|
} |
||||
|
|
||||
|
var lastAppendAtNs uint64 |
||||
|
for { |
||||
|
resp, recvErr := copyStream.Recv() |
||||
|
if recvErr == io.EOF { |
||||
|
break |
||||
|
} |
||||
|
if recvErr != nil { |
||||
|
t.Fatalf("VolumeCopy recv failed: %v", recvErr) |
||||
|
} |
||||
|
if ts := resp.GetLastAppendAtNs(); ts > 0 { |
||||
|
lastAppendAtNs = ts |
||||
|
} |
||||
|
} |
||||
|
if lastAppendAtNs == 0 { |
||||
|
t.Fatalf("volume copy did not return a last append timestamp") |
||||
|
} |
||||
|
|
||||
|
destDir := filepath.Join(cluster.BaseDir(), "volume1") |
||||
|
actualLastAppend := readLastAppendAtNs(t, destDir, volumeID) |
||||
|
if actualLastAppend == 0 { |
||||
|
t.Fatalf("failed to compute last append timestamp from destination files") |
||||
|
} |
||||
|
|
||||
|
if lastAppendAtNs != actualLastAppend { |
||||
|
t.Fatalf("last append timestamp mismatch: got %d, actual %d", lastAppendAtNs, actualLastAppend) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func readLastAppendAtNs(t testing.TB, volumeDir string, volumeID uint32) uint64 { |
||||
|
t.Helper() |
||||
|
|
||||
|
baseName := storage.VolumeFileName(volumeDir, "", int(volumeID)) |
||||
|
idxPath := baseName + ".idx" |
||||
|
idxFile, err := os.Open(idxPath) |
||||
|
if err != nil { |
||||
|
t.Fatalf("open idx file %s: %v", idxPath, err) |
||||
|
} |
||||
|
defer idxFile.Close() |
||||
|
|
||||
|
stat, err := idxFile.Stat() |
||||
|
if err != nil { |
||||
|
t.Fatalf("stat idx file %s: %v", idxPath, err) |
||||
|
} |
||||
|
if stat.Size() == 0 { |
||||
|
return 0 |
||||
|
} |
||||
|
if stat.Size()%int64(types.NeedleMapEntrySize) != 0 { |
||||
|
t.Fatalf("unexpected idx file size %d", stat.Size()) |
||||
|
} |
||||
|
|
||||
|
buf := make([]byte, types.NeedleMapEntrySize) |
||||
|
if _, err := idxFile.ReadAt(buf, stat.Size()-int64(types.NeedleMapEntrySize)); err != nil { |
||||
|
t.Fatalf("read idx entry: %v", err) |
||||
|
} |
||||
|
_, offset, _ := idx.IdxFileEntry(buf) |
||||
|
if offset.IsZero() { |
||||
|
return 0 |
||||
|
} |
||||
|
|
||||
|
datPath := baseName + ".dat" |
||||
|
datFile, err := os.Open(datPath) |
||||
|
if err != nil { |
||||
|
t.Fatalf("open dat file %s: %v", datPath, err) |
||||
|
} |
||||
|
defer datFile.Close() |
||||
|
|
||||
|
datBackend := backend.NewDiskFile(datFile) |
||||
|
n, _, _, err := needle.ReadNeedleHeader(datBackend, needle.GetCurrentVersion(), offset.ToActualOffset()) |
||||
|
if err != nil { |
||||
|
t.Fatalf("read needle header: %v", err) |
||||
|
} |
||||
|
|
||||
|
tailOffset := offset.ToActualOffset() + int64(types.NeedleHeaderSize) + int64(n.Size) |
||||
|
tail := make([]byte, needle.NeedleChecksumSize+types.TimestampSize) |
||||
|
readCount, readErr := datBackend.ReadAt(tail, tailOffset) |
||||
|
if readErr == io.EOF && readCount == len(tail) { |
||||
|
readErr = nil |
||||
|
} |
||||
|
if readErr != nil { |
||||
|
t.Fatalf("read needle tail: %v", readErr) |
||||
|
} |
||||
|
|
||||
|
return util.BytesToUint64(tail[needle.NeedleChecksumSize : needle.NeedleChecksumSize+types.TimestampSize]) |
||||
|
} |
||||
|
|
||||
|
func TestVolumeMoveHandlesInFlightWrites(t *testing.T) { |
||||
|
if testing.Short() { |
||||
|
t.Skip("skipping integration test in short mode") |
||||
|
} |
||||
|
|
||||
|
cluster := framework.StartDualVolumeCluster(t, matrix.P1()) |
||||
|
sourceConn, sourceClient := framework.DialVolumeServer(t, cluster.VolumeGRPCAddress(0)) |
||||
|
defer sourceConn.Close() |
||||
|
destConn, destClient := framework.DialVolumeServer(t, cluster.VolumeGRPCAddress(1)) |
||||
|
defer destConn.Close() |
||||
|
|
||||
|
const volumeID = uint32(988) |
||||
|
framework.AllocateVolume(t, sourceClient, volumeID, "") |
||||
|
|
||||
|
httpClient := framework.NewHTTPClient() |
||||
|
fid := framework.NewFileID(volumeID, 7, 0xABCDEF01) |
||||
|
payload := []byte("volume-move-live-payload") |
||||
|
uploadResp := framework.UploadBytes(t, httpClient, cluster.VolumeAdminURL(0), fid, payload) |
||||
|
_ = framework.ReadAllAndClose(t, uploadResp) |
||||
|
if uploadResp.StatusCode != http.StatusCreated { |
||||
|
t.Fatalf("initial upload failed: %d", uploadResp.StatusCode) |
||||
|
} |
||||
|
|
||||
|
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) |
||||
|
defer cancel() |
||||
|
|
||||
|
sourceDataNode := cluster.VolumeAdminAddress(0) + "." + strings.Split(cluster.VolumeGRPCAddress(0), ":")[1] |
||||
|
copyStream, err := destClient.VolumeCopy(ctx, &volume_server_pb.VolumeCopyRequest{ |
||||
|
VolumeId: volumeID, |
||||
|
SourceDataNode: sourceDataNode, |
||||
|
}) |
||||
|
if err != nil { |
||||
|
t.Fatalf("VolumeCopy start failed: %v", err) |
||||
|
} |
||||
|
|
||||
|
var lastAppendAtNs uint64 |
||||
|
for { |
||||
|
resp, recvErr := copyStream.Recv() |
||||
|
if recvErr == io.EOF { |
||||
|
break |
||||
|
} |
||||
|
if recvErr != nil { |
||||
|
t.Fatalf("VolumeCopy recv failed: %v", recvErr) |
||||
|
} |
||||
|
if ts := resp.GetLastAppendAtNs(); ts > 0 { |
||||
|
lastAppendAtNs = ts |
||||
|
} |
||||
|
} |
||||
|
if lastAppendAtNs == 0 { |
||||
|
t.Fatalf("volume copy did not return a last append timestamp") |
||||
|
} |
||||
|
|
||||
|
type written struct { |
||||
|
fid string |
||||
|
data []byte |
||||
|
} |
||||
|
|
||||
|
var writesMu sync.Mutex |
||||
|
var writes []written |
||||
|
writeCtx, writeCancel := context.WithCancel(context.Background()) |
||||
|
var writerWG sync.WaitGroup |
||||
|
writerWG.Add(1) |
||||
|
go func() { |
||||
|
defer writerWG.Done() |
||||
|
client := framework.NewHTTPClient() |
||||
|
for i := 0; i < 12; i++ { |
||||
|
select { |
||||
|
case <-writeCtx.Done(): |
||||
|
return |
||||
|
default: |
||||
|
} |
||||
|
livePayload := []byte("live-data-" + fmt.Sprintf("%02d", i)) |
||||
|
liveFid := framework.NewFileID(volumeID, uint64(2000+i), 0xAAAA1111+uint32(i)) |
||||
|
resp := framework.UploadBytes(t, client, cluster.VolumeAdminURL(0), liveFid, livePayload) |
||||
|
_ = framework.ReadAllAndClose(t, resp) |
||||
|
if resp.StatusCode != http.StatusCreated { |
||||
|
t.Fatalf("live upload failed: %d", resp.StatusCode) |
||||
|
} |
||||
|
writesMu.Lock() |
||||
|
writes = append(writes, written{fid: liveFid, data: livePayload}) |
||||
|
writesMu.Unlock() |
||||
|
time.Sleep(250 * time.Millisecond) |
||||
|
} |
||||
|
}() |
||||
|
|
||||
|
tailCtx, tailCancel := context.WithTimeout(context.Background(), 60*time.Second) |
||||
|
defer tailCancel() |
||||
|
_, err = destClient.VolumeTailReceiver(tailCtx, &volume_server_pb.VolumeTailReceiverRequest{ |
||||
|
VolumeId: volumeID, |
||||
|
SourceVolumeServer: sourceDataNode, |
||||
|
SinceNs: lastAppendAtNs, |
||||
|
IdleTimeoutSeconds: 3, |
||||
|
}) |
||||
|
if err != nil { |
||||
|
writeCancel() |
||||
|
writerWG.Wait() |
||||
|
t.Fatalf("VolumeTailReceiver failed: %v", err) |
||||
|
} |
||||
|
|
||||
|
writeCancel() |
||||
|
writerWG.Wait() |
||||
|
|
||||
|
writesMu.Lock() |
||||
|
sampleCount := len(writes) |
||||
|
if sampleCount == 0 { |
||||
|
writesMu.Unlock() |
||||
|
t.Fatal("no live writes captured") |
||||
|
} |
||||
|
sample := writes |
||||
|
if sampleCount > 3 { |
||||
|
sample = writes[sampleCount-3:] |
||||
|
} |
||||
|
writesMu.Unlock() |
||||
|
|
||||
|
httpCheckClient := framework.NewHTTPClient() |
||||
|
for _, w := range sample { |
||||
|
resp := framework.ReadBytes(t, httpCheckClient, cluster.VolumeAdminURL(1), w.fid) |
||||
|
body := framework.ReadAllAndClose(t, resp) |
||||
|
if resp.StatusCode != http.StatusOK { |
||||
|
t.Fatalf("dest read %s status %d", w.fid, resp.StatusCode) |
||||
|
} |
||||
|
if !bytes.Equal(body, w.data) { |
||||
|
t.Fatalf("dest read body mismatch for %s: %q vs %q", w.fid, string(body), string(w.data)) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
Write
Preview
Loading…
Cancel
Save
Reference in new issue