snapshot of https://github.com/seaweedfs/seaweedfs.git (master, 2026-09-16; blobs > 131072B omitted)
Some checks failed
go: build dev binaries / cleanup (push) Successful in 4s
Codespell / Check for spelling errors (push) Failing after 3s
docker: build dev containers / build-rust-binaries (amd64, x86_64-unknown-linux-musl) (push) Failing after 3s
docker: build dev containers / build-dev-containers (push) Has been skipped
docker: build dev containers / build-rust-binaries (arm64, true, aarch64-unknown-linux-musl) (push) Failing after 3s
End to End / FUSE Mount (push) Failing after 2s
EC Integration Tests / EC Integration Tests (push) Failing after 30s
FUSE DLM Integration Tests / FUSE DLM Integration Tests (push) Failing after 3s
FUSE Volume Server Failover Tests / FUSE Volume Server Failover (push) Failing after 3s
FUSE Integration Tests / FUSE Integration Testing (push) Failing after 2s
FUSE P2P Peer Chunk Sharing Integration Tests / FUSE P2P Peer Chunk Sharing (push) Failing after 3s
go: build binary / Go Vet (push) Failing after 2s
go: build binary / Go Vet 32-bit (push) Failing after 3s
go: build binary / Build (push) Failing after 4s
go: build binary / Build cross-platform (freebsd/amd64) (push) Failing after 3s
go: build binary / Build cross-platform (windows/amd64) (push) Failing after 3s
go: build binary / Build cross-platform (darwin/arm64) (push) Failing after 3s
go: build binary / Build cross-platform (windows/arm64) (push) Failing after 3s
go: build binary / Test (push) Failing after 2s
go: build binary / Test 32-bit (push) Failing after 3s
helm: lint and test charts / lint-test (push) Failing after 3s
Java Client Integration Tests / Java Integration Tests (11) (push) Failing after 3s
Java Client Integration Tests / Java Integration Tests (17) (push) Failing after 3s
go: build dev binaries / build_dev_linux_windows (amd64, linux) (push) Failing after 2s
go: build dev binaries / build_dev_linux_windows (amd64, windows) (push) Failing after 3s
go: build dev binaries / build_dev_darwin (amd64, darwin) (push) Has been skipped
go: build dev binaries / build_dev_darwin (arm64, darwin) (push) Has been skipped
Java Client Unit Tests / Java Unit Tests (11) (push) Failing after 3s
Java Client Unit Tests / Java Unit Tests (17) (push) Failing after 2s
Java Client Unit Tests / Java Unit Tests (21) (push) Failing after 3s
Java Client Unit Tests / Java Unit Tests (8) (push) Failing after 3s
Kafka Quick Test (Load Test with Schema Registry) / Kafka Client Load Test (Quick) (push) Failing after 3s
Kafka Gateway Tests / Kafka Unit Tests (unit-tests-1) (push) Failing after 11s
Kafka Gateway Tests / Kafka Integration Tests (Critical) (integration-1) (push) Failing after 3s
Kafka Gateway Tests / Kafka End-to-End Tests (with SMQ) (e2e-1) (push) Failing after 3s
Kafka Gateway Tests / Kafka Consumer Group Tests (Highly Isolated) (consumer-group-1) (push) Failing after 2s
Kafka Gateway Tests / Kafka Client Compatibility (with SMQ) (client-compat-1) (push) Failing after 3s
Kafka Gateway Tests / Kafka SMQ Integration Tests (Full Stack) (smq-integration-1) (push) Failing after 3s
Kafka Gateway Tests / Kafka Protocol Tests (Isolated) (protocol-1) (push) Failing after 3s
KMS Tests / KMS Provider Integration Tests (push) Failing after 2s
KMS Tests / S3 KMS End-to-End Tests (push) Failing after 3s
Master Cold Start Tests / Master Cold Start Tests (push) Failing after 4s
Metadata Subscribe Integration Tests / Metadata Subscribe Integration Tests (push) Failing after 3s
Multi-Master Tests / Multi-Master Failover Tests (push) Failing after 3s
Performance / CPU and Heap Profile (push) Failing after 2s
Performance / S3 Read/Write Benchmark (rust volume) (push) Failing after 3s
pjdfstest POSIX Compliance / pjdfstest (push) Failing after 2s
Plugin Worker Integration Tests / Plugin Worker: erasure_coding (push) Failing after 3s
Plugin Worker Integration Tests / Plugin Worker: vacuum (push) Failing after 3s
Rust Volume Server Tests / Rust Integration Tests (push) Failing after 3s
Rust Volume Server Tests / Go Tests with Rust Volume (grpc - Shard 1) (push) Failing after 2s
Rust Volume Server Tests / Go Tests with Rust Volume (http - Shard 1) (push) Failing after 3s
Rust Volume Server Tests / Go Tests with Rust Volume (grpc - Shard 2) (push) Failing after 3s
Rust Volume Server Tests / Go Tests with Rust Volume (http - Shard 2) (push) Failing after 3s
Rust Volume Server Tests / Go Tests with Rust Volume (grpc - Shard 3) (push) Failing after 3s
S3 IAM Integration Tests / S3 IAM Distributed Tests (push) Failing after 2s
S3 IAM Integration Tests / S3 IAM Performance Tests (push) Failing after 3s
Performance / Throughput Benchmark (go volume) (push) Failing after 3s
Performance / Throughput Benchmark (rust volume) (push) Failing after 2s
Performance / S3 Read/Write Benchmark (go volume) (push) Failing after 3s
Plugin Worker Integration Tests / Plugin Worker: volume_balance (push) Failing after 3s
PostgreSQL Gateway Tests / PostgreSQL Basic Tests (push) Failing after 3s
Rust Volume Server Tests / Rust Unit Tests (push) Failing after 2s
Rust Volume Server Tests / Go Tests with Rust Volume (http - Shard 3) (push) Failing after 3s
Rust Plugin Worker Tests / Rust Plugin Worker Build and Unit Tests (push) Failing after 3s
rust: build dev volume server binaries / cleanup (push) Successful in 3s
S3 ETag and ACL Tests / S3 ETag + ACL Integration Tests (push) Failing after 3s
S3 IAM Integration Tests / IAM Unit Tests (push) Failing after 3s
S3 IAM Integration Tests / S3 IAM Integration Tests (advanced) (push) Failing after 3s
S3 IAM Integration Tests / S3 IAM Integration Tests (basic) (push) Failing after 2s
S3 IAM Integration Tests / S3 IAM Integration Tests (group) (push) Failing after 3s
S3 IAM Integration Tests / S3 IAM Integration Tests (policy-enforcement) (push) Failing after 2s
S3 IAM Integration Tests / S3 IAM Integration Tests (sts) (push) Failing after 4s
S3 Keycloak Integration Tests / S3 Keycloak Integration Tests (push) Failing after 3s
S3 PyArrow Parquet Tests / PyArrow Parquet Tests (Python 3.11) (push) Failing after 3s
S3 PyArrow Parquet Tests / Go Unit Tests (Implicit Directory) (push) Has been cancelled
S3 PyArrow Parquet Tests / PyArrow Parquet Tests (Python 3.12) (push) Has been cancelled
rust: build dev volume server binaries / build-rust-volume-dev-darwin (darwin-amd64, x86_64-apple-darwin) (push) Has been cancelled
rust: build dev volume server binaries / build-rust-volume-dev-darwin (darwin-arm64, aarch64-apple-darwin) (push) Has been cancelled
rust: build dev volume server binaries / build-rust-volume-dev-linux (linux-amd64, x86_64-unknown-linux-gnu) (push) Has been cancelled
S3 PyArrow Parquet Tests / PyArrow Parquet Tests (Python 3.9) (push) Has been cancelled
S3 Proxy Signature Tests / S3 Proxy Signature Verification Tests (push) Has been cancelled
S3 SDK V2 Route Disambiguation Tests / S3 SDK V2 Routing Tests (push) Has been cancelled
S3 SSE Tests / S3 SSE Integration Tests (quick) (push) Has been cancelled
S3 SSE Tests / S3 SSE Compatibility Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Metadata Persistence Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Copy Operations Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Multipart Upload Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Performance Test (push) Has been cancelled
S3 SSE Tests / S3 Volume Encryption Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Integration Tests (comprehensive) (push) Has been cancelled
Spark Integration Tests / Spark Integration Tests (push) Failing after 2s
Telemetry Integration Tests / telemetry-integration-test (push) Failing after 3s
Volume Server Integration Tests / Volume Server Integration Tests (grpc - Shard 1) (push) Failing after 3s
Volume Server Integration Tests / Volume Server Integration Tests (http - Shard 1) (push) Failing after 3s
Ceph S3 tests / Basic S3 tests (KV store) (push) Has been cancelled
Ceph S3 tests / S3 Versioning & Object Lock tests (push) Has been cancelled
S3 Policy Integration Tests / S3 Policy Unit Tests (push) Failing after 3s
Ceph S3 tests / S3 CORS tests (push) Has been cancelled
Ceph S3 tests / SeaweedFS Custom S3 Copy tests (push) Has been cancelled
Ceph S3 tests / Basic S3 tests (SQL store) (push) Has been cancelled
test s3 over https using aws-cli / awscli-tests (push) Has been cancelled
TLS Rotation Integration Tests / TLS Rotation Integration Tests (push) Has been cancelled
Vacuum Integration Tests / Vacuum Integration Tests (push) Has been cancelled
S3 Policy Integration Tests / S3 Policy Variables Integration Tests (push) Failing after 3s
S3 Policy Integration Tests / S3 Policy Enforcement Integration Tests (advanced-policy) (push) Failing after 3s
S3 Policy Integration Tests / S3 Policy Enforcement Integration Tests (basic-policy) (push) Failing after 2s
S3 Policy Integration Tests / S3 Policy Enforcement Integration Tests (contextual-policy) (push) Failing after 4s
S3 Policy Integration Tests / Trusted Proxy Detection Tests (push) Failing after 2s
S3 Policy Integration Tests / S3 Policy Shell Integration Tests (push) Failing after 3s
Samba on FUSE Integration / samba-integration (push) Failing after 2s
SFTP Integration Tests / SFTP Integration Testing (push) Failing after 3s
terraform: validate and test modules / fmt, validate, plan-level tests (push) Failing after 3s
terraform: validate and test modules / local cluster smoke test (real weed) (push) Failing after 2s
Volume Server Integration Tests / Volume Server Integration Tests (grpc - Shard 3) (push) Failing after 3s
Volume Server Integration Tests / Volume Server Integration Tests (grpc - Shard 2) (push) Failing after 3s
Volume Server Integration Tests / Volume Server Integration Tests (http - Shard 2) (push) Failing after 2s
Volume Server Integration Tests / Volume Server Integration Tests (http - Shard 3) (push) Failing after 5s
mount: windows conformance / WinFsp conformance (push) Has been cancelled
mount: windows / Mount on Windows (push) Has been cancelled
Some checks failed
go: build dev binaries / cleanup (push) Successful in 4s
Codespell / Check for spelling errors (push) Failing after 3s
docker: build dev containers / build-rust-binaries (amd64, x86_64-unknown-linux-musl) (push) Failing after 3s
docker: build dev containers / build-dev-containers (push) Has been skipped
docker: build dev containers / build-rust-binaries (arm64, true, aarch64-unknown-linux-musl) (push) Failing after 3s
End to End / FUSE Mount (push) Failing after 2s
EC Integration Tests / EC Integration Tests (push) Failing after 30s
FUSE DLM Integration Tests / FUSE DLM Integration Tests (push) Failing after 3s
FUSE Volume Server Failover Tests / FUSE Volume Server Failover (push) Failing after 3s
FUSE Integration Tests / FUSE Integration Testing (push) Failing after 2s
FUSE P2P Peer Chunk Sharing Integration Tests / FUSE P2P Peer Chunk Sharing (push) Failing after 3s
go: build binary / Go Vet (push) Failing after 2s
go: build binary / Go Vet 32-bit (push) Failing after 3s
go: build binary / Build (push) Failing after 4s
go: build binary / Build cross-platform (freebsd/amd64) (push) Failing after 3s
go: build binary / Build cross-platform (windows/amd64) (push) Failing after 3s
go: build binary / Build cross-platform (darwin/arm64) (push) Failing after 3s
go: build binary / Build cross-platform (windows/arm64) (push) Failing after 3s
go: build binary / Test (push) Failing after 2s
go: build binary / Test 32-bit (push) Failing after 3s
helm: lint and test charts / lint-test (push) Failing after 3s
Java Client Integration Tests / Java Integration Tests (11) (push) Failing after 3s
Java Client Integration Tests / Java Integration Tests (17) (push) Failing after 3s
go: build dev binaries / build_dev_linux_windows (amd64, linux) (push) Failing after 2s
go: build dev binaries / build_dev_linux_windows (amd64, windows) (push) Failing after 3s
go: build dev binaries / build_dev_darwin (amd64, darwin) (push) Has been skipped
go: build dev binaries / build_dev_darwin (arm64, darwin) (push) Has been skipped
Java Client Unit Tests / Java Unit Tests (11) (push) Failing after 3s
Java Client Unit Tests / Java Unit Tests (17) (push) Failing after 2s
Java Client Unit Tests / Java Unit Tests (21) (push) Failing after 3s
Java Client Unit Tests / Java Unit Tests (8) (push) Failing after 3s
Kafka Quick Test (Load Test with Schema Registry) / Kafka Client Load Test (Quick) (push) Failing after 3s
Kafka Gateway Tests / Kafka Unit Tests (unit-tests-1) (push) Failing after 11s
Kafka Gateway Tests / Kafka Integration Tests (Critical) (integration-1) (push) Failing after 3s
Kafka Gateway Tests / Kafka End-to-End Tests (with SMQ) (e2e-1) (push) Failing after 3s
Kafka Gateway Tests / Kafka Consumer Group Tests (Highly Isolated) (consumer-group-1) (push) Failing after 2s
Kafka Gateway Tests / Kafka Client Compatibility (with SMQ) (client-compat-1) (push) Failing after 3s
Kafka Gateway Tests / Kafka SMQ Integration Tests (Full Stack) (smq-integration-1) (push) Failing after 3s
Kafka Gateway Tests / Kafka Protocol Tests (Isolated) (protocol-1) (push) Failing after 3s
KMS Tests / KMS Provider Integration Tests (push) Failing after 2s
KMS Tests / S3 KMS End-to-End Tests (push) Failing after 3s
Master Cold Start Tests / Master Cold Start Tests (push) Failing after 4s
Metadata Subscribe Integration Tests / Metadata Subscribe Integration Tests (push) Failing after 3s
Multi-Master Tests / Multi-Master Failover Tests (push) Failing after 3s
Performance / CPU and Heap Profile (push) Failing after 2s
Performance / S3 Read/Write Benchmark (rust volume) (push) Failing after 3s
pjdfstest POSIX Compliance / pjdfstest (push) Failing after 2s
Plugin Worker Integration Tests / Plugin Worker: erasure_coding (push) Failing after 3s
Plugin Worker Integration Tests / Plugin Worker: vacuum (push) Failing after 3s
Rust Volume Server Tests / Rust Integration Tests (push) Failing after 3s
Rust Volume Server Tests / Go Tests with Rust Volume (grpc - Shard 1) (push) Failing after 2s
Rust Volume Server Tests / Go Tests with Rust Volume (http - Shard 1) (push) Failing after 3s
Rust Volume Server Tests / Go Tests with Rust Volume (grpc - Shard 2) (push) Failing after 3s
Rust Volume Server Tests / Go Tests with Rust Volume (http - Shard 2) (push) Failing after 3s
Rust Volume Server Tests / Go Tests with Rust Volume (grpc - Shard 3) (push) Failing after 3s
S3 IAM Integration Tests / S3 IAM Distributed Tests (push) Failing after 2s
S3 IAM Integration Tests / S3 IAM Performance Tests (push) Failing after 3s
Performance / Throughput Benchmark (go volume) (push) Failing after 3s
Performance / Throughput Benchmark (rust volume) (push) Failing after 2s
Performance / S3 Read/Write Benchmark (go volume) (push) Failing after 3s
Plugin Worker Integration Tests / Plugin Worker: volume_balance (push) Failing after 3s
PostgreSQL Gateway Tests / PostgreSQL Basic Tests (push) Failing after 3s
Rust Volume Server Tests / Rust Unit Tests (push) Failing after 2s
Rust Volume Server Tests / Go Tests with Rust Volume (http - Shard 3) (push) Failing after 3s
Rust Plugin Worker Tests / Rust Plugin Worker Build and Unit Tests (push) Failing after 3s
rust: build dev volume server binaries / cleanup (push) Successful in 3s
S3 ETag and ACL Tests / S3 ETag + ACL Integration Tests (push) Failing after 3s
S3 IAM Integration Tests / IAM Unit Tests (push) Failing after 3s
S3 IAM Integration Tests / S3 IAM Integration Tests (advanced) (push) Failing after 3s
S3 IAM Integration Tests / S3 IAM Integration Tests (basic) (push) Failing after 2s
S3 IAM Integration Tests / S3 IAM Integration Tests (group) (push) Failing after 3s
S3 IAM Integration Tests / S3 IAM Integration Tests (policy-enforcement) (push) Failing after 2s
S3 IAM Integration Tests / S3 IAM Integration Tests (sts) (push) Failing after 4s
S3 Keycloak Integration Tests / S3 Keycloak Integration Tests (push) Failing after 3s
S3 PyArrow Parquet Tests / PyArrow Parquet Tests (Python 3.11) (push) Failing after 3s
S3 PyArrow Parquet Tests / Go Unit Tests (Implicit Directory) (push) Has been cancelled
S3 PyArrow Parquet Tests / PyArrow Parquet Tests (Python 3.12) (push) Has been cancelled
rust: build dev volume server binaries / build-rust-volume-dev-darwin (darwin-amd64, x86_64-apple-darwin) (push) Has been cancelled
rust: build dev volume server binaries / build-rust-volume-dev-darwin (darwin-arm64, aarch64-apple-darwin) (push) Has been cancelled
rust: build dev volume server binaries / build-rust-volume-dev-linux (linux-amd64, x86_64-unknown-linux-gnu) (push) Has been cancelled
S3 PyArrow Parquet Tests / PyArrow Parquet Tests (Python 3.9) (push) Has been cancelled
S3 Proxy Signature Tests / S3 Proxy Signature Verification Tests (push) Has been cancelled
S3 SDK V2 Route Disambiguation Tests / S3 SDK V2 Routing Tests (push) Has been cancelled
S3 SSE Tests / S3 SSE Integration Tests (quick) (push) Has been cancelled
S3 SSE Tests / S3 SSE Compatibility Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Metadata Persistence Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Copy Operations Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Multipart Upload Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Performance Test (push) Has been cancelled
S3 SSE Tests / S3 Volume Encryption Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Integration Tests (comprehensive) (push) Has been cancelled
Spark Integration Tests / Spark Integration Tests (push) Failing after 2s
Telemetry Integration Tests / telemetry-integration-test (push) Failing after 3s
Volume Server Integration Tests / Volume Server Integration Tests (grpc - Shard 1) (push) Failing after 3s
Volume Server Integration Tests / Volume Server Integration Tests (http - Shard 1) (push) Failing after 3s
Ceph S3 tests / Basic S3 tests (KV store) (push) Has been cancelled
Ceph S3 tests / S3 Versioning & Object Lock tests (push) Has been cancelled
S3 Policy Integration Tests / S3 Policy Unit Tests (push) Failing after 3s
Ceph S3 tests / S3 CORS tests (push) Has been cancelled
Ceph S3 tests / SeaweedFS Custom S3 Copy tests (push) Has been cancelled
Ceph S3 tests / Basic S3 tests (SQL store) (push) Has been cancelled
test s3 over https using aws-cli / awscli-tests (push) Has been cancelled
TLS Rotation Integration Tests / TLS Rotation Integration Tests (push) Has been cancelled
Vacuum Integration Tests / Vacuum Integration Tests (push) Has been cancelled
S3 Policy Integration Tests / S3 Policy Variables Integration Tests (push) Failing after 3s
S3 Policy Integration Tests / S3 Policy Enforcement Integration Tests (advanced-policy) (push) Failing after 3s
S3 Policy Integration Tests / S3 Policy Enforcement Integration Tests (basic-policy) (push) Failing after 2s
S3 Policy Integration Tests / S3 Policy Enforcement Integration Tests (contextual-policy) (push) Failing after 4s
S3 Policy Integration Tests / Trusted Proxy Detection Tests (push) Failing after 2s
S3 Policy Integration Tests / S3 Policy Shell Integration Tests (push) Failing after 3s
Samba on FUSE Integration / samba-integration (push) Failing after 2s
SFTP Integration Tests / SFTP Integration Testing (push) Failing after 3s
terraform: validate and test modules / fmt, validate, plan-level tests (push) Failing after 3s
terraform: validate and test modules / local cluster smoke test (real weed) (push) Failing after 2s
Volume Server Integration Tests / Volume Server Integration Tests (grpc - Shard 3) (push) Failing after 3s
Volume Server Integration Tests / Volume Server Integration Tests (grpc - Shard 2) (push) Failing after 3s
Volume Server Integration Tests / Volume Server Integration Tests (http - Shard 2) (push) Failing after 2s
Volume Server Integration Tests / Volume Server Integration Tests (http - Shard 3) (push) Failing after 5s
mount: windows conformance / WinFsp conformance (push) Has been cancelled
mount: windows / Mount on Windows (push) Has been cancelled
This commit is contained in:
598
weed/shell/command_volume_merge.go
Normal file
598
weed/shell/command_volume_merge.go
Normal file
@@ -0,0 +1,598 @@
|
||||
package shell
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"container/heap"
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/seaweedfs/seaweedfs/weed/glog"
|
||||
"github.com/seaweedfs/seaweedfs/weed/operation"
|
||||
"github.com/seaweedfs/seaweedfs/weed/pb"
|
||||
"github.com/seaweedfs/seaweedfs/weed/pb/master_pb"
|
||||
"github.com/seaweedfs/seaweedfs/weed/pb/volume_server_pb"
|
||||
"github.com/seaweedfs/seaweedfs/weed/storage/needle"
|
||||
"github.com/seaweedfs/seaweedfs/weed/storage/super_block"
|
||||
"github.com/seaweedfs/seaweedfs/weed/storage/types"
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
// mergeIdleTimeoutSeconds is the timeout for idle streams during needle tailing.
|
||||
// This ensures that slow or stalled streams don't block the merge indefinitely.
|
||||
// Set to 5 seconds to handle network congestion and avoid premature stream termination.
|
||||
// Can be made configurable in the future if needed for different deployment scenarios.
|
||||
const mergeIdleTimeoutSeconds = 5
|
||||
|
||||
// mergeDeduplicationWindowNs defines the time window for deduplication across replicas.
|
||||
// Since the same needle ID can have different timestamps on different servers due to
|
||||
// clock skew and replication lag, we deduplicate needles with the same ID within this window.
|
||||
// Set to 5 seconds in nanoseconds to handle typical server clock differences.
|
||||
const mergeDeduplicationWindowNs = 5 * time.Second
|
||||
|
||||
func init() {
|
||||
Commands = append(Commands, &commandVolumeMerge{})
|
||||
}
|
||||
|
||||
type commandVolumeMerge struct{}
|
||||
|
||||
func (c *commandVolumeMerge) Name() string {
|
||||
return "volume.merge"
|
||||
}
|
||||
|
||||
func (c *commandVolumeMerge) Help() string {
|
||||
return `merge replicas for a volume id in timestamp order into a fresh copy
|
||||
|
||||
volume.merge -volumeId <volume id>
|
||||
|
||||
This command:
|
||||
1) marks the volume readonly on replicas (if not already)
|
||||
2) allocates a temporary copy on a third location
|
||||
3) merges replicas in append timestamp order, skipping duplicates
|
||||
4) verifies the merged copy is not short before touching the replicas
|
||||
5) replaces the original replicas with the merged volume
|
||||
6) restores writable state if it was writable before
|
||||
`
|
||||
}
|
||||
|
||||
func (c *commandVolumeMerge) HasTag(CommandTag) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (c *commandVolumeMerge) Do(args []string, commandEnv *CommandEnv, writer io.Writer) (err error) {
|
||||
mergeCommand := flag.NewFlagSet(c.Name(), flag.ContinueOnError)
|
||||
volumeIdInt := mergeCommand.Int("volumeId", 0, "the volume id")
|
||||
targetNodeStr := mergeCommand.String("target", "", "optional target volume server <host>:<port> for temporary merge output")
|
||||
noLock := mergeCommand.Bool("noLock", false, "do not lock the admin shell at one's own risk")
|
||||
if err = mergeCommand.Parse(args); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if *volumeIdInt == 0 {
|
||||
return fmt.Errorf("volumeId is required")
|
||||
}
|
||||
|
||||
if *noLock {
|
||||
commandEnv.noLock = true
|
||||
} else if err = commandEnv.confirmIsLocked(args); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
volumeId := needle.VolumeId(*volumeIdInt)
|
||||
|
||||
topologyInfo, _, err := collectTopologyInfo(commandEnv, 0)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
volumeReplicas, allLocations := collectVolumeReplicaLocations(topologyInfo)
|
||||
replicas := volumeReplicas[uint32(volumeId)]
|
||||
if len(replicas) < 2 {
|
||||
return fmt.Errorf("volume %d has %d replica(s); merge requires at least two", volumeId, len(replicas))
|
||||
}
|
||||
|
||||
volumeInfo := replicas[0].info
|
||||
replicaPlacement, err := super_block.NewReplicaPlacementFromByte(byte(volumeInfo.ReplicaPlacement))
|
||||
if err != nil {
|
||||
return fmt.Errorf("parse replica placement for volume %d: %w", volumeId, err)
|
||||
}
|
||||
|
||||
var targetServer pb.ServerAddress
|
||||
if *targetNodeStr != "" {
|
||||
targetServer = pb.ServerAddress(*targetNodeStr)
|
||||
if isReplicaServer(targetServer, replicas) {
|
||||
return fmt.Errorf("target %s already hosts volume %d", *targetNodeStr, volumeId)
|
||||
}
|
||||
if err = allocateMergeVolume(commandEnv.option.GrpcDialOption, targetServer, volumeInfo, replicaPlacement); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
targetServer, err = allocateMergeVolumeOnThirdLocation(commandEnv.option.GrpcDialOption, allLocations, replicas, volumeInfo, replicaPlacement)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
cleanupTarget := true
|
||||
defer func() {
|
||||
if !cleanupTarget {
|
||||
return
|
||||
}
|
||||
if delErr := deleteVolume(context.Background(), commandEnv.option.GrpcDialOption, volumeId, targetServer, false, false); delErr != nil {
|
||||
glog.Warningf("failed to clean up temporary merge volume %d on %s: %v", volumeId, targetServer, delErr)
|
||||
}
|
||||
}()
|
||||
|
||||
writableReplicaIndices, err := ensureVolumeReadonly(commandEnv, replicas)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(writableReplicaIndices) > 0 {
|
||||
defer func() {
|
||||
// Only restore writable state for replicas that were originally writable
|
||||
writableReplicas := make([]*VolumeReplica, 0, len(writableReplicaIndices))
|
||||
for _, idx := range writableReplicaIndices {
|
||||
writableReplicas = append(writableReplicas, replicas[idx])
|
||||
}
|
||||
if restoreErr := markReplicasWritable(commandEnv.option.GrpcDialOption, writableReplicas, true, false); restoreErr != nil {
|
||||
glog.Warningf("failed to restore writable state for volume %d: %v", volumeId, restoreErr)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
done := make(chan struct{})
|
||||
defer close(done)
|
||||
|
||||
sources := make([]needleStream, 0, len(replicas))
|
||||
for _, replica := range replicas {
|
||||
server := pb.NewServerAddressFromDataNode(replica.location.dataNode)
|
||||
sources = append(sources, startTailNeedleStream(commandEnv.option.GrpcDialOption, volumeId, server, done))
|
||||
}
|
||||
|
||||
mergeErr := operation.WithVolumeServerClient(false, targetServer, commandEnv.option.GrpcDialOption, func(client volume_server_pb.VolumeServerClient) error {
|
||||
version := needle.Version(volumeInfo.Version)
|
||||
if version == 0 {
|
||||
version = needle.GetCurrentVersion()
|
||||
}
|
||||
return mergeNeedleStreams(sources, func(streamIndex int, n *needle.Needle) error {
|
||||
blob, size, err := needleBlobFromNeedle(n, version)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = client.WriteNeedleBlob(context.Background(), &volume_server_pb.WriteNeedleBlobRequest{
|
||||
VolumeId: uint32(volumeId),
|
||||
NeedleId: uint64(n.Id),
|
||||
Size: int32(size),
|
||||
NeedleBlob: blob,
|
||||
})
|
||||
return err
|
||||
})
|
||||
})
|
||||
if mergeErr != nil {
|
||||
return mergeErr
|
||||
}
|
||||
|
||||
// Verify the merged copy before overwriting any replica. A short or empty
|
||||
// merge stamped over every replica at once is unrecoverable; on failure the
|
||||
// originals are left intact and the bad merged copy is cleaned up.
|
||||
if err = verifyMergedVolume(commandEnv.option.GrpcDialOption, volumeId, targetServer, replicas); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// The merged copy is now the only authoritative copy. Keep it until every
|
||||
// replica is rebuilt from it so a mid-loop failure can still be finished.
|
||||
cleanupTarget = false
|
||||
|
||||
for i, replica := range replicas {
|
||||
sourceServer := pb.NewServerAddressFromDataNode(replica.location.dataNode)
|
||||
if _, err = copyVolume(context.Background(), commandEnv.option.GrpcDialOption, writer, volumeId, targetServer, sourceServer, "", 0, false); err != nil {
|
||||
return fmt.Errorf("rebuild replica %d/%d on %s from merged volume %d: %w; merged copy kept on %s, re-run to finish", i+1, len(replicas), sourceServer, volumeId, err, targetServer)
|
||||
}
|
||||
}
|
||||
|
||||
if err = deleteVolume(context.Background(), commandEnv.option.GrpcDialOption, volumeId, targetServer, false, false); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Fprintf(writer, "merged volume %d from %d replicas via %s\n", volumeId, len(replicas), targetServer)
|
||||
return nil
|
||||
}
|
||||
|
||||
type needleStream interface {
|
||||
Next() (*needle.Needle, bool)
|
||||
Err() error
|
||||
}
|
||||
|
||||
type tailNeedleStream struct {
|
||||
ch <-chan *needle.Needle
|
||||
errMu sync.Mutex
|
||||
err error
|
||||
}
|
||||
|
||||
func (s *tailNeedleStream) Next() (*needle.Needle, bool) {
|
||||
n, ok := <-s.ch
|
||||
return n, ok
|
||||
}
|
||||
|
||||
func (s *tailNeedleStream) Err() error {
|
||||
s.errMu.Lock()
|
||||
defer s.errMu.Unlock()
|
||||
return s.err
|
||||
}
|
||||
|
||||
func (s *tailNeedleStream) setErr(err error) {
|
||||
s.errMu.Lock()
|
||||
s.err = err
|
||||
s.errMu.Unlock()
|
||||
}
|
||||
|
||||
func startTailNeedleStream(grpcDialOption grpc.DialOption, volumeId needle.VolumeId, server pb.ServerAddress, done <-chan struct{}) *tailNeedleStream {
|
||||
ch := make(chan *needle.Needle, 32)
|
||||
stream := &tailNeedleStream{ch: ch}
|
||||
go func() {
|
||||
err := operation.TailVolumeFromSource(server, grpcDialOption, volumeId, 0, mergeIdleTimeoutSeconds, func(n *needle.Needle) error {
|
||||
select {
|
||||
case ch <- n:
|
||||
case <-done:
|
||||
return fmt.Errorf("merge cancelled")
|
||||
}
|
||||
return nil
|
||||
})
|
||||
close(ch)
|
||||
stream.setErr(err)
|
||||
}()
|
||||
return stream
|
||||
}
|
||||
|
||||
type needleMergeItem struct {
|
||||
streamIndex int
|
||||
needle *needle.Needle
|
||||
timestamp uint64
|
||||
}
|
||||
|
||||
type needleMergeHeap []needleMergeItem
|
||||
|
||||
func (h needleMergeHeap) Len() int { return len(h) }
|
||||
func (h needleMergeHeap) Less(i, j int) bool {
|
||||
if h[i].timestamp == h[j].timestamp {
|
||||
return h[i].needle.Id < h[j].needle.Id
|
||||
}
|
||||
return h[i].timestamp < h[j].timestamp
|
||||
}
|
||||
func (h needleMergeHeap) Swap(i, j int) { h[i], h[j] = h[j], h[i] }
|
||||
func (h *needleMergeHeap) Push(x any) {
|
||||
*h = append(*h, x.(needleMergeItem))
|
||||
}
|
||||
func (h *needleMergeHeap) Pop() any {
|
||||
old := *h
|
||||
n := len(old)
|
||||
item := old[n-1]
|
||||
*h = old[:n-1]
|
||||
return item
|
||||
}
|
||||
|
||||
func mergeNeedleStreams(streams []needleStream, consume func(int, *needle.Needle) error) error {
|
||||
h := &needleMergeHeap{}
|
||||
heap.Init(h)
|
||||
|
||||
for i, stream := range streams {
|
||||
if n, ok := stream.Next(); ok {
|
||||
heap.Push(h, needleMergeItem{streamIndex: i, needle: n, timestamp: needleTimestamp(n)})
|
||||
}
|
||||
}
|
||||
|
||||
// Track seen needle IDs (by stream) within a time window to skip cross-stream duplicates only.
|
||||
// Needles with the same ID from different streams within mergeDeduplicationWindowNs are considered
|
||||
// cross-stream duplicates and skipped. Same-stream duplicates (overwrites) are kept.
|
||||
// Map: needleId -> streamIndex that first processed it in this window
|
||||
seenAtTimestamp := make(map[types.NeedleId]int)
|
||||
var windowStartTimestamp uint64
|
||||
windowInitialized := false
|
||||
|
||||
for h.Len() > 0 {
|
||||
item := heap.Pop(h).(needleMergeItem)
|
||||
ts := item.timestamp
|
||||
n := item.needle
|
||||
|
||||
// Initialize window on first timestamp, or move to new window when outside current window
|
||||
if !windowInitialized {
|
||||
windowStartTimestamp = ts
|
||||
windowInitialized = true
|
||||
} else if ts > windowStartTimestamp+uint64(mergeDeduplicationWindowNs) {
|
||||
// Moving to a new window: clear the watermark to reduce memory usage.
|
||||
// This is safe because we only skip duplicates within the same time window.
|
||||
seenAtTimestamp = make(map[types.NeedleId]int)
|
||||
windowStartTimestamp = ts
|
||||
}
|
||||
|
||||
// Skip cross-stream duplicates: if we've already seen this needle ID from a DIFFERENT stream
|
||||
// within this time window, skip it. Same-stream duplicates (overwrites) are kept.
|
||||
if seenStreamIdx, exists := seenAtTimestamp[n.Id]; exists && seenStreamIdx != item.streamIndex {
|
||||
// Cross-stream duplicate from different stream - skip this occurrence
|
||||
if nextN, ok := streams[item.streamIndex].Next(); ok {
|
||||
heap.Push(h, needleMergeItem{streamIndex: item.streamIndex, needle: nextN, timestamp: needleTimestamp(nextN)})
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// Record this stream's occurrence of this needle ID in this window
|
||||
// (overwrite if from same stream, since we process in timestamp order)
|
||||
seenAtTimestamp[n.Id] = item.streamIndex
|
||||
if err := consume(item.streamIndex, n); err != nil {
|
||||
return err
|
||||
}
|
||||
if nextN, ok := streams[item.streamIndex].Next(); ok {
|
||||
heap.Push(h, needleMergeItem{streamIndex: item.streamIndex, needle: nextN, timestamp: needleTimestamp(nextN)})
|
||||
}
|
||||
}
|
||||
|
||||
for _, stream := range streams {
|
||||
if err := stream.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func needleTimestamp(n *needle.Needle) uint64 {
|
||||
if n.AppendAtNs != 0 {
|
||||
return n.AppendAtNs
|
||||
}
|
||||
if n.LastModified != 0 {
|
||||
return uint64(time.Unix(int64(n.LastModified), 0).UnixNano())
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// memoryBackendFile implements backend.BackendStorageFile using an in-memory buffer
|
||||
type memoryBackendFile struct {
|
||||
buf *bytes.Buffer
|
||||
}
|
||||
|
||||
func (m *memoryBackendFile) ReadAt(p []byte, off int64) (n int, err error) {
|
||||
data := m.buf.Bytes()
|
||||
if off >= int64(len(data)) {
|
||||
return 0, io.EOF
|
||||
}
|
||||
n = copy(p, data[off:])
|
||||
if off+int64(n) < int64(len(data)) {
|
||||
return n, nil
|
||||
}
|
||||
return n, io.EOF
|
||||
}
|
||||
|
||||
func (m *memoryBackendFile) WriteAt(p []byte, off int64) (n int, err error) {
|
||||
data := m.buf.Bytes()
|
||||
if off > int64(len(data)) {
|
||||
// Pad with zeros
|
||||
m.buf.Write(make([]byte, off-int64(len(data))))
|
||||
// Refresh data snapshot after padding to see the padded length
|
||||
data = m.buf.Bytes()
|
||||
}
|
||||
if off == int64(len(data)) {
|
||||
return m.buf.Write(p)
|
||||
}
|
||||
// Overwrite existing data: preserve any trailing bytes beyond the write range
|
||||
newLen := off + int64(len(p))
|
||||
if newLen < int64(len(data)) {
|
||||
newLen = int64(len(data))
|
||||
}
|
||||
newData := make([]byte, newLen)
|
||||
copy(newData, data)
|
||||
copy(newData[off:], p)
|
||||
m.buf = bytes.NewBuffer(newData)
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
func (m *memoryBackendFile) Truncate(off int64) error {
|
||||
data := m.buf.Bytes()
|
||||
if off > int64(len(data)) {
|
||||
m.buf.Write(make([]byte, off-int64(len(data))))
|
||||
} else {
|
||||
m.buf = bytes.NewBuffer(data[:off])
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *memoryBackendFile) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *memoryBackendFile) GetStat() (datSize int64, modTime time.Time, err error) {
|
||||
return int64(m.buf.Len()), time.Now(), nil
|
||||
}
|
||||
|
||||
func (m *memoryBackendFile) Name() string {
|
||||
return "memory"
|
||||
}
|
||||
|
||||
func (m *memoryBackendFile) Sync() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func newMemoryBackendFile() *memoryBackendFile {
|
||||
return &memoryBackendFile{
|
||||
buf: &bytes.Buffer{},
|
||||
}
|
||||
}
|
||||
|
||||
func needleBlobFromNeedle(n *needle.Needle, version needle.Version) ([]byte, types.Size, error) {
|
||||
// Use in-memory buffer for serialization to avoid expensive temporary file I/O
|
||||
memFile := newMemoryBackendFile()
|
||||
defer memFile.Close()
|
||||
|
||||
// Append reports Size(n.DataSize); the .dat header and the needle map both use
|
||||
// n.Size, which Append fills in as it serializes.
|
||||
_, _, actualSize, err := n.Append(memFile, version)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
buf := make([]byte, actualSize)
|
||||
read, err := memFile.ReadAt(buf, 0)
|
||||
if err != nil && err != io.EOF {
|
||||
return nil, 0, err
|
||||
}
|
||||
return buf[:read], n.Size, nil
|
||||
}
|
||||
|
||||
func allocateMergeVolumeOnThirdLocation(grpcDialOption grpc.DialOption, allLocations []location, replicas []*VolumeReplica, info *master_pb.VolumeInformationMessage, replicaPlacement *super_block.ReplicaPlacement) (pb.ServerAddress, error) {
|
||||
replicaNodes := map[string]struct{}{}
|
||||
for _, replica := range replicas {
|
||||
replicaNodes[replica.location.dataNode.Id] = struct{}{}
|
||||
}
|
||||
|
||||
for _, loc := range allLocations {
|
||||
if _, exists := replicaNodes[loc.dataNode.Id]; exists {
|
||||
continue
|
||||
}
|
||||
if !locationHasDiskType(loc, info.DiskType) {
|
||||
continue
|
||||
}
|
||||
server := pb.NewServerAddressFromDataNode(loc.dataNode)
|
||||
if err := allocateMergeVolume(grpcDialOption, server, info, replicaPlacement); err != nil {
|
||||
glog.V(1).Infof("failed to allocate merge volume on %s with replication %s: %v", server, replicaPlacement.String(), err)
|
||||
continue
|
||||
}
|
||||
return server, nil
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("no third location available to merge volume %d", info.Id)
|
||||
}
|
||||
|
||||
func allocateMergeVolume(grpcDialOption grpc.DialOption, server pb.ServerAddress, info *master_pb.VolumeInformationMessage, replicaPlacement *super_block.ReplicaPlacement) error {
|
||||
return operation.WithVolumeServerClient(false, server, grpcDialOption, func(client volume_server_pb.VolumeServerClient) error {
|
||||
_, err := client.AllocateVolume(context.Background(), &volume_server_pb.AllocateVolumeRequest{
|
||||
VolumeId: info.Id,
|
||||
Collection: info.Collection,
|
||||
Preallocate: 0,
|
||||
Replication: replicaPlacement.String(),
|
||||
Ttl: needle.LoadTTLFromUint32(info.Ttl).String(),
|
||||
DiskType: info.DiskType,
|
||||
Version: info.Version,
|
||||
})
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
// ensureVolumeReadonly marks all replicas as readonly and returns the indices of replicas that were writable
|
||||
func ensureVolumeReadonly(commandEnv *CommandEnv, replicas []*VolumeReplica) ([]int, error) {
|
||||
var writableReplicaIndices []int
|
||||
for i, replica := range replicas {
|
||||
server := pb.NewServerAddressFromDataNode(replica.location.dataNode)
|
||||
status, err := readVolumeStatus(commandEnv.option.GrpcDialOption, server, needle.VolumeId(replica.info.Id))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !status.IsReadOnly {
|
||||
writableReplicaIndices = append(writableReplicaIndices, i)
|
||||
}
|
||||
}
|
||||
if len(writableReplicaIndices) > 0 {
|
||||
if err := markReplicasWritable(commandEnv.option.GrpcDialOption, replicas, false, false); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return writableReplicaIndices, nil
|
||||
}
|
||||
|
||||
// verifyMergedVolume checks the freshly merged copy is at least as complete as the
|
||||
// most complete source replica before the originals are overwritten.
|
||||
func verifyMergedVolume(grpcDialOption grpc.DialOption, volumeId needle.VolumeId, targetServer pb.ServerAddress, replicas []*VolumeReplica) error {
|
||||
merged, err := readVolumeStatus(grpcDialOption, targetServer, volumeId)
|
||||
if err != nil {
|
||||
return fmt.Errorf("read merged volume %d on %s: %w", volumeId, targetServer, err)
|
||||
}
|
||||
replicaLive := make(map[pb.ServerAddress]uint64, len(replicas))
|
||||
for _, replica := range replicas {
|
||||
server := pb.NewServerAddressFromDataNode(replica.location.dataNode)
|
||||
status, err := readVolumeStatus(grpcDialOption, server, volumeId)
|
||||
if err != nil {
|
||||
return fmt.Errorf("read replica volume %d on %s: %w", volumeId, server, err)
|
||||
}
|
||||
replicaLive[server] = liveNeedleCount(status)
|
||||
}
|
||||
return evaluateMergedVolume(volumeId, liveNeedleCount(merged), replicaLive)
|
||||
}
|
||||
|
||||
// evaluateMergedVolume reports why a merged copy with mergedLive live needles is
|
||||
// unsafe to overwrite replicas whose live counts are replicaLive. The merge is the
|
||||
// union of every replica's needles, so the merged copy must be non-empty and hold
|
||||
// at least as many live needles as the most complete replica.
|
||||
func evaluateMergedVolume(volumeId needle.VolumeId, mergedLive uint64, replicaLive map[pb.ServerAddress]uint64) error {
|
||||
if mergedLive == 0 {
|
||||
return fmt.Errorf("merged volume %d is empty; keeping original replicas", volumeId)
|
||||
}
|
||||
var maxLive uint64
|
||||
var maxFrom pb.ServerAddress
|
||||
for server, live := range replicaLive {
|
||||
if live > maxLive {
|
||||
maxLive, maxFrom = live, server
|
||||
}
|
||||
}
|
||||
if mergedLive < maxLive {
|
||||
return fmt.Errorf("merged volume %d has %d live needles, fewer than replica %s with %d; refusing to overwrite replicas", volumeId, mergedLive, maxFrom, maxLive)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func liveNeedleCount(status *volume_server_pb.VolumeStatusResponse) uint64 {
|
||||
if status == nil || status.FileCount < status.FileDeletedCount {
|
||||
return 0
|
||||
}
|
||||
return status.FileCount - status.FileDeletedCount
|
||||
}
|
||||
|
||||
func readVolumeStatus(grpcDialOption grpc.DialOption, server pb.ServerAddress, volumeId needle.VolumeId) (*volume_server_pb.VolumeStatusResponse, error) {
|
||||
var resp *volume_server_pb.VolumeStatusResponse
|
||||
err := operation.WithVolumeServerClient(false, server, grpcDialOption, func(client volume_server_pb.VolumeServerClient) error {
|
||||
r, statusErr := client.VolumeStatus(context.Background(), &volume_server_pb.VolumeStatusRequest{VolumeId: uint32(volumeId)})
|
||||
if statusErr != nil {
|
||||
return statusErr
|
||||
}
|
||||
resp = r
|
||||
return nil
|
||||
})
|
||||
if err == nil && resp == nil {
|
||||
return nil, fmt.Errorf("empty volume status response from %s", server)
|
||||
}
|
||||
return resp, err
|
||||
}
|
||||
|
||||
func isReplicaServer(target pb.ServerAddress, replicas []*VolumeReplica) bool {
|
||||
for _, replica := range replicas {
|
||||
if pb.NewServerAddressFromDataNode(replica.location.dataNode).Equals(target) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func locationHasDiskType(loc location, diskType string) bool {
|
||||
for _, diskInfo := range loc.dataNode.DiskInfos {
|
||||
if diskInfo.Type == diskType {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func markReplicasWritable(grpcDialOption grpc.DialOption, replicas []*VolumeReplica, writable bool, persist bool) error {
|
||||
for _, replica := range replicas {
|
||||
server := pb.NewServerAddressFromDataNode(replica.location.dataNode)
|
||||
err := operation.WithVolumeServerClient(false, server, grpcDialOption, func(client volume_server_pb.VolumeServerClient) error {
|
||||
if writable {
|
||||
_, err := client.VolumeMarkWritable(context.Background(), &volume_server_pb.VolumeMarkWritableRequest{VolumeId: replica.info.Id})
|
||||
return err
|
||||
}
|
||||
_, err := client.VolumeMarkReadonly(context.Background(), &volume_server_pb.VolumeMarkReadonlyRequest{VolumeId: replica.info.Id, Persist: persist})
|
||||
return err
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user