mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-08-16 20:26:45 +00:00
* operation: add shared volume_move package for volume and EC shard moves The shell commands (volume.move, volume.balance, ec.balance, tier moves) and the maintenance workers (balance, ec_balance) each carried their own copy of the move RPC sequences, and the copies had drifted: the worker verified the target before deleting the source but dropped the disk type and IO throttle; the shell passed those but deleted the source unverified. volume_move.Mover carries the merged sequences, keeping the stricter behavior from each side: - LiveMoveVolume: check-then-hard-freeze the source (VolumeStatus's IsReadOnly also covers low-disk and readonly-but-can-delete states, which still accept needle deletes), copy with disk type and IO throttle, tail, verify the target is not behind the source before the destructive source delete (a target that is ahead holds writes it accepted during the tail and the move commits to keep them), and restore the source's writability when a failure precedes the delete and this move did the freezing. Aborts clean up the incomplete target copy; a failed cleanup or an ambiguous source delete keeps the source readonly (ErrSourceKeptReadonly) so callers do not thaw a source next to a possibly-authoritative copy. With a readonly source, an existing or unknown-state target refuses the move outright: no client-side observation can prove such a copy is a stale remnant rather than the authoritative copy of an unfinished move. - MoveEcShards: copy with the .ecx/.ecj/.vif/.ecsum sidecars, mount, verify the target registered every shard before unmount+delete on the source, and reject same-server moves (the EC delete is server-wide). Server identity is the grpc endpoint (SameServer), so node:8080 and node:8080.18080 compare equal while test servers sharing a degenerate HTTP address stay distinct; addresses are validated non-fatally before dialing and before being embedded in copy/tail requests, since both the client dialer and the receiving server normalize them through a parser that aborts the process on a malformed port. The Rust volume server's codes.NotFound counts as a definitively absent probe answer alongside the Go server's plain-error code Unknown. All RPCs go through an injectable ClientFunc, so the sequences are unit tested against a fake volume server client: RPC order, request fields, and that verification failures keep the source intact. * shell, worker: delegate volume and EC shard moves to operation/volume_move LiveMoveVolume and the copy/tail/delete/mark-writable helpers become thin wrappers over the shared mover, keeping their signatures; the EC helpers keep their per-step output and delegate the RPCs. BalanceTask and ECBalanceTask keep their parameter validation, progress reporting, and guards (same-node cross-disk rejection, dedup keep-node verification, shard ids range-checked before the uint8 narrowing) and hand the RPC sequences to the mover. volume.tier.move skips its thaw-on-failure when the mover deliberately kept the source readonly, since reopening the replicas beside a possibly-authoritative target copy would fork the volume. The tail-failure tolerance moves inside the mover: a failed tail is tolerated only when the volume was already readonly before the move began, backstopped by a stability re-read across the idle window, so volume.balance's -skipTailError-by-readonly heuristic and tier-move's unconditional skip both become the same authoritative rule. * volume_move: keep the source readonly when a failed copy leaves a target of unknown origin A failed copy can leave a complete, mounted copy on the target (the server finishes after the client loses the stream). The abort probed the target only when its pre-copy state was known-absent; an unknown prior state skipped both the probe and the cleanup and then reopened the source - two writable replicas of one volume, diverging from the next write on. The abort now probes the target on every failed copy and restores the source only when the target provably holds nothing. A copy whose provenance cannot be proven (unknown prior state, a pre-existing replica, or an unreachable target) is never deleted, and the source stays readonly with ErrSourceKeptReadonly naming the recovery. * test: teach the plugin worker harness the shared move sequence The fake volume server lacked VolumeStatus, which the shared mover now issues before freezing the source, and the batch execution test's status-read accounting predates the pre-copy target probe and the verification reads. Mirrors the harness the enterprise tree already carries.
552 lines
16 KiB
Go
552 lines
16 KiB
Go
package pluginworkers
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"net"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/seaweedfs/seaweedfs/weed/operation"
|
|
"github.com/seaweedfs/seaweedfs/weed/pb"
|
|
"github.com/seaweedfs/seaweedfs/weed/pb/volume_server_pb"
|
|
"google.golang.org/grpc"
|
|
"google.golang.org/grpc/credentials/insecure"
|
|
)
|
|
|
|
// VolumeServer provides a minimal volume server for erasure coding tests.
|
|
type VolumeServer struct {
|
|
volume_server_pb.UnimplementedVolumeServerServer
|
|
|
|
t *testing.T
|
|
|
|
server *grpc.Server
|
|
listener net.Listener
|
|
address string
|
|
baseDir string
|
|
|
|
mu sync.Mutex
|
|
receivedFiles map[string]uint64
|
|
readonlyVolumes map[uint32]bool
|
|
mountRequests []*volume_server_pb.VolumeEcShardsMountRequest
|
|
deleteRequests []*volume_server_pb.VolumeDeleteRequest
|
|
markReadonlyCalls int
|
|
markWritableCalls int
|
|
readFileStatusCalls int
|
|
vacuumGarbageRatio float64
|
|
vacuumCommitReadOnly bool
|
|
vacuumCheckCalls int
|
|
vacuumCompactCalls int
|
|
vacuumCommitCalls int
|
|
vacuumCleanupCalls int
|
|
volumeCopyCalls int
|
|
volumeMountCalls int
|
|
tailReceiverCalls int
|
|
}
|
|
|
|
// NewVolumeServer starts a test volume server using the provided base directory.
|
|
func NewVolumeServer(t *testing.T, baseDir string) *VolumeServer {
|
|
t.Helper()
|
|
|
|
if baseDir == "" {
|
|
baseDir = t.TempDir()
|
|
}
|
|
if err := os.MkdirAll(baseDir, 0755); err != nil {
|
|
t.Fatalf("create volume base dir: %v", err)
|
|
}
|
|
|
|
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
|
if err != nil {
|
|
t.Fatalf("listen volume server: %v", err)
|
|
}
|
|
|
|
grpcPort := listener.Addr().(*net.TCPAddr).Port
|
|
server := pb.NewGrpcServer()
|
|
vs := &VolumeServer{
|
|
t: t,
|
|
server: server,
|
|
listener: listener,
|
|
address: fmt.Sprintf("127.0.0.1:0.%d", grpcPort),
|
|
baseDir: baseDir,
|
|
receivedFiles: make(map[string]uint64),
|
|
readonlyVolumes: make(map[uint32]bool),
|
|
}
|
|
|
|
volume_server_pb.RegisterVolumeServerServer(server, vs)
|
|
go func() {
|
|
_ = server.Serve(listener)
|
|
}()
|
|
|
|
t.Cleanup(func() {
|
|
vs.Shutdown()
|
|
})
|
|
|
|
return vs
|
|
}
|
|
|
|
// Address returns the gRPC address of the volume server.
|
|
func (v *VolumeServer) Address() string {
|
|
return v.address
|
|
}
|
|
|
|
// BaseDir returns the base directory used by the server.
|
|
func (v *VolumeServer) BaseDir() string {
|
|
return v.baseDir
|
|
}
|
|
|
|
// ReceivedFiles returns a snapshot of received files and byte counts.
|
|
func (v *VolumeServer) ReceivedFiles() map[string]uint64 {
|
|
v.mu.Lock()
|
|
defer v.mu.Unlock()
|
|
|
|
out := make(map[string]uint64, len(v.receivedFiles))
|
|
for key, value := range v.receivedFiles {
|
|
out[key] = value
|
|
}
|
|
return out
|
|
}
|
|
|
|
// SetVacuumGarbageRatio sets the garbage ratio returned by VacuumVolumeCheck.
|
|
func (v *VolumeServer) SetVacuumGarbageRatio(ratio float64) {
|
|
v.mu.Lock()
|
|
defer v.mu.Unlock()
|
|
v.vacuumGarbageRatio = ratio
|
|
}
|
|
|
|
// SetVacuumCommitReadOnly sets the IsReadOnly value returned by VacuumVolumeCommit.
|
|
func (v *VolumeServer) SetVacuumCommitReadOnly(readOnly bool) {
|
|
v.mu.Lock()
|
|
defer v.mu.Unlock()
|
|
v.vacuumCommitReadOnly = readOnly
|
|
}
|
|
|
|
// VacuumStats returns the vacuum RPC call counts.
|
|
func (v *VolumeServer) VacuumStats() (check, compact, commit, cleanup int) {
|
|
v.mu.Lock()
|
|
defer v.mu.Unlock()
|
|
return v.vacuumCheckCalls, v.vacuumCompactCalls, v.vacuumCommitCalls, v.vacuumCleanupCalls
|
|
}
|
|
|
|
// BalanceStats returns the balance RPC call counts.
|
|
func (v *VolumeServer) BalanceStats() (copyCalls, mountCalls, tailCalls int) {
|
|
v.mu.Lock()
|
|
defer v.mu.Unlock()
|
|
return v.volumeCopyCalls, v.volumeMountCalls, v.tailReceiverCalls
|
|
}
|
|
|
|
// MountRequests returns recorded mount requests.
|
|
func (v *VolumeServer) MountRequests() []*volume_server_pb.VolumeEcShardsMountRequest {
|
|
v.mu.Lock()
|
|
defer v.mu.Unlock()
|
|
|
|
out := make([]*volume_server_pb.VolumeEcShardsMountRequest, len(v.mountRequests))
|
|
copy(out, v.mountRequests)
|
|
return out
|
|
}
|
|
|
|
// DeleteRequests returns recorded delete requests.
|
|
func (v *VolumeServer) DeleteRequests() []*volume_server_pb.VolumeDeleteRequest {
|
|
v.mu.Lock()
|
|
defer v.mu.Unlock()
|
|
|
|
out := make([]*volume_server_pb.VolumeDeleteRequest, len(v.deleteRequests))
|
|
copy(out, v.deleteRequests)
|
|
return out
|
|
}
|
|
|
|
// MarkReadonlyCount returns the number of readonly calls.
|
|
func (v *VolumeServer) MarkReadonlyCount() int {
|
|
v.mu.Lock()
|
|
defer v.mu.Unlock()
|
|
return v.markReadonlyCalls
|
|
}
|
|
|
|
// MarkWritableCount returns the number of writable calls.
|
|
func (v *VolumeServer) MarkWritableCount() int {
|
|
v.mu.Lock()
|
|
defer v.mu.Unlock()
|
|
return v.markWritableCalls
|
|
}
|
|
|
|
// ReadFileStatusCount returns the number of ReadVolumeFileStatus calls.
|
|
func (v *VolumeServer) ReadFileStatusCount() int {
|
|
v.mu.Lock()
|
|
defer v.mu.Unlock()
|
|
return v.readFileStatusCalls
|
|
}
|
|
|
|
// Shutdown stops the volume server.
|
|
func (v *VolumeServer) Shutdown() {
|
|
if v.server != nil {
|
|
v.server.GracefulStop()
|
|
}
|
|
if v.listener != nil {
|
|
_ = v.listener.Close()
|
|
}
|
|
}
|
|
|
|
func (v *VolumeServer) filePath(volumeID uint32, ext string) string {
|
|
return filepath.Join(v.baseDir, fmt.Sprintf("%d%s", volumeID, ext))
|
|
}
|
|
|
|
func (v *VolumeServer) CopyFile(req *volume_server_pb.CopyFileRequest, stream volume_server_pb.VolumeServer_CopyFileServer) error {
|
|
if req == nil {
|
|
return fmt.Errorf("copy file request is nil")
|
|
}
|
|
path := v.filePath(req.VolumeId, req.Ext)
|
|
file, err := os.Open(path)
|
|
if err != nil {
|
|
if req.IgnoreSourceFileNotFound {
|
|
return nil
|
|
}
|
|
return err
|
|
}
|
|
defer file.Close()
|
|
|
|
buf := make([]byte, 64*1024)
|
|
remaining := int64(req.GetStopOffset())
|
|
for {
|
|
if remaining == 0 {
|
|
break
|
|
}
|
|
|
|
readBuf := buf
|
|
if remaining > 0 && remaining < int64(len(buf)) {
|
|
readBuf = buf[:remaining]
|
|
}
|
|
|
|
n, readErr := file.Read(readBuf)
|
|
if n > 0 {
|
|
if err := stream.Send(&volume_server_pb.CopyFileResponse{FileContent: readBuf[:n]}); err != nil {
|
|
return err
|
|
}
|
|
if remaining > 0 {
|
|
remaining -= int64(n)
|
|
}
|
|
}
|
|
if readErr == io.EOF {
|
|
break
|
|
}
|
|
if readErr != nil {
|
|
return readErr
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (v *VolumeServer) ReceiveFile(stream volume_server_pb.VolumeServer_ReceiveFileServer) error {
|
|
var (
|
|
info *volume_server_pb.ReceiveFileInfo
|
|
file *os.File
|
|
bytesWritten uint64
|
|
filePath string
|
|
)
|
|
defer func() {
|
|
if file != nil {
|
|
_ = file.Close()
|
|
}
|
|
}()
|
|
|
|
for {
|
|
req, err := stream.Recv()
|
|
if err == io.EOF {
|
|
if info == nil {
|
|
return stream.SendAndClose(&volume_server_pb.ReceiveFileResponse{Error: "missing file info"})
|
|
}
|
|
v.mu.Lock()
|
|
v.receivedFiles[filePath] = bytesWritten
|
|
v.mu.Unlock()
|
|
return stream.SendAndClose(&volume_server_pb.ReceiveFileResponse{BytesWritten: bytesWritten})
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if reqInfo := req.GetInfo(); reqInfo != nil {
|
|
info = reqInfo
|
|
filePath = v.filePath(info.VolumeId, info.Ext)
|
|
if err := os.MkdirAll(filepath.Dir(filePath), 0755); err != nil {
|
|
return err
|
|
}
|
|
file, err = os.Create(filePath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
continue
|
|
}
|
|
|
|
chunk := req.GetFileContent()
|
|
if len(chunk) == 0 {
|
|
continue
|
|
}
|
|
if file == nil {
|
|
return fmt.Errorf("file info not received")
|
|
}
|
|
n, writeErr := file.Write(chunk)
|
|
if writeErr != nil {
|
|
return writeErr
|
|
}
|
|
bytesWritten += uint64(n)
|
|
}
|
|
}
|
|
|
|
func (v *VolumeServer) VolumeEcShardsMount(ctx context.Context, req *volume_server_pb.VolumeEcShardsMountRequest) (*volume_server_pb.VolumeEcShardsMountResponse, error) {
|
|
v.mu.Lock()
|
|
v.mountRequests = append(v.mountRequests, req)
|
|
v.mu.Unlock()
|
|
return &volume_server_pb.VolumeEcShardsMountResponse{}, nil
|
|
}
|
|
|
|
// VolumeEcShardsUnmount is a no-op stub: the worker's pre-distribute
|
|
// cleanup calls it against every destination, and the fake server has no
|
|
// mounted state to clear.
|
|
func (v *VolumeServer) VolumeEcShardsUnmount(ctx context.Context, req *volume_server_pb.VolumeEcShardsUnmountRequest) (*volume_server_pb.VolumeEcShardsUnmountResponse, error) {
|
|
return &volume_server_pb.VolumeEcShardsUnmountResponse{}, nil
|
|
}
|
|
|
|
// VolumeEcShardsDelete is a no-op stub paired with VolumeEcShardsUnmount
|
|
// above; the fake server doesn't persist shard files beyond what
|
|
// ReceiveFile wrote, so there's nothing to remove. It still echoes the
|
|
// full_teardown acknowledgement so the worker doesn't treat the fake as a
|
|
// pre-upgrade server that silently skipped the teardown.
|
|
func (v *VolumeServer) VolumeEcShardsDelete(ctx context.Context, req *volume_server_pb.VolumeEcShardsDeleteRequest) (*volume_server_pb.VolumeEcShardsDeleteResponse, error) {
|
|
return &volume_server_pb.VolumeEcShardsDeleteResponse{FullTeardownDone: req.FullTeardown}, nil
|
|
}
|
|
|
|
func (v *VolumeServer) VolumeEcShardsInfo(ctx context.Context, req *volume_server_pb.VolumeEcShardsInfoRequest) (*volume_server_pb.VolumeEcShardsInfoResponse, error) {
|
|
if req == nil {
|
|
return nil, fmt.Errorf("VolumeEcShardsInfo request is nil")
|
|
}
|
|
v.mu.Lock()
|
|
defer v.mu.Unlock()
|
|
|
|
// Report whichever shards exist on disk: seeded or mounted. Collection
|
|
// comes from the matching mount request when one exists.
|
|
collectionByShard := make(map[uint32]string)
|
|
for _, mr := range v.mountRequests {
|
|
if mr == nil || mr.VolumeId != req.VolumeId {
|
|
continue
|
|
}
|
|
for _, shardId := range mr.ShardIds {
|
|
if _, ok := collectionByShard[shardId]; !ok {
|
|
collectionByShard[shardId] = mr.Collection
|
|
}
|
|
}
|
|
}
|
|
|
|
resp := &volume_server_pb.VolumeEcShardsInfoResponse{}
|
|
prefix := fmt.Sprintf("%d.ec", req.VolumeId)
|
|
entries, _ := os.ReadDir(v.baseDir)
|
|
for _, entry := range entries {
|
|
if entry.IsDir() {
|
|
continue
|
|
}
|
|
name := entry.Name()
|
|
if !strings.HasPrefix(name, prefix) {
|
|
continue
|
|
}
|
|
suffix := strings.TrimPrefix(name, prefix)
|
|
if len(suffix) < 2 {
|
|
continue
|
|
}
|
|
var shardId uint32
|
|
if _, err := fmt.Sscanf(suffix[:2], "%d", &shardId); err != nil {
|
|
continue
|
|
}
|
|
var size int64
|
|
if info, err := entry.Info(); err == nil {
|
|
size = info.Size()
|
|
}
|
|
resp.EcShardInfos = append(resp.EcShardInfos, &volume_server_pb.EcShardInfo{
|
|
ShardId: shardId,
|
|
Size: size,
|
|
Collection: collectionByShard[shardId],
|
|
VolumeId: req.VolumeId,
|
|
})
|
|
}
|
|
return resp, nil
|
|
}
|
|
|
|
func (v *VolumeServer) VolumeDelete(ctx context.Context, req *volume_server_pb.VolumeDeleteRequest) (*volume_server_pb.VolumeDeleteResponse, error) {
|
|
v.mu.Lock()
|
|
v.deleteRequests = append(v.deleteRequests, req)
|
|
v.mu.Unlock()
|
|
|
|
if req != nil {
|
|
_ = os.Remove(v.filePath(req.VolumeId, ".dat"))
|
|
_ = os.Remove(v.filePath(req.VolumeId, ".idx"))
|
|
}
|
|
|
|
return &volume_server_pb.VolumeDeleteResponse{}, nil
|
|
}
|
|
|
|
func (v *VolumeServer) VolumeMarkReadonly(ctx context.Context, req *volume_server_pb.VolumeMarkReadonlyRequest) (*volume_server_pb.VolumeMarkReadonlyResponse, error) {
|
|
v.mu.Lock()
|
|
v.markReadonlyCalls++
|
|
if req != nil {
|
|
v.readonlyVolumes[req.VolumeId] = true
|
|
}
|
|
v.mu.Unlock()
|
|
return &volume_server_pb.VolumeMarkReadonlyResponse{}, nil
|
|
}
|
|
|
|
func (v *VolumeServer) VolumeMarkWritable(ctx context.Context, req *volume_server_pb.VolumeMarkWritableRequest) (*volume_server_pb.VolumeMarkWritableResponse, error) {
|
|
v.mu.Lock()
|
|
v.markWritableCalls++
|
|
if req != nil {
|
|
v.readonlyVolumes[req.VolumeId] = false
|
|
}
|
|
v.mu.Unlock()
|
|
return &volume_server_pb.VolumeMarkWritableResponse{}, nil
|
|
}
|
|
|
|
func (v *VolumeServer) VolumeStatus(ctx context.Context, req *volume_server_pb.VolumeStatusRequest) (*volume_server_pb.VolumeStatusResponse, error) {
|
|
v.mu.Lock()
|
|
defer v.mu.Unlock()
|
|
return &volume_server_pb.VolumeStatusResponse{IsReadOnly: v.readonlyVolumes[req.GetVolumeId()]}, nil
|
|
}
|
|
|
|
func (v *VolumeServer) ReadVolumeFileStatus(ctx context.Context, req *volume_server_pb.ReadVolumeFileStatusRequest) (*volume_server_pb.ReadVolumeFileStatusResponse, error) {
|
|
v.mu.Lock()
|
|
v.readFileStatusCalls++
|
|
v.mu.Unlock()
|
|
|
|
datInfo, err := os.Stat(v.filePath(req.VolumeId, ".dat"))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
idxInfo, err := os.Stat(v.filePath(req.VolumeId, ".idx"))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &volume_server_pb.ReadVolumeFileStatusResponse{
|
|
VolumeId: req.VolumeId,
|
|
DatFileSize: uint64(datInfo.Size()),
|
|
IdxFileSize: uint64(idxInfo.Size()),
|
|
FileCount: 1,
|
|
}, nil
|
|
}
|
|
|
|
func (v *VolumeServer) VacuumVolumeCheck(ctx context.Context, req *volume_server_pb.VacuumVolumeCheckRequest) (*volume_server_pb.VacuumVolumeCheckResponse, error) {
|
|
v.mu.Lock()
|
|
v.vacuumCheckCalls++
|
|
ratio := v.vacuumGarbageRatio
|
|
v.mu.Unlock()
|
|
return &volume_server_pb.VacuumVolumeCheckResponse{GarbageRatio: ratio}, nil
|
|
}
|
|
|
|
func (v *VolumeServer) VacuumVolumeCompact(req *volume_server_pb.VacuumVolumeCompactRequest, stream volume_server_pb.VolumeServer_VacuumVolumeCompactServer) error {
|
|
v.mu.Lock()
|
|
v.vacuumCompactCalls++
|
|
v.mu.Unlock()
|
|
return stream.Send(&volume_server_pb.VacuumVolumeCompactResponse{ProcessedBytes: 1024})
|
|
}
|
|
|
|
func (v *VolumeServer) VacuumVolumeCommit(ctx context.Context, req *volume_server_pb.VacuumVolumeCommitRequest) (*volume_server_pb.VacuumVolumeCommitResponse, error) {
|
|
v.mu.Lock()
|
|
v.vacuumCommitCalls++
|
|
readOnly := v.vacuumCommitReadOnly
|
|
v.mu.Unlock()
|
|
return &volume_server_pb.VacuumVolumeCommitResponse{IsReadOnly: readOnly}, nil
|
|
}
|
|
|
|
func (v *VolumeServer) VacuumVolumeCleanup(ctx context.Context, req *volume_server_pb.VacuumVolumeCleanupRequest) (*volume_server_pb.VacuumVolumeCleanupResponse, error) {
|
|
v.mu.Lock()
|
|
v.vacuumCleanupCalls++
|
|
v.mu.Unlock()
|
|
return &volume_server_pb.VacuumVolumeCleanupResponse{}, nil
|
|
}
|
|
|
|
func (v *VolumeServer) VolumeCopy(req *volume_server_pb.VolumeCopyRequest, stream volume_server_pb.VolumeServer_VolumeCopyServer) error {
|
|
v.mu.Lock()
|
|
v.volumeCopyCalls++
|
|
v.mu.Unlock()
|
|
|
|
dialOption := grpc.WithTransportCredentials(insecure.NewCredentials())
|
|
var statusResp *volume_server_pb.ReadVolumeFileStatusResponse
|
|
if err := operation.WithVolumeServerClient(false, pb.ServerAddress(req.SourceDataNode), dialOption,
|
|
func(client volume_server_pb.VolumeServerClient) error {
|
|
var readErr error
|
|
statusResp, readErr = client.ReadVolumeFileStatus(stream.Context(), &volume_server_pb.ReadVolumeFileStatusRequest{
|
|
VolumeId: req.VolumeId,
|
|
})
|
|
return readErr
|
|
}); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := v.copyRemoteFile(stream.Context(), req.SourceDataNode, req.VolumeId, ".dat", statusResp.DatFileSize, dialOption); err != nil {
|
|
return err
|
|
}
|
|
if err := v.copyRemoteFile(stream.Context(), req.SourceDataNode, req.VolumeId, ".idx", statusResp.IdxFileSize, dialOption); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := stream.Send(&volume_server_pb.VolumeCopyResponse{ProcessedBytes: int64(statusResp.DatFileSize + statusResp.IdxFileSize)}); err != nil {
|
|
return err
|
|
}
|
|
return stream.Send(&volume_server_pb.VolumeCopyResponse{LastAppendAtNs: uint64(time.Now().UnixNano())})
|
|
}
|
|
|
|
func (v *VolumeServer) VolumeMount(ctx context.Context, req *volume_server_pb.VolumeMountRequest) (*volume_server_pb.VolumeMountResponse, error) {
|
|
v.mu.Lock()
|
|
v.volumeMountCalls++
|
|
v.mu.Unlock()
|
|
return &volume_server_pb.VolumeMountResponse{}, nil
|
|
}
|
|
|
|
func (v *VolumeServer) VolumeTailReceiver(ctx context.Context, req *volume_server_pb.VolumeTailReceiverRequest) (*volume_server_pb.VolumeTailReceiverResponse, error) {
|
|
v.mu.Lock()
|
|
v.tailReceiverCalls++
|
|
v.mu.Unlock()
|
|
return &volume_server_pb.VolumeTailReceiverResponse{}, nil
|
|
}
|
|
|
|
func (v *VolumeServer) copyRemoteFile(ctx context.Context, sourceDataNode string, volumeID uint32, ext string, fileSize uint64, dialOption grpc.DialOption) error {
|
|
path := v.filePath(volumeID, ext)
|
|
if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
|
|
return err
|
|
}
|
|
|
|
file, err := os.Create(path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer file.Close()
|
|
|
|
return operation.WithVolumeServerClient(true, pb.ServerAddress(sourceDataNode), dialOption,
|
|
func(client volume_server_pb.VolumeServerClient) error {
|
|
stream, err := client.CopyFile(ctx, &volume_server_pb.CopyFileRequest{
|
|
VolumeId: volumeID,
|
|
Ext: ext,
|
|
StopOffset: fileSize,
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
for {
|
|
resp, recvErr := stream.Recv()
|
|
if recvErr == io.EOF {
|
|
return nil
|
|
}
|
|
if recvErr != nil {
|
|
return recvErr
|
|
}
|
|
if len(resp.FileContent) == 0 {
|
|
continue
|
|
}
|
|
if _, err := file.Write(resp.FileContent); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
})
|
|
}
|