diff --git a/weed/command/master.go b/weed/command/master.go
index 565c9cc581..6be079c991 100644
--- a/weed/command/master.go
+++ b/weed/command/master.go
@@ -71,8 +71,9 @@ type MasterOptions struct {
raftBootstrap *bool
telemetryUrl *string
telemetryEnabled *bool
- debug *bool
- debugPort *int
+ debug *bool
+ debugPort *int
+ blockPromotionLSNTolerance *int
}
func init() {
@@ -104,6 +105,7 @@ func init() {
m.telemetryEnabled = cmdMaster.Flag.Bool("telemetry", false, "enable telemetry reporting")
m.debug = cmdMaster.Flag.Bool("debug", false, "serves runtime profiling data via pprof on the port specified by -debug.port")
m.debugPort = cmdMaster.Flag.Int("debug.port", 6060, "http port for debugging")
+ m.blockPromotionLSNTolerance = cmdMaster.Flag.Int("block.promotion.lsnTolerance", 100, "max WAL LSN lag for block volume replica promotion eligibility")
}
var cmdMaster = &Command{
@@ -410,7 +412,8 @@ func (m *MasterOptions) toMasterOption(whiteList []string) *weed_server.MasterOp
DisableHttp: *m.disableHttp,
MetricsAddress: *m.metricsAddress,
MetricsIntervalSec: *m.metricsIntervalSec,
- TelemetryUrl: *m.telemetryUrl,
- TelemetryEnabled: *m.telemetryEnabled,
+ TelemetryUrl: *m.telemetryUrl,
+ TelemetryEnabled: *m.telemetryEnabled,
+ BlockPromotionLSNTolerance: *m.blockPromotionLSNTolerance,
}
}
diff --git a/weed/command/volume.go b/weed/command/volume.go
index 3ec6fce728..5d607d269b 100644
--- a/weed/command/volume.go
+++ b/weed/command/volume.go
@@ -310,6 +310,9 @@ func (v VolumeServerOptions) startVolumeServer(volumeFolders, maxVolumeCounts, v
// Start block volume iSCSI service (disabled if block.dir is empty).
blockService := weed_server.StartBlockService(*v.blockListen, *v.blockDir, *v.blockIQNPrefix)
+ if blockService != nil {
+ volumeServer.SetBlockService(blockService)
+ }
grace.OnReload(volumeServer.LoadNewVolumes)
grace.OnReload(volumeServer.Reload)
diff --git a/weed/pb/master.proto b/weed/pb/master.proto
index 7673b0a9a5..1332e5119f 100644
--- a/weed/pb/master.proto
+++ b/weed/pb/master.proto
@@ -63,6 +63,14 @@ service Seaweed {
}
rpc LookupBlockVolume (LookupBlockVolumeRequest) returns (LookupBlockVolumeResponse) {
}
+ rpc CreateBlockSnapshot (CreateBlockSnapshotRequest) returns (CreateBlockSnapshotResponse) {
+ }
+ rpc DeleteBlockSnapshot (DeleteBlockSnapshotRequest) returns (DeleteBlockSnapshotResponse) {
+ }
+ rpc ListBlockSnapshots (ListBlockSnapshotsRequest) returns (ListBlockSnapshotsResponse) {
+ }
+ rpc ExpandBlockVolume (ExpandBlockVolumeRequest) returns (ExpandBlockVolumeResponse) {
+ }
}
//////////////////////////////////////////////////
@@ -493,6 +501,10 @@ message BlockVolumeInfoMessage {
string disk_type = 9;
string replica_data_addr = 10;
string replica_ctrl_addr = 11;
+ double health_score = 12;
+ int64 scrub_errors = 13;
+ int64 last_scrub_time = 14;
+ bool replica_degraded = 15;
}
message BlockVolumeShortInfoMessage {
@@ -510,12 +522,19 @@ message BlockVolumeAssignment {
string replica_data_addr = 5;
string replica_ctrl_addr = 6;
string rebuild_addr = 7;
+ repeated ReplicaAddrMessage replica_addrs = 8;
+}
+
+message ReplicaAddrMessage {
+ string data_addr = 1;
+ string ctrl_addr = 2;
}
message CreateBlockVolumeRequest {
string name = 1;
uint64 size_bytes = 2;
string disk_type = 3;
+ uint32 replica_factor = 4;
}
message CreateBlockVolumeResponse {
string volume_id = 1;
@@ -524,6 +543,7 @@ message CreateBlockVolumeResponse {
string iqn = 4;
uint64 capacity_bytes = 5;
string replica_server = 6;
+ repeated string replica_servers = 7;
}
message DeleteBlockVolumeRequest {
@@ -541,4 +561,43 @@ message LookupBlockVolumeResponse {
string iqn = 3;
uint64 capacity_bytes = 4;
string replica_server = 5;
+ uint32 replica_factor = 6;
+ repeated string replica_servers = 7;
+}
+
+message CreateBlockSnapshotRequest {
+ string volume_name = 1;
+ uint32 snapshot_id = 2;
+}
+message CreateBlockSnapshotResponse {
+ uint32 snapshot_id = 1;
+ int64 created_at = 2; // Unix timestamp in seconds
+ uint64 size_bytes = 3;
+}
+
+message DeleteBlockSnapshotRequest {
+ string volume_name = 1;
+ uint32 snapshot_id = 2;
+}
+message DeleteBlockSnapshotResponse {
+}
+
+message ListBlockSnapshotsRequest {
+ string volume_name = 1;
+}
+message ListBlockSnapshotsResponse {
+ repeated BlockSnapshotInfo snapshots = 1;
+}
+message BlockSnapshotInfo {
+ uint32 snapshot_id = 1;
+ int64 created_at = 2; // Unix timestamp in seconds
+ uint64 volume_size_bytes = 3;
+}
+
+message ExpandBlockVolumeRequest {
+ string name = 1;
+ uint64 new_size_bytes = 2;
+}
+message ExpandBlockVolumeResponse {
+ uint64 capacity_bytes = 1;
}
\ No newline at end of file
diff --git a/weed/pb/master_pb/master.pb.go b/weed/pb/master_pb/master.pb.go
index 99b37ff813..0d16c4646e 100644
--- a/weed/pb/master_pb/master.pb.go
+++ b/weed/pb/master_pb/master.pb.go
@@ -3895,6 +3895,10 @@ type BlockVolumeInfoMessage struct {
DiskType string `protobuf:"bytes,9,opt,name=disk_type,json=diskType,proto3" json:"disk_type,omitempty"`
ReplicaDataAddr string `protobuf:"bytes,10,opt,name=replica_data_addr,json=replicaDataAddr,proto3" json:"replica_data_addr,omitempty"`
ReplicaCtrlAddr string `protobuf:"bytes,11,opt,name=replica_ctrl_addr,json=replicaCtrlAddr,proto3" json:"replica_ctrl_addr,omitempty"`
+ HealthScore float64 `protobuf:"fixed64,12,opt,name=health_score,json=healthScore,proto3" json:"health_score,omitempty"`
+ ScrubErrors int64 `protobuf:"varint,13,opt,name=scrub_errors,json=scrubErrors,proto3" json:"scrub_errors,omitempty"`
+ LastScrubTime int64 `protobuf:"varint,14,opt,name=last_scrub_time,json=lastScrubTime,proto3" json:"last_scrub_time,omitempty"`
+ ReplicaDegraded bool `protobuf:"varint,15,opt,name=replica_degraded,json=replicaDegraded,proto3" json:"replica_degraded,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
@@ -4006,6 +4010,34 @@ func (x *BlockVolumeInfoMessage) GetReplicaCtrlAddr() string {
return ""
}
+func (x *BlockVolumeInfoMessage) GetHealthScore() float64 {
+ if x != nil {
+ return x.HealthScore
+ }
+ return 0
+}
+
+func (x *BlockVolumeInfoMessage) GetScrubErrors() int64 {
+ if x != nil {
+ return x.ScrubErrors
+ }
+ return 0
+}
+
+func (x *BlockVolumeInfoMessage) GetLastScrubTime() int64 {
+ if x != nil {
+ return x.LastScrubTime
+ }
+ return 0
+}
+
+func (x *BlockVolumeInfoMessage) GetReplicaDegraded() bool {
+ if x != nil {
+ return x.ReplicaDegraded
+ }
+ return false
+}
+
type BlockVolumeShortInfoMessage struct {
state protoimpl.MessageState `protogen:"open.v1"`
Path string `protobuf:"bytes,1,opt,name=path,proto3" json:"path,omitempty"`
@@ -4083,6 +4115,7 @@ type BlockVolumeAssignment struct {
ReplicaDataAddr string `protobuf:"bytes,5,opt,name=replica_data_addr,json=replicaDataAddr,proto3" json:"replica_data_addr,omitempty"`
ReplicaCtrlAddr string `protobuf:"bytes,6,opt,name=replica_ctrl_addr,json=replicaCtrlAddr,proto3" json:"replica_ctrl_addr,omitempty"`
RebuildAddr string `protobuf:"bytes,7,opt,name=rebuild_addr,json=rebuildAddr,proto3" json:"rebuild_addr,omitempty"`
+ ReplicaAddrs []*ReplicaAddrMessage `protobuf:"bytes,8,rep,name=replica_addrs,json=replicaAddrs,proto3" json:"replica_addrs,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
@@ -4166,11 +4199,46 @@ func (x *BlockVolumeAssignment) GetRebuildAddr() string {
return ""
}
+func (x *BlockVolumeAssignment) GetReplicaAddrs() []*ReplicaAddrMessage {
+ if x != nil {
+ return x.ReplicaAddrs
+ }
+ return nil
+}
+
+// ReplicaAddrMessage holds data and control addresses for one replica.
+type ReplicaAddrMessage struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ DataAddr string `protobuf:"bytes,1,opt,name=data_addr,json=dataAddr,proto3" json:"data_addr,omitempty"`
+ CtrlAddr string `protobuf:"bytes,2,opt,name=ctrl_addr,json=ctrlAddr,proto3" json:"ctrl_addr,omitempty"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *ReplicaAddrMessage) Reset() { *x = ReplicaAddrMessage{} }
+func (x *ReplicaAddrMessage) String() string { return "ReplicaAddr{" + x.DataAddr + "," + x.CtrlAddr + "}" }
+func (*ReplicaAddrMessage) ProtoMessage() {}
+
+func (x *ReplicaAddrMessage) GetDataAddr() string {
+ if x != nil {
+ return x.DataAddr
+ }
+ return ""
+}
+
+func (x *ReplicaAddrMessage) GetCtrlAddr() string {
+ if x != nil {
+ return x.CtrlAddr
+ }
+ return ""
+}
+
type CreateBlockVolumeRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
SizeBytes uint64 `protobuf:"varint,2,opt,name=size_bytes,json=sizeBytes,proto3" json:"size_bytes,omitempty"`
DiskType string `protobuf:"bytes,3,opt,name=disk_type,json=diskType,proto3" json:"disk_type,omitempty"`
+ ReplicaFactor uint32 `protobuf:"varint,4,opt,name=replica_factor,json=replicaFactor,proto3" json:"replica_factor,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
@@ -4226,6 +4294,13 @@ func (x *CreateBlockVolumeRequest) GetDiskType() string {
return ""
}
+func (x *CreateBlockVolumeRequest) GetReplicaFactor() uint32 {
+ if x != nil {
+ return x.ReplicaFactor
+ }
+ return 0
+}
+
type CreateBlockVolumeResponse struct {
state protoimpl.MessageState `protogen:"open.v1"`
VolumeId string `protobuf:"bytes,1,opt,name=volume_id,json=volumeId,proto3" json:"volume_id,omitempty"`
@@ -4233,9 +4308,10 @@ type CreateBlockVolumeResponse struct {
IscsiAddr string `protobuf:"bytes,3,opt,name=iscsi_addr,json=iscsiAddr,proto3" json:"iscsi_addr,omitempty"`
Iqn string `protobuf:"bytes,4,opt,name=iqn,proto3" json:"iqn,omitempty"`
CapacityBytes uint64 `protobuf:"varint,5,opt,name=capacity_bytes,json=capacityBytes,proto3" json:"capacity_bytes,omitempty"`
- ReplicaServer string `protobuf:"bytes,6,opt,name=replica_server,json=replicaServer,proto3" json:"replica_server,omitempty"`
- unknownFields protoimpl.UnknownFields
- sizeCache protoimpl.SizeCache
+ ReplicaServer string `protobuf:"bytes,6,opt,name=replica_server,json=replicaServer,proto3" json:"replica_server,omitempty"`
+ ReplicaServers []string `protobuf:"bytes,7,rep,name=replica_servers,json=replicaServers,proto3" json:"replica_servers,omitempty"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
}
func (x *CreateBlockVolumeResponse) Reset() {
@@ -4310,6 +4386,13 @@ func (x *CreateBlockVolumeResponse) GetReplicaServer() string {
return ""
}
+func (x *CreateBlockVolumeResponse) GetReplicaServers() []string {
+ if x != nil {
+ return x.ReplicaServers
+ }
+ return nil
+}
+
type DeleteBlockVolumeRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
@@ -4435,14 +4518,16 @@ func (x *LookupBlockVolumeRequest) GetName() string {
}
type LookupBlockVolumeResponse struct {
- state protoimpl.MessageState `protogen:"open.v1"`
- VolumeServer string `protobuf:"bytes,1,opt,name=volume_server,json=volumeServer,proto3" json:"volume_server,omitempty"`
- IscsiAddr string `protobuf:"bytes,2,opt,name=iscsi_addr,json=iscsiAddr,proto3" json:"iscsi_addr,omitempty"`
- Iqn string `protobuf:"bytes,3,opt,name=iqn,proto3" json:"iqn,omitempty"`
- CapacityBytes uint64 `protobuf:"varint,4,opt,name=capacity_bytes,json=capacityBytes,proto3" json:"capacity_bytes,omitempty"`
- ReplicaServer string `protobuf:"bytes,5,opt,name=replica_server,json=replicaServer,proto3" json:"replica_server,omitempty"`
- unknownFields protoimpl.UnknownFields
- sizeCache protoimpl.SizeCache
+ state protoimpl.MessageState `protogen:"open.v1"`
+ VolumeServer string `protobuf:"bytes,1,opt,name=volume_server,json=volumeServer,proto3" json:"volume_server,omitempty"`
+ IscsiAddr string `protobuf:"bytes,2,opt,name=iscsi_addr,json=iscsiAddr,proto3" json:"iscsi_addr,omitempty"`
+ Iqn string `protobuf:"bytes,3,opt,name=iqn,proto3" json:"iqn,omitempty"`
+ CapacityBytes uint64 `protobuf:"varint,4,opt,name=capacity_bytes,json=capacityBytes,proto3" json:"capacity_bytes,omitempty"`
+ ReplicaServer string `protobuf:"bytes,5,opt,name=replica_server,json=replicaServer,proto3" json:"replica_server,omitempty"`
+ ReplicaFactor uint32 `protobuf:"varint,6,opt,name=replica_factor,json=replicaFactor,proto3" json:"replica_factor,omitempty"`
+ ReplicaServers []string `protobuf:"bytes,7,rep,name=replica_servers,json=replicaServers,proto3" json:"replica_servers,omitempty"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
}
func (x *LookupBlockVolumeResponse) Reset() {
@@ -4510,6 +4595,464 @@ func (x *LookupBlockVolumeResponse) GetReplicaServer() string {
return ""
}
+func (x *LookupBlockVolumeResponse) GetReplicaFactor() uint32 {
+ if x != nil {
+ return x.ReplicaFactor
+ }
+ return 0
+}
+
+func (x *LookupBlockVolumeResponse) GetReplicaServers() []string {
+ if x != nil {
+ return x.ReplicaServers
+ }
+ return nil
+}
+
+type CreateBlockSnapshotRequest struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ VolumeName string `protobuf:"bytes,1,opt,name=volume_name,json=volumeName,proto3" json:"volume_name,omitempty"`
+ SnapshotId uint32 `protobuf:"varint,2,opt,name=snapshot_id,json=snapshotId,proto3" json:"snapshot_id,omitempty"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *CreateBlockSnapshotRequest) Reset() {
+ *x = CreateBlockSnapshotRequest{}
+ mi := &file_master_proto_msgTypes[70]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *CreateBlockSnapshotRequest) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*CreateBlockSnapshotRequest) ProtoMessage() {}
+
+func (x *CreateBlockSnapshotRequest) ProtoReflect() protoreflect.Message {
+ mi := &file_master_proto_msgTypes[70]
+ if x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use CreateBlockSnapshotRequest.ProtoReflect.Descriptor instead.
+func (*CreateBlockSnapshotRequest) Descriptor() ([]byte, []int) {
+ return file_master_proto_rawDescGZIP(), []int{70}
+}
+
+func (x *CreateBlockSnapshotRequest) GetVolumeName() string {
+ if x != nil {
+ return x.VolumeName
+ }
+ return ""
+}
+
+func (x *CreateBlockSnapshotRequest) GetSnapshotId() uint32 {
+ if x != nil {
+ return x.SnapshotId
+ }
+ return 0
+}
+
+type CreateBlockSnapshotResponse struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ SnapshotId uint32 `protobuf:"varint,1,opt,name=snapshot_id,json=snapshotId,proto3" json:"snapshot_id,omitempty"`
+ CreatedAt int64 `protobuf:"varint,2,opt,name=created_at,json=createdAt,proto3" json:"created_at,omitempty"` // Unix timestamp in seconds
+ SizeBytes uint64 `protobuf:"varint,3,opt,name=size_bytes,json=sizeBytes,proto3" json:"size_bytes,omitempty"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *CreateBlockSnapshotResponse) Reset() {
+ *x = CreateBlockSnapshotResponse{}
+ mi := &file_master_proto_msgTypes[71]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *CreateBlockSnapshotResponse) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*CreateBlockSnapshotResponse) ProtoMessage() {}
+
+func (x *CreateBlockSnapshotResponse) ProtoReflect() protoreflect.Message {
+ mi := &file_master_proto_msgTypes[71]
+ if x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use CreateBlockSnapshotResponse.ProtoReflect.Descriptor instead.
+func (*CreateBlockSnapshotResponse) Descriptor() ([]byte, []int) {
+ return file_master_proto_rawDescGZIP(), []int{71}
+}
+
+func (x *CreateBlockSnapshotResponse) GetSnapshotId() uint32 {
+ if x != nil {
+ return x.SnapshotId
+ }
+ return 0
+}
+
+func (x *CreateBlockSnapshotResponse) GetCreatedAt() int64 {
+ if x != nil {
+ return x.CreatedAt
+ }
+ return 0
+}
+
+func (x *CreateBlockSnapshotResponse) GetSizeBytes() uint64 {
+ if x != nil {
+ return x.SizeBytes
+ }
+ return 0
+}
+
+type DeleteBlockSnapshotRequest struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ VolumeName string `protobuf:"bytes,1,opt,name=volume_name,json=volumeName,proto3" json:"volume_name,omitempty"`
+ SnapshotId uint32 `protobuf:"varint,2,opt,name=snapshot_id,json=snapshotId,proto3" json:"snapshot_id,omitempty"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *DeleteBlockSnapshotRequest) Reset() {
+ *x = DeleteBlockSnapshotRequest{}
+ mi := &file_master_proto_msgTypes[72]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *DeleteBlockSnapshotRequest) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*DeleteBlockSnapshotRequest) ProtoMessage() {}
+
+func (x *DeleteBlockSnapshotRequest) ProtoReflect() protoreflect.Message {
+ mi := &file_master_proto_msgTypes[72]
+ if x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use DeleteBlockSnapshotRequest.ProtoReflect.Descriptor instead.
+func (*DeleteBlockSnapshotRequest) Descriptor() ([]byte, []int) {
+ return file_master_proto_rawDescGZIP(), []int{72}
+}
+
+func (x *DeleteBlockSnapshotRequest) GetVolumeName() string {
+ if x != nil {
+ return x.VolumeName
+ }
+ return ""
+}
+
+func (x *DeleteBlockSnapshotRequest) GetSnapshotId() uint32 {
+ if x != nil {
+ return x.SnapshotId
+ }
+ return 0
+}
+
+type DeleteBlockSnapshotResponse struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *DeleteBlockSnapshotResponse) Reset() {
+ *x = DeleteBlockSnapshotResponse{}
+ mi := &file_master_proto_msgTypes[73]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *DeleteBlockSnapshotResponse) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*DeleteBlockSnapshotResponse) ProtoMessage() {}
+
+func (x *DeleteBlockSnapshotResponse) ProtoReflect() protoreflect.Message {
+ mi := &file_master_proto_msgTypes[73]
+ if x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use DeleteBlockSnapshotResponse.ProtoReflect.Descriptor instead.
+func (*DeleteBlockSnapshotResponse) Descriptor() ([]byte, []int) {
+ return file_master_proto_rawDescGZIP(), []int{73}
+}
+
+type ListBlockSnapshotsRequest struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ VolumeName string `protobuf:"bytes,1,opt,name=volume_name,json=volumeName,proto3" json:"volume_name,omitempty"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *ListBlockSnapshotsRequest) Reset() {
+ *x = ListBlockSnapshotsRequest{}
+ mi := &file_master_proto_msgTypes[74]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *ListBlockSnapshotsRequest) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*ListBlockSnapshotsRequest) ProtoMessage() {}
+
+func (x *ListBlockSnapshotsRequest) ProtoReflect() protoreflect.Message {
+ mi := &file_master_proto_msgTypes[74]
+ if x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use ListBlockSnapshotsRequest.ProtoReflect.Descriptor instead.
+func (*ListBlockSnapshotsRequest) Descriptor() ([]byte, []int) {
+ return file_master_proto_rawDescGZIP(), []int{74}
+}
+
+func (x *ListBlockSnapshotsRequest) GetVolumeName() string {
+ if x != nil {
+ return x.VolumeName
+ }
+ return ""
+}
+
+type ListBlockSnapshotsResponse struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ Snapshots []*BlockSnapshotInfo `protobuf:"bytes,1,rep,name=snapshots,proto3" json:"snapshots,omitempty"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *ListBlockSnapshotsResponse) Reset() {
+ *x = ListBlockSnapshotsResponse{}
+ mi := &file_master_proto_msgTypes[75]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *ListBlockSnapshotsResponse) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*ListBlockSnapshotsResponse) ProtoMessage() {}
+
+func (x *ListBlockSnapshotsResponse) ProtoReflect() protoreflect.Message {
+ mi := &file_master_proto_msgTypes[75]
+ if x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use ListBlockSnapshotsResponse.ProtoReflect.Descriptor instead.
+func (*ListBlockSnapshotsResponse) Descriptor() ([]byte, []int) {
+ return file_master_proto_rawDescGZIP(), []int{75}
+}
+
+func (x *ListBlockSnapshotsResponse) GetSnapshots() []*BlockSnapshotInfo {
+ if x != nil {
+ return x.Snapshots
+ }
+ return nil
+}
+
+type BlockSnapshotInfo struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ SnapshotId uint32 `protobuf:"varint,1,opt,name=snapshot_id,json=snapshotId,proto3" json:"snapshot_id,omitempty"`
+ CreatedAt int64 `protobuf:"varint,2,opt,name=created_at,json=createdAt,proto3" json:"created_at,omitempty"` // Unix timestamp in seconds
+ VolumeSizeBytes uint64 `protobuf:"varint,3,opt,name=volume_size_bytes,json=volumeSizeBytes,proto3" json:"volume_size_bytes,omitempty"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *BlockSnapshotInfo) Reset() {
+ *x = BlockSnapshotInfo{}
+ mi := &file_master_proto_msgTypes[76]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *BlockSnapshotInfo) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*BlockSnapshotInfo) ProtoMessage() {}
+
+func (x *BlockSnapshotInfo) ProtoReflect() protoreflect.Message {
+ mi := &file_master_proto_msgTypes[76]
+ if x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use BlockSnapshotInfo.ProtoReflect.Descriptor instead.
+func (*BlockSnapshotInfo) Descriptor() ([]byte, []int) {
+ return file_master_proto_rawDescGZIP(), []int{76}
+}
+
+func (x *BlockSnapshotInfo) GetSnapshotId() uint32 {
+ if x != nil {
+ return x.SnapshotId
+ }
+ return 0
+}
+
+func (x *BlockSnapshotInfo) GetCreatedAt() int64 {
+ if x != nil {
+ return x.CreatedAt
+ }
+ return 0
+}
+
+func (x *BlockSnapshotInfo) GetVolumeSizeBytes() uint64 {
+ if x != nil {
+ return x.VolumeSizeBytes
+ }
+ return 0
+}
+
+type ExpandBlockVolumeRequest struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
+ NewSizeBytes uint64 `protobuf:"varint,2,opt,name=new_size_bytes,json=newSizeBytes,proto3" json:"new_size_bytes,omitempty"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *ExpandBlockVolumeRequest) Reset() {
+ *x = ExpandBlockVolumeRequest{}
+ mi := &file_master_proto_msgTypes[77]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *ExpandBlockVolumeRequest) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*ExpandBlockVolumeRequest) ProtoMessage() {}
+
+func (x *ExpandBlockVolumeRequest) ProtoReflect() protoreflect.Message {
+ mi := &file_master_proto_msgTypes[77]
+ if x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use ExpandBlockVolumeRequest.ProtoReflect.Descriptor instead.
+func (*ExpandBlockVolumeRequest) Descriptor() ([]byte, []int) {
+ return file_master_proto_rawDescGZIP(), []int{77}
+}
+
+func (x *ExpandBlockVolumeRequest) GetName() string {
+ if x != nil {
+ return x.Name
+ }
+ return ""
+}
+
+func (x *ExpandBlockVolumeRequest) GetNewSizeBytes() uint64 {
+ if x != nil {
+ return x.NewSizeBytes
+ }
+ return 0
+}
+
+type ExpandBlockVolumeResponse struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ CapacityBytes uint64 `protobuf:"varint,1,opt,name=capacity_bytes,json=capacityBytes,proto3" json:"capacity_bytes,omitempty"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *ExpandBlockVolumeResponse) Reset() {
+ *x = ExpandBlockVolumeResponse{}
+ mi := &file_master_proto_msgTypes[78]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *ExpandBlockVolumeResponse) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*ExpandBlockVolumeResponse) ProtoMessage() {}
+
+func (x *ExpandBlockVolumeResponse) ProtoReflect() protoreflect.Message {
+ mi := &file_master_proto_msgTypes[78]
+ if x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use ExpandBlockVolumeResponse.ProtoReflect.Descriptor instead.
+func (*ExpandBlockVolumeResponse) Descriptor() ([]byte, []int) {
+ return file_master_proto_rawDescGZIP(), []int{78}
+}
+
+func (x *ExpandBlockVolumeResponse) GetCapacityBytes() uint64 {
+ if x != nil {
+ return x.CapacityBytes
+ }
+ return 0
+}
+
type SuperBlockExtra_ErasureCoding struct {
state protoimpl.MessageState `protogen:"open.v1"`
Data uint32 `protobuf:"varint,1,opt,name=data,proto3" json:"data,omitempty"`
@@ -4521,7 +5064,7 @@ type SuperBlockExtra_ErasureCoding struct {
func (x *SuperBlockExtra_ErasureCoding) Reset() {
*x = SuperBlockExtra_ErasureCoding{}
- mi := &file_master_proto_msgTypes[72]
+ mi := &file_master_proto_msgTypes[81]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -4533,7 +5076,7 @@ func (x *SuperBlockExtra_ErasureCoding) String() string {
func (*SuperBlockExtra_ErasureCoding) ProtoMessage() {}
func (x *SuperBlockExtra_ErasureCoding) ProtoReflect() protoreflect.Message {
- mi := &file_master_proto_msgTypes[72]
+ mi := &file_master_proto_msgTypes[81]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -4582,7 +5125,7 @@ type LookupVolumeResponse_VolumeIdLocation struct {
func (x *LookupVolumeResponse_VolumeIdLocation) Reset() {
*x = LookupVolumeResponse_VolumeIdLocation{}
- mi := &file_master_proto_msgTypes[73]
+ mi := &file_master_proto_msgTypes[82]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -4594,7 +5137,7 @@ func (x *LookupVolumeResponse_VolumeIdLocation) String() string {
func (*LookupVolumeResponse_VolumeIdLocation) ProtoMessage() {}
func (x *LookupVolumeResponse_VolumeIdLocation) ProtoReflect() protoreflect.Message {
- mi := &file_master_proto_msgTypes[73]
+ mi := &file_master_proto_msgTypes[82]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -4648,7 +5191,7 @@ type LookupEcVolumeResponse_EcShardIdLocation struct {
func (x *LookupEcVolumeResponse_EcShardIdLocation) Reset() {
*x = LookupEcVolumeResponse_EcShardIdLocation{}
- mi := &file_master_proto_msgTypes[78]
+ mi := &file_master_proto_msgTypes[87]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -4660,7 +5203,7 @@ func (x *LookupEcVolumeResponse_EcShardIdLocation) String() string {
func (*LookupEcVolumeResponse_EcShardIdLocation) ProtoMessage() {}
func (x *LookupEcVolumeResponse_EcShardIdLocation) ProtoReflect() protoreflect.Message {
- mi := &file_master_proto_msgTypes[78]
+ mi := &file_master_proto_msgTypes[87]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -4703,7 +5246,7 @@ type ListClusterNodesResponse_ClusterNode struct {
func (x *ListClusterNodesResponse_ClusterNode) Reset() {
*x = ListClusterNodesResponse_ClusterNode{}
- mi := &file_master_proto_msgTypes[79]
+ mi := &file_master_proto_msgTypes[88]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -4715,7 +5258,7 @@ func (x *ListClusterNodesResponse_ClusterNode) String() string {
func (*ListClusterNodesResponse_ClusterNode) ProtoMessage() {}
func (x *ListClusterNodesResponse_ClusterNode) ProtoReflect() protoreflect.Message {
- mi := &file_master_proto_msgTypes[79]
+ mi := &file_master_proto_msgTypes[88]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -4778,7 +5321,7 @@ type RaftListClusterServersResponse_ClusterServers struct {
func (x *RaftListClusterServersResponse_ClusterServers) Reset() {
*x = RaftListClusterServersResponse_ClusterServers{}
- mi := &file_master_proto_msgTypes[80]
+ mi := &file_master_proto_msgTypes[89]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -4790,7 +5333,7 @@ func (x *RaftListClusterServersResponse_ClusterServers) String() string {
func (*RaftListClusterServersResponse_ClusterServers) ProtoMessage() {}
func (x *RaftListClusterServersResponse_ClusterServers) ProtoReflect() protoreflect.Message {
- mi := &file_master_proto_msgTypes[80]
+ mi := &file_master_proto_msgTypes[89]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -5205,7 +5748,7 @@ const file_master_proto_rawDesc = "" +
"\x0fprevious_leader\x18\x01 \x01(\tR\x0epreviousLeader\x12\x1d\n" +
"\n" +
"new_leader\x18\x02 \x01(\tR\tnewLeader\"\x14\n" +
- "\x12VolumeGrowResponse\"\x99\x02\n" +
+ "\x12VolumeGrowResponse\"\xf1\x02\n" +
"\x16BlockVolumeInfoMessage\x12\x12\n" +
"\x04path\x18\x01 \x01(\tR\x04path\x12\x1f\n" +
"\vvolume_size\x18\x02 \x01(\x04R\n" +
@@ -5218,43 +5761,85 @@ const file_master_proto_rawDesc = "" +
"walHeadLsn\x12%\n" +
"\x0echeckpoint_lsn\x18\a \x01(\x04R\rcheckpointLsn\x12\x1b\n" +
"\thas_lease\x18\b \x01(\bR\bhasLease\x12\x1b\n" +
- "\tdisk_type\x18\t \x01(\tR\bdiskType\"\x8e\x01\n" +
+ "\tdisk_type\x18\t \x01(\tR\bdiskType\x12*\n" +
+ "\x11replica_data_addr\x18\n" +
+ " \x01(\tR\x0freplicaDataAddr\x12*\n" +
+ "\x11replica_ctrl_addr\x18\v \x01(\tR\x0freplicaCtrlAddr\"\x8e\x01\n" +
"\x1bBlockVolumeShortInfoMessage\x12\x12\n" +
"\x04path\x18\x01 \x01(\tR\x04path\x12\x1f\n" +
"\vvolume_size\x18\x02 \x01(\x04R\n" +
"volumeSize\x12\x1d\n" +
"\n" +
"block_size\x18\x03 \x01(\rR\tblockSize\x12\x1b\n" +
- "\tdisk_type\x18\x04 \x01(\tR\bdiskType\"w\n" +
+ "\tdisk_type\x18\x04 \x01(\tR\bdiskType\"\xf2\x01\n" +
"\x15BlockVolumeAssignment\x12\x12\n" +
"\x04path\x18\x01 \x01(\tR\x04path\x12\x14\n" +
"\x05epoch\x18\x02 \x01(\x04R\x05epoch\x12\x12\n" +
"\x04role\x18\x03 \x01(\rR\x04role\x12 \n" +
"\flease_ttl_ms\x18\x04 \x01(\rR\n" +
- "leaseTtlMs\"j\n" +
+ "leaseTtlMs\x12*\n" +
+ "\x11replica_data_addr\x18\x05 \x01(\tR\x0freplicaDataAddr\x12*\n" +
+ "\x11replica_ctrl_addr\x18\x06 \x01(\tR\x0freplicaCtrlAddr\x12!\n" +
+ "\frebuild_addr\x18\a \x01(\tR\vrebuildAddr\"j\n" +
"\x18CreateBlockVolumeRequest\x12\x12\n" +
"\x04name\x18\x01 \x01(\tR\x04name\x12\x1d\n" +
"\n" +
"size_bytes\x18\x02 \x01(\x04R\tsizeBytes\x12\x1b\n" +
- "\tdisk_type\x18\x03 \x01(\tR\bdiskType\"\xb5\x01\n" +
+ "\tdisk_type\x18\x03 \x01(\tR\bdiskType\"\xdc\x01\n" +
"\x19CreateBlockVolumeResponse\x12\x1b\n" +
"\tvolume_id\x18\x01 \x01(\tR\bvolumeId\x12#\n" +
"\rvolume_server\x18\x02 \x01(\tR\fvolumeServer\x12\x1d\n" +
"\n" +
"iscsi_addr\x18\x03 \x01(\tR\tiscsiAddr\x12\x10\n" +
"\x03iqn\x18\x04 \x01(\tR\x03iqn\x12%\n" +
- "\x0ecapacity_bytes\x18\x05 \x01(\x04R\rcapacityBytes\".\n" +
+ "\x0ecapacity_bytes\x18\x05 \x01(\x04R\rcapacityBytes\x12%\n" +
+ "\x0ereplica_server\x18\x06 \x01(\tR\rreplicaServer\".\n" +
"\x18DeleteBlockVolumeRequest\x12\x12\n" +
"\x04name\x18\x01 \x01(\tR\x04name\"\x1b\n" +
"\x19DeleteBlockVolumeResponse\".\n" +
"\x18LookupBlockVolumeRequest\x12\x12\n" +
- "\x04name\x18\x01 \x01(\tR\x04name\"\x98\x01\n" +
+ "\x04name\x18\x01 \x01(\tR\x04name\"\xbf\x01\n" +
"\x19LookupBlockVolumeResponse\x12#\n" +
"\rvolume_server\x18\x01 \x01(\tR\fvolumeServer\x12\x1d\n" +
"\n" +
"iscsi_addr\x18\x02 \x01(\tR\tiscsiAddr\x12\x10\n" +
"\x03iqn\x18\x03 \x01(\tR\x03iqn\x12%\n" +
- "\x0ecapacity_bytes\x18\x04 \x01(\x04R\rcapacityBytes2\xec\x12\n" +
+ "\x0ecapacity_bytes\x18\x04 \x01(\x04R\rcapacityBytes\x12%\n" +
+ "\x0ereplica_server\x18\x05 \x01(\tR\rreplicaServer\"^\n" +
+ "\x1aCreateBlockSnapshotRequest\x12\x1f\n" +
+ "\vvolume_name\x18\x01 \x01(\tR\n" +
+ "volumeName\x12\x1f\n" +
+ "\vsnapshot_id\x18\x02 \x01(\rR\n" +
+ "snapshotId\"|\n" +
+ "\x1bCreateBlockSnapshotResponse\x12\x1f\n" +
+ "\vsnapshot_id\x18\x01 \x01(\rR\n" +
+ "snapshotId\x12\x1d\n" +
+ "\n" +
+ "created_at\x18\x02 \x01(\x03R\tcreatedAt\x12\x1d\n" +
+ "\n" +
+ "size_bytes\x18\x03 \x01(\x04R\tsizeBytes\"^\n" +
+ "\x1aDeleteBlockSnapshotRequest\x12\x1f\n" +
+ "\vvolume_name\x18\x01 \x01(\tR\n" +
+ "volumeName\x12\x1f\n" +
+ "\vsnapshot_id\x18\x02 \x01(\rR\n" +
+ "snapshotId\"\x1d\n" +
+ "\x1bDeleteBlockSnapshotResponse\"<\n" +
+ "\x19ListBlockSnapshotsRequest\x12\x1f\n" +
+ "\vvolume_name\x18\x01 \x01(\tR\n" +
+ "volumeName\"X\n" +
+ "\x1aListBlockSnapshotsResponse\x12:\n" +
+ "\tsnapshots\x18\x01 \x03(\v2\x1c.master_pb.BlockSnapshotInfoR\tsnapshots\"\x7f\n" +
+ "\x11BlockSnapshotInfo\x12\x1f\n" +
+ "\vsnapshot_id\x18\x01 \x01(\rR\n" +
+ "snapshotId\x12\x1d\n" +
+ "\n" +
+ "created_at\x18\x02 \x01(\x03R\tcreatedAt\x12*\n" +
+ "\x11volume_size_bytes\x18\x03 \x01(\x04R\x0fvolumeSizeBytes\"T\n" +
+ "\x18ExpandBlockVolumeRequest\x12\x12\n" +
+ "\x04name\x18\x01 \x01(\tR\x04name\x12$\n" +
+ "\x0enew_size_bytes\x18\x02 \x01(\x04R\fnewSizeBytes\"B\n" +
+ "\x19ExpandBlockVolumeResponse\x12%\n" +
+ "\x0ecapacity_bytes\x18\x01 \x01(\x04R\rcapacityBytes2\x83\x16\n" +
"\aSeaweed\x12I\n" +
"\rSendHeartbeat\x12\x14.master_pb.Heartbeat\x1a\x1c.master_pb.HeartbeatResponse\"\x00(\x010\x01\x12X\n" +
"\rKeepConnected\x12\x1f.master_pb.KeepConnectedRequest\x1a .master_pb.KeepConnectedResponse\"\x00(\x010\x01\x12Q\n" +
@@ -5285,7 +5870,11 @@ const file_master_proto_rawDesc = "" +
"VolumeGrow\x12\x1c.master_pb.VolumeGrowRequest\x1a\x1d.master_pb.VolumeGrowResponse\"\x00\x12`\n" +
"\x11CreateBlockVolume\x12#.master_pb.CreateBlockVolumeRequest\x1a$.master_pb.CreateBlockVolumeResponse\"\x00\x12`\n" +
"\x11DeleteBlockVolume\x12#.master_pb.DeleteBlockVolumeRequest\x1a$.master_pb.DeleteBlockVolumeResponse\"\x00\x12`\n" +
- "\x11LookupBlockVolume\x12#.master_pb.LookupBlockVolumeRequest\x1a$.master_pb.LookupBlockVolumeResponse\"\x00B2Z0github.com/seaweedfs/seaweedfs/weed/pb/master_pbb\x06proto3"
+ "\x11LookupBlockVolume\x12#.master_pb.LookupBlockVolumeRequest\x1a$.master_pb.LookupBlockVolumeResponse\"\x00\x12f\n" +
+ "\x13CreateBlockSnapshot\x12%.master_pb.CreateBlockSnapshotRequest\x1a&.master_pb.CreateBlockSnapshotResponse\"\x00\x12f\n" +
+ "\x13DeleteBlockSnapshot\x12%.master_pb.DeleteBlockSnapshotRequest\x1a&.master_pb.DeleteBlockSnapshotResponse\"\x00\x12c\n" +
+ "\x12ListBlockSnapshots\x12$.master_pb.ListBlockSnapshotsRequest\x1a%.master_pb.ListBlockSnapshotsResponse\"\x00\x12`\n" +
+ "\x11ExpandBlockVolume\x12#.master_pb.ExpandBlockVolumeRequest\x1a$.master_pb.ExpandBlockVolumeResponse\"\x00B2Z0github.com/seaweedfs/seaweedfs/weed/pb/master_pbb\x06proto3"
var (
file_master_proto_rawDescOnce sync.Once
@@ -5299,7 +5888,7 @@ func file_master_proto_rawDescGZIP() []byte {
return file_master_proto_rawDescData
}
-var file_master_proto_msgTypes = make([]protoimpl.MessageInfo, 81)
+var file_master_proto_msgTypes = make([]protoimpl.MessageInfo, 90)
var file_master_proto_goTypes = []any{
(*Heartbeat)(nil), // 0: master_pb.Heartbeat
(*HeartbeatResponse)(nil), // 1: master_pb.HeartbeatResponse
@@ -5371,18 +5960,27 @@ var file_master_proto_goTypes = []any{
(*DeleteBlockVolumeResponse)(nil), // 67: master_pb.DeleteBlockVolumeResponse
(*LookupBlockVolumeRequest)(nil), // 68: master_pb.LookupBlockVolumeRequest
(*LookupBlockVolumeResponse)(nil), // 69: master_pb.LookupBlockVolumeResponse
- nil, // 70: master_pb.Heartbeat.MaxVolumeCountsEntry
- nil, // 71: master_pb.StorageBackend.PropertiesEntry
- (*SuperBlockExtra_ErasureCoding)(nil), // 72: master_pb.SuperBlockExtra.ErasureCoding
- (*LookupVolumeResponse_VolumeIdLocation)(nil), // 73: master_pb.LookupVolumeResponse.VolumeIdLocation
- nil, // 74: master_pb.DataNodeInfo.DiskInfosEntry
- nil, // 75: master_pb.RackInfo.DiskInfosEntry
- nil, // 76: master_pb.DataCenterInfo.DiskInfosEntry
- nil, // 77: master_pb.TopologyInfo.DiskInfosEntry
- (*LookupEcVolumeResponse_EcShardIdLocation)(nil), // 78: master_pb.LookupEcVolumeResponse.EcShardIdLocation
- (*ListClusterNodesResponse_ClusterNode)(nil), // 79: master_pb.ListClusterNodesResponse.ClusterNode
- (*RaftListClusterServersResponse_ClusterServers)(nil), // 80: master_pb.RaftListClusterServersResponse.ClusterServers
- (*volume_server_pb.VolumeServerState)(nil), // 81: volume_server_pb.VolumeServerState
+ (*CreateBlockSnapshotRequest)(nil), // 70: master_pb.CreateBlockSnapshotRequest
+ (*CreateBlockSnapshotResponse)(nil), // 71: master_pb.CreateBlockSnapshotResponse
+ (*DeleteBlockSnapshotRequest)(nil), // 72: master_pb.DeleteBlockSnapshotRequest
+ (*DeleteBlockSnapshotResponse)(nil), // 73: master_pb.DeleteBlockSnapshotResponse
+ (*ListBlockSnapshotsRequest)(nil), // 74: master_pb.ListBlockSnapshotsRequest
+ (*ListBlockSnapshotsResponse)(nil), // 75: master_pb.ListBlockSnapshotsResponse
+ (*BlockSnapshotInfo)(nil), // 76: master_pb.BlockSnapshotInfo
+ (*ExpandBlockVolumeRequest)(nil), // 77: master_pb.ExpandBlockVolumeRequest
+ (*ExpandBlockVolumeResponse)(nil), // 78: master_pb.ExpandBlockVolumeResponse
+ nil, // 79: master_pb.Heartbeat.MaxVolumeCountsEntry
+ nil, // 80: master_pb.StorageBackend.PropertiesEntry
+ (*SuperBlockExtra_ErasureCoding)(nil), // 81: master_pb.SuperBlockExtra.ErasureCoding
+ (*LookupVolumeResponse_VolumeIdLocation)(nil), // 82: master_pb.LookupVolumeResponse.VolumeIdLocation
+ nil, // 83: master_pb.DataNodeInfo.DiskInfosEntry
+ nil, // 84: master_pb.RackInfo.DiskInfosEntry
+ nil, // 85: master_pb.DataCenterInfo.DiskInfosEntry
+ nil, // 86: master_pb.TopologyInfo.DiskInfosEntry
+ (*LookupEcVolumeResponse_EcShardIdLocation)(nil), // 87: master_pb.LookupEcVolumeResponse.EcShardIdLocation
+ (*ListClusterNodesResponse_ClusterNode)(nil), // 88: master_pb.ListClusterNodesResponse.ClusterNode
+ (*RaftListClusterServersResponse_ClusterServers)(nil), // 89: master_pb.RaftListClusterServersResponse.ClusterServers
+ (*volume_server_pb.VolumeServerState)(nil), // 90: volume_server_pb.VolumeServerState
}
var file_master_proto_depIdxs = []int32{
2, // 0: master_pb.Heartbeat.volumes:type_name -> master_pb.VolumeInformationMessage
@@ -5391,100 +5989,109 @@ var file_master_proto_depIdxs = []int32{
4, // 3: master_pb.Heartbeat.ec_shards:type_name -> master_pb.VolumeEcShardInformationMessage
4, // 4: master_pb.Heartbeat.new_ec_shards:type_name -> master_pb.VolumeEcShardInformationMessage
4, // 5: master_pb.Heartbeat.deleted_ec_shards:type_name -> master_pb.VolumeEcShardInformationMessage
- 70, // 6: master_pb.Heartbeat.max_volume_counts:type_name -> master_pb.Heartbeat.MaxVolumeCountsEntry
- 81, // 7: master_pb.Heartbeat.state:type_name -> volume_server_pb.VolumeServerState
+ 79, // 6: master_pb.Heartbeat.max_volume_counts:type_name -> master_pb.Heartbeat.MaxVolumeCountsEntry
+ 90, // 7: master_pb.Heartbeat.state:type_name -> volume_server_pb.VolumeServerState
61, // 8: master_pb.Heartbeat.block_volume_infos:type_name -> master_pb.BlockVolumeInfoMessage
62, // 9: master_pb.Heartbeat.new_block_volumes:type_name -> master_pb.BlockVolumeShortInfoMessage
62, // 10: master_pb.Heartbeat.deleted_block_volumes:type_name -> master_pb.BlockVolumeShortInfoMessage
5, // 11: master_pb.HeartbeatResponse.storage_backends:type_name -> master_pb.StorageBackend
63, // 12: master_pb.HeartbeatResponse.block_volume_assignments:type_name -> master_pb.BlockVolumeAssignment
- 71, // 13: master_pb.StorageBackend.properties:type_name -> master_pb.StorageBackend.PropertiesEntry
- 72, // 14: master_pb.SuperBlockExtra.erasure_coding:type_name -> master_pb.SuperBlockExtra.ErasureCoding
+ 80, // 13: master_pb.StorageBackend.properties:type_name -> master_pb.StorageBackend.PropertiesEntry
+ 81, // 14: master_pb.SuperBlockExtra.erasure_coding:type_name -> master_pb.SuperBlockExtra.ErasureCoding
9, // 15: master_pb.KeepConnectedResponse.volume_location:type_name -> master_pb.VolumeLocation
10, // 16: master_pb.KeepConnectedResponse.cluster_node_update:type_name -> master_pb.ClusterNodeUpdate
- 73, // 17: master_pb.LookupVolumeResponse.volume_id_locations:type_name -> master_pb.LookupVolumeResponse.VolumeIdLocation
+ 82, // 17: master_pb.LookupVolumeResponse.volume_id_locations:type_name -> master_pb.LookupVolumeResponse.VolumeIdLocation
14, // 18: master_pb.AssignResponse.replicas:type_name -> master_pb.Location
14, // 19: master_pb.AssignResponse.location:type_name -> master_pb.Location
20, // 20: master_pb.CollectionListResponse.collections:type_name -> master_pb.Collection
2, // 21: master_pb.DiskInfo.volume_infos:type_name -> master_pb.VolumeInformationMessage
4, // 22: master_pb.DiskInfo.ec_shard_infos:type_name -> master_pb.VolumeEcShardInformationMessage
- 74, // 23: master_pb.DataNodeInfo.diskInfos:type_name -> master_pb.DataNodeInfo.DiskInfosEntry
+ 83, // 23: master_pb.DataNodeInfo.diskInfos:type_name -> master_pb.DataNodeInfo.DiskInfosEntry
26, // 24: master_pb.RackInfo.data_node_infos:type_name -> master_pb.DataNodeInfo
- 75, // 25: master_pb.RackInfo.diskInfos:type_name -> master_pb.RackInfo.DiskInfosEntry
+ 84, // 25: master_pb.RackInfo.diskInfos:type_name -> master_pb.RackInfo.DiskInfosEntry
27, // 26: master_pb.DataCenterInfo.rack_infos:type_name -> master_pb.RackInfo
- 76, // 27: master_pb.DataCenterInfo.diskInfos:type_name -> master_pb.DataCenterInfo.DiskInfosEntry
+ 85, // 27: master_pb.DataCenterInfo.diskInfos:type_name -> master_pb.DataCenterInfo.DiskInfosEntry
28, // 28: master_pb.TopologyInfo.data_center_infos:type_name -> master_pb.DataCenterInfo
- 77, // 29: master_pb.TopologyInfo.diskInfos:type_name -> master_pb.TopologyInfo.DiskInfosEntry
+ 86, // 29: master_pb.TopologyInfo.diskInfos:type_name -> master_pb.TopologyInfo.DiskInfosEntry
29, // 30: master_pb.VolumeListResponse.topology_info:type_name -> master_pb.TopologyInfo
- 78, // 31: master_pb.LookupEcVolumeResponse.shard_id_locations:type_name -> master_pb.LookupEcVolumeResponse.EcShardIdLocation
+ 87, // 31: master_pb.LookupEcVolumeResponse.shard_id_locations:type_name -> master_pb.LookupEcVolumeResponse.EcShardIdLocation
5, // 32: master_pb.GetMasterConfigurationResponse.storage_backends:type_name -> master_pb.StorageBackend
- 79, // 33: master_pb.ListClusterNodesResponse.cluster_nodes:type_name -> master_pb.ListClusterNodesResponse.ClusterNode
- 80, // 34: master_pb.RaftListClusterServersResponse.cluster_servers:type_name -> master_pb.RaftListClusterServersResponse.ClusterServers
- 14, // 35: master_pb.LookupVolumeResponse.VolumeIdLocation.locations:type_name -> master_pb.Location
- 25, // 36: master_pb.DataNodeInfo.DiskInfosEntry.value:type_name -> master_pb.DiskInfo
- 25, // 37: master_pb.RackInfo.DiskInfosEntry.value:type_name -> master_pb.DiskInfo
- 25, // 38: master_pb.DataCenterInfo.DiskInfosEntry.value:type_name -> master_pb.DiskInfo
- 25, // 39: master_pb.TopologyInfo.DiskInfosEntry.value:type_name -> master_pb.DiskInfo
- 14, // 40: master_pb.LookupEcVolumeResponse.EcShardIdLocation.locations:type_name -> master_pb.Location
- 0, // 41: master_pb.Seaweed.SendHeartbeat:input_type -> master_pb.Heartbeat
- 8, // 42: master_pb.Seaweed.KeepConnected:input_type -> master_pb.KeepConnectedRequest
- 12, // 43: master_pb.Seaweed.LookupVolume:input_type -> master_pb.LookupVolumeRequest
- 15, // 44: master_pb.Seaweed.Assign:input_type -> master_pb.AssignRequest
- 15, // 45: master_pb.Seaweed.StreamAssign:input_type -> master_pb.AssignRequest
- 18, // 46: master_pb.Seaweed.Statistics:input_type -> master_pb.StatisticsRequest
- 21, // 47: master_pb.Seaweed.CollectionList:input_type -> master_pb.CollectionListRequest
- 23, // 48: master_pb.Seaweed.CollectionDelete:input_type -> master_pb.CollectionDeleteRequest
- 30, // 49: master_pb.Seaweed.VolumeList:input_type -> master_pb.VolumeListRequest
- 32, // 50: master_pb.Seaweed.LookupEcVolume:input_type -> master_pb.LookupEcVolumeRequest
- 34, // 51: master_pb.Seaweed.VacuumVolume:input_type -> master_pb.VacuumVolumeRequest
- 36, // 52: master_pb.Seaweed.DisableVacuum:input_type -> master_pb.DisableVacuumRequest
- 38, // 53: master_pb.Seaweed.EnableVacuum:input_type -> master_pb.EnableVacuumRequest
- 40, // 54: master_pb.Seaweed.VolumeMarkReadonly:input_type -> master_pb.VolumeMarkReadonlyRequest
- 42, // 55: master_pb.Seaweed.GetMasterConfiguration:input_type -> master_pb.GetMasterConfigurationRequest
- 44, // 56: master_pb.Seaweed.ListClusterNodes:input_type -> master_pb.ListClusterNodesRequest
- 46, // 57: master_pb.Seaweed.LeaseAdminToken:input_type -> master_pb.LeaseAdminTokenRequest
- 48, // 58: master_pb.Seaweed.ReleaseAdminToken:input_type -> master_pb.ReleaseAdminTokenRequest
- 50, // 59: master_pb.Seaweed.Ping:input_type -> master_pb.PingRequest
- 56, // 60: master_pb.Seaweed.RaftListClusterServers:input_type -> master_pb.RaftListClusterServersRequest
- 52, // 61: master_pb.Seaweed.RaftAddServer:input_type -> master_pb.RaftAddServerRequest
- 54, // 62: master_pb.Seaweed.RaftRemoveServer:input_type -> master_pb.RaftRemoveServerRequest
- 58, // 63: master_pb.Seaweed.RaftLeadershipTransfer:input_type -> master_pb.RaftLeadershipTransferRequest
- 16, // 64: master_pb.Seaweed.VolumeGrow:input_type -> master_pb.VolumeGrowRequest
- 64, // 65: master_pb.Seaweed.CreateBlockVolume:input_type -> master_pb.CreateBlockVolumeRequest
- 66, // 66: master_pb.Seaweed.DeleteBlockVolume:input_type -> master_pb.DeleteBlockVolumeRequest
- 68, // 67: master_pb.Seaweed.LookupBlockVolume:input_type -> master_pb.LookupBlockVolumeRequest
- 1, // 68: master_pb.Seaweed.SendHeartbeat:output_type -> master_pb.HeartbeatResponse
- 11, // 69: master_pb.Seaweed.KeepConnected:output_type -> master_pb.KeepConnectedResponse
- 13, // 70: master_pb.Seaweed.LookupVolume:output_type -> master_pb.LookupVolumeResponse
- 17, // 71: master_pb.Seaweed.Assign:output_type -> master_pb.AssignResponse
- 17, // 72: master_pb.Seaweed.StreamAssign:output_type -> master_pb.AssignResponse
- 19, // 73: master_pb.Seaweed.Statistics:output_type -> master_pb.StatisticsResponse
- 22, // 74: master_pb.Seaweed.CollectionList:output_type -> master_pb.CollectionListResponse
- 24, // 75: master_pb.Seaweed.CollectionDelete:output_type -> master_pb.CollectionDeleteResponse
- 31, // 76: master_pb.Seaweed.VolumeList:output_type -> master_pb.VolumeListResponse
- 33, // 77: master_pb.Seaweed.LookupEcVolume:output_type -> master_pb.LookupEcVolumeResponse
- 35, // 78: master_pb.Seaweed.VacuumVolume:output_type -> master_pb.VacuumVolumeResponse
- 37, // 79: master_pb.Seaweed.DisableVacuum:output_type -> master_pb.DisableVacuumResponse
- 39, // 80: master_pb.Seaweed.EnableVacuum:output_type -> master_pb.EnableVacuumResponse
- 41, // 81: master_pb.Seaweed.VolumeMarkReadonly:output_type -> master_pb.VolumeMarkReadonlyResponse
- 43, // 82: master_pb.Seaweed.GetMasterConfiguration:output_type -> master_pb.GetMasterConfigurationResponse
- 45, // 83: master_pb.Seaweed.ListClusterNodes:output_type -> master_pb.ListClusterNodesResponse
- 47, // 84: master_pb.Seaweed.LeaseAdminToken:output_type -> master_pb.LeaseAdminTokenResponse
- 49, // 85: master_pb.Seaweed.ReleaseAdminToken:output_type -> master_pb.ReleaseAdminTokenResponse
- 51, // 86: master_pb.Seaweed.Ping:output_type -> master_pb.PingResponse
- 57, // 87: master_pb.Seaweed.RaftListClusterServers:output_type -> master_pb.RaftListClusterServersResponse
- 53, // 88: master_pb.Seaweed.RaftAddServer:output_type -> master_pb.RaftAddServerResponse
- 55, // 89: master_pb.Seaweed.RaftRemoveServer:output_type -> master_pb.RaftRemoveServerResponse
- 59, // 90: master_pb.Seaweed.RaftLeadershipTransfer:output_type -> master_pb.RaftLeadershipTransferResponse
- 60, // 91: master_pb.Seaweed.VolumeGrow:output_type -> master_pb.VolumeGrowResponse
- 65, // 92: master_pb.Seaweed.CreateBlockVolume:output_type -> master_pb.CreateBlockVolumeResponse
- 67, // 93: master_pb.Seaweed.DeleteBlockVolume:output_type -> master_pb.DeleteBlockVolumeResponse
- 69, // 94: master_pb.Seaweed.LookupBlockVolume:output_type -> master_pb.LookupBlockVolumeResponse
- 68, // [68:95] is the sub-list for method output_type
- 41, // [41:68] is the sub-list for method input_type
- 41, // [41:41] is the sub-list for extension type_name
- 41, // [41:41] is the sub-list for extension extendee
- 0, // [0:41] is the sub-list for field type_name
+ 88, // 33: master_pb.ListClusterNodesResponse.cluster_nodes:type_name -> master_pb.ListClusterNodesResponse.ClusterNode
+ 89, // 34: master_pb.RaftListClusterServersResponse.cluster_servers:type_name -> master_pb.RaftListClusterServersResponse.ClusterServers
+ 76, // 35: master_pb.ListBlockSnapshotsResponse.snapshots:type_name -> master_pb.BlockSnapshotInfo
+ 14, // 36: master_pb.LookupVolumeResponse.VolumeIdLocation.locations:type_name -> master_pb.Location
+ 25, // 37: master_pb.DataNodeInfo.DiskInfosEntry.value:type_name -> master_pb.DiskInfo
+ 25, // 38: master_pb.RackInfo.DiskInfosEntry.value:type_name -> master_pb.DiskInfo
+ 25, // 39: master_pb.DataCenterInfo.DiskInfosEntry.value:type_name -> master_pb.DiskInfo
+ 25, // 40: master_pb.TopologyInfo.DiskInfosEntry.value:type_name -> master_pb.DiskInfo
+ 14, // 41: master_pb.LookupEcVolumeResponse.EcShardIdLocation.locations:type_name -> master_pb.Location
+ 0, // 42: master_pb.Seaweed.SendHeartbeat:input_type -> master_pb.Heartbeat
+ 8, // 43: master_pb.Seaweed.KeepConnected:input_type -> master_pb.KeepConnectedRequest
+ 12, // 44: master_pb.Seaweed.LookupVolume:input_type -> master_pb.LookupVolumeRequest
+ 15, // 45: master_pb.Seaweed.Assign:input_type -> master_pb.AssignRequest
+ 15, // 46: master_pb.Seaweed.StreamAssign:input_type -> master_pb.AssignRequest
+ 18, // 47: master_pb.Seaweed.Statistics:input_type -> master_pb.StatisticsRequest
+ 21, // 48: master_pb.Seaweed.CollectionList:input_type -> master_pb.CollectionListRequest
+ 23, // 49: master_pb.Seaweed.CollectionDelete:input_type -> master_pb.CollectionDeleteRequest
+ 30, // 50: master_pb.Seaweed.VolumeList:input_type -> master_pb.VolumeListRequest
+ 32, // 51: master_pb.Seaweed.LookupEcVolume:input_type -> master_pb.LookupEcVolumeRequest
+ 34, // 52: master_pb.Seaweed.VacuumVolume:input_type -> master_pb.VacuumVolumeRequest
+ 36, // 53: master_pb.Seaweed.DisableVacuum:input_type -> master_pb.DisableVacuumRequest
+ 38, // 54: master_pb.Seaweed.EnableVacuum:input_type -> master_pb.EnableVacuumRequest
+ 40, // 55: master_pb.Seaweed.VolumeMarkReadonly:input_type -> master_pb.VolumeMarkReadonlyRequest
+ 42, // 56: master_pb.Seaweed.GetMasterConfiguration:input_type -> master_pb.GetMasterConfigurationRequest
+ 44, // 57: master_pb.Seaweed.ListClusterNodes:input_type -> master_pb.ListClusterNodesRequest
+ 46, // 58: master_pb.Seaweed.LeaseAdminToken:input_type -> master_pb.LeaseAdminTokenRequest
+ 48, // 59: master_pb.Seaweed.ReleaseAdminToken:input_type -> master_pb.ReleaseAdminTokenRequest
+ 50, // 60: master_pb.Seaweed.Ping:input_type -> master_pb.PingRequest
+ 56, // 61: master_pb.Seaweed.RaftListClusterServers:input_type -> master_pb.RaftListClusterServersRequest
+ 52, // 62: master_pb.Seaweed.RaftAddServer:input_type -> master_pb.RaftAddServerRequest
+ 54, // 63: master_pb.Seaweed.RaftRemoveServer:input_type -> master_pb.RaftRemoveServerRequest
+ 58, // 64: master_pb.Seaweed.RaftLeadershipTransfer:input_type -> master_pb.RaftLeadershipTransferRequest
+ 16, // 65: master_pb.Seaweed.VolumeGrow:input_type -> master_pb.VolumeGrowRequest
+ 64, // 66: master_pb.Seaweed.CreateBlockVolume:input_type -> master_pb.CreateBlockVolumeRequest
+ 66, // 67: master_pb.Seaweed.DeleteBlockVolume:input_type -> master_pb.DeleteBlockVolumeRequest
+ 68, // 68: master_pb.Seaweed.LookupBlockVolume:input_type -> master_pb.LookupBlockVolumeRequest
+ 70, // 69: master_pb.Seaweed.CreateBlockSnapshot:input_type -> master_pb.CreateBlockSnapshotRequest
+ 72, // 70: master_pb.Seaweed.DeleteBlockSnapshot:input_type -> master_pb.DeleteBlockSnapshotRequest
+ 74, // 71: master_pb.Seaweed.ListBlockSnapshots:input_type -> master_pb.ListBlockSnapshotsRequest
+ 77, // 72: master_pb.Seaweed.ExpandBlockVolume:input_type -> master_pb.ExpandBlockVolumeRequest
+ 1, // 73: master_pb.Seaweed.SendHeartbeat:output_type -> master_pb.HeartbeatResponse
+ 11, // 74: master_pb.Seaweed.KeepConnected:output_type -> master_pb.KeepConnectedResponse
+ 13, // 75: master_pb.Seaweed.LookupVolume:output_type -> master_pb.LookupVolumeResponse
+ 17, // 76: master_pb.Seaweed.Assign:output_type -> master_pb.AssignResponse
+ 17, // 77: master_pb.Seaweed.StreamAssign:output_type -> master_pb.AssignResponse
+ 19, // 78: master_pb.Seaweed.Statistics:output_type -> master_pb.StatisticsResponse
+ 22, // 79: master_pb.Seaweed.CollectionList:output_type -> master_pb.CollectionListResponse
+ 24, // 80: master_pb.Seaweed.CollectionDelete:output_type -> master_pb.CollectionDeleteResponse
+ 31, // 81: master_pb.Seaweed.VolumeList:output_type -> master_pb.VolumeListResponse
+ 33, // 82: master_pb.Seaweed.LookupEcVolume:output_type -> master_pb.LookupEcVolumeResponse
+ 35, // 83: master_pb.Seaweed.VacuumVolume:output_type -> master_pb.VacuumVolumeResponse
+ 37, // 84: master_pb.Seaweed.DisableVacuum:output_type -> master_pb.DisableVacuumResponse
+ 39, // 85: master_pb.Seaweed.EnableVacuum:output_type -> master_pb.EnableVacuumResponse
+ 41, // 86: master_pb.Seaweed.VolumeMarkReadonly:output_type -> master_pb.VolumeMarkReadonlyResponse
+ 43, // 87: master_pb.Seaweed.GetMasterConfiguration:output_type -> master_pb.GetMasterConfigurationResponse
+ 45, // 88: master_pb.Seaweed.ListClusterNodes:output_type -> master_pb.ListClusterNodesResponse
+ 47, // 89: master_pb.Seaweed.LeaseAdminToken:output_type -> master_pb.LeaseAdminTokenResponse
+ 49, // 90: master_pb.Seaweed.ReleaseAdminToken:output_type -> master_pb.ReleaseAdminTokenResponse
+ 51, // 91: master_pb.Seaweed.Ping:output_type -> master_pb.PingResponse
+ 57, // 92: master_pb.Seaweed.RaftListClusterServers:output_type -> master_pb.RaftListClusterServersResponse
+ 53, // 93: master_pb.Seaweed.RaftAddServer:output_type -> master_pb.RaftAddServerResponse
+ 55, // 94: master_pb.Seaweed.RaftRemoveServer:output_type -> master_pb.RaftRemoveServerResponse
+ 59, // 95: master_pb.Seaweed.RaftLeadershipTransfer:output_type -> master_pb.RaftLeadershipTransferResponse
+ 60, // 96: master_pb.Seaweed.VolumeGrow:output_type -> master_pb.VolumeGrowResponse
+ 65, // 97: master_pb.Seaweed.CreateBlockVolume:output_type -> master_pb.CreateBlockVolumeResponse
+ 67, // 98: master_pb.Seaweed.DeleteBlockVolume:output_type -> master_pb.DeleteBlockVolumeResponse
+ 69, // 99: master_pb.Seaweed.LookupBlockVolume:output_type -> master_pb.LookupBlockVolumeResponse
+ 71, // 100: master_pb.Seaweed.CreateBlockSnapshot:output_type -> master_pb.CreateBlockSnapshotResponse
+ 73, // 101: master_pb.Seaweed.DeleteBlockSnapshot:output_type -> master_pb.DeleteBlockSnapshotResponse
+ 75, // 102: master_pb.Seaweed.ListBlockSnapshots:output_type -> master_pb.ListBlockSnapshotsResponse
+ 78, // 103: master_pb.Seaweed.ExpandBlockVolume:output_type -> master_pb.ExpandBlockVolumeResponse
+ 73, // [73:104] is the sub-list for method output_type
+ 42, // [42:73] is the sub-list for method input_type
+ 42, // [42:42] is the sub-list for extension type_name
+ 42, // [42:42] is the sub-list for extension extendee
+ 0, // [0:42] is the sub-list for field type_name
}
func init() { file_master_proto_init() }
@@ -5498,7 +6105,7 @@ func file_master_proto_init() {
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_master_proto_rawDesc), len(file_master_proto_rawDesc)),
NumEnums: 0,
- NumMessages: 81,
+ NumMessages: 90,
NumExtensions: 0,
NumServices: 1,
},
diff --git a/weed/pb/master_pb/master_grpc.pb.go b/weed/pb/master_pb/master_grpc.pb.go
index ad2359292e..cd8a3ad458 100644
--- a/weed/pb/master_pb/master_grpc.pb.go
+++ b/weed/pb/master_pb/master_grpc.pb.go
@@ -46,6 +46,10 @@ const (
Seaweed_CreateBlockVolume_FullMethodName = "/master_pb.Seaweed/CreateBlockVolume"
Seaweed_DeleteBlockVolume_FullMethodName = "/master_pb.Seaweed/DeleteBlockVolume"
Seaweed_LookupBlockVolume_FullMethodName = "/master_pb.Seaweed/LookupBlockVolume"
+ Seaweed_CreateBlockSnapshot_FullMethodName = "/master_pb.Seaweed/CreateBlockSnapshot"
+ Seaweed_DeleteBlockSnapshot_FullMethodName = "/master_pb.Seaweed/DeleteBlockSnapshot"
+ Seaweed_ListBlockSnapshots_FullMethodName = "/master_pb.Seaweed/ListBlockSnapshots"
+ Seaweed_ExpandBlockVolume_FullMethodName = "/master_pb.Seaweed/ExpandBlockVolume"
)
// SeaweedClient is the client API for Seaweed service.
@@ -79,6 +83,10 @@ type SeaweedClient interface {
CreateBlockVolume(ctx context.Context, in *CreateBlockVolumeRequest, opts ...grpc.CallOption) (*CreateBlockVolumeResponse, error)
DeleteBlockVolume(ctx context.Context, in *DeleteBlockVolumeRequest, opts ...grpc.CallOption) (*DeleteBlockVolumeResponse, error)
LookupBlockVolume(ctx context.Context, in *LookupBlockVolumeRequest, opts ...grpc.CallOption) (*LookupBlockVolumeResponse, error)
+ CreateBlockSnapshot(ctx context.Context, in *CreateBlockSnapshotRequest, opts ...grpc.CallOption) (*CreateBlockSnapshotResponse, error)
+ DeleteBlockSnapshot(ctx context.Context, in *DeleteBlockSnapshotRequest, opts ...grpc.CallOption) (*DeleteBlockSnapshotResponse, error)
+ ListBlockSnapshots(ctx context.Context, in *ListBlockSnapshotsRequest, opts ...grpc.CallOption) (*ListBlockSnapshotsResponse, error)
+ ExpandBlockVolume(ctx context.Context, in *ExpandBlockVolumeRequest, opts ...grpc.CallOption) (*ExpandBlockVolumeResponse, error)
}
type seaweedClient struct {
@@ -368,6 +376,46 @@ func (c *seaweedClient) LookupBlockVolume(ctx context.Context, in *LookupBlockVo
return out, nil
}
+func (c *seaweedClient) CreateBlockSnapshot(ctx context.Context, in *CreateBlockSnapshotRequest, opts ...grpc.CallOption) (*CreateBlockSnapshotResponse, error) {
+ cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
+ out := new(CreateBlockSnapshotResponse)
+ err := c.cc.Invoke(ctx, Seaweed_CreateBlockSnapshot_FullMethodName, in, out, cOpts...)
+ if err != nil {
+ return nil, err
+ }
+ return out, nil
+}
+
+func (c *seaweedClient) DeleteBlockSnapshot(ctx context.Context, in *DeleteBlockSnapshotRequest, opts ...grpc.CallOption) (*DeleteBlockSnapshotResponse, error) {
+ cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
+ out := new(DeleteBlockSnapshotResponse)
+ err := c.cc.Invoke(ctx, Seaweed_DeleteBlockSnapshot_FullMethodName, in, out, cOpts...)
+ if err != nil {
+ return nil, err
+ }
+ return out, nil
+}
+
+func (c *seaweedClient) ListBlockSnapshots(ctx context.Context, in *ListBlockSnapshotsRequest, opts ...grpc.CallOption) (*ListBlockSnapshotsResponse, error) {
+ cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
+ out := new(ListBlockSnapshotsResponse)
+ err := c.cc.Invoke(ctx, Seaweed_ListBlockSnapshots_FullMethodName, in, out, cOpts...)
+ if err != nil {
+ return nil, err
+ }
+ return out, nil
+}
+
+func (c *seaweedClient) ExpandBlockVolume(ctx context.Context, in *ExpandBlockVolumeRequest, opts ...grpc.CallOption) (*ExpandBlockVolumeResponse, error) {
+ cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
+ out := new(ExpandBlockVolumeResponse)
+ err := c.cc.Invoke(ctx, Seaweed_ExpandBlockVolume_FullMethodName, in, out, cOpts...)
+ if err != nil {
+ return nil, err
+ }
+ return out, nil
+}
+
// SeaweedServer is the server API for Seaweed service.
// All implementations must embed UnimplementedSeaweedServer
// for forward compatibility.
@@ -399,6 +447,10 @@ type SeaweedServer interface {
CreateBlockVolume(context.Context, *CreateBlockVolumeRequest) (*CreateBlockVolumeResponse, error)
DeleteBlockVolume(context.Context, *DeleteBlockVolumeRequest) (*DeleteBlockVolumeResponse, error)
LookupBlockVolume(context.Context, *LookupBlockVolumeRequest) (*LookupBlockVolumeResponse, error)
+ CreateBlockSnapshot(context.Context, *CreateBlockSnapshotRequest) (*CreateBlockSnapshotResponse, error)
+ DeleteBlockSnapshot(context.Context, *DeleteBlockSnapshotRequest) (*DeleteBlockSnapshotResponse, error)
+ ListBlockSnapshots(context.Context, *ListBlockSnapshotsRequest) (*ListBlockSnapshotsResponse, error)
+ ExpandBlockVolume(context.Context, *ExpandBlockVolumeRequest) (*ExpandBlockVolumeResponse, error)
mustEmbedUnimplementedSeaweedServer()
}
@@ -490,6 +542,18 @@ func (UnimplementedSeaweedServer) DeleteBlockVolume(context.Context, *DeleteBloc
func (UnimplementedSeaweedServer) LookupBlockVolume(context.Context, *LookupBlockVolumeRequest) (*LookupBlockVolumeResponse, error) {
return nil, status.Error(codes.Unimplemented, "method LookupBlockVolume not implemented")
}
+func (UnimplementedSeaweedServer) CreateBlockSnapshot(context.Context, *CreateBlockSnapshotRequest) (*CreateBlockSnapshotResponse, error) {
+ return nil, status.Error(codes.Unimplemented, "method CreateBlockSnapshot not implemented")
+}
+func (UnimplementedSeaweedServer) DeleteBlockSnapshot(context.Context, *DeleteBlockSnapshotRequest) (*DeleteBlockSnapshotResponse, error) {
+ return nil, status.Error(codes.Unimplemented, "method DeleteBlockSnapshot not implemented")
+}
+func (UnimplementedSeaweedServer) ListBlockSnapshots(context.Context, *ListBlockSnapshotsRequest) (*ListBlockSnapshotsResponse, error) {
+ return nil, status.Error(codes.Unimplemented, "method ListBlockSnapshots not implemented")
+}
+func (UnimplementedSeaweedServer) ExpandBlockVolume(context.Context, *ExpandBlockVolumeRequest) (*ExpandBlockVolumeResponse, error) {
+ return nil, status.Error(codes.Unimplemented, "method ExpandBlockVolume not implemented")
+}
func (UnimplementedSeaweedServer) mustEmbedUnimplementedSeaweedServer() {}
func (UnimplementedSeaweedServer) testEmbeddedByValue() {}
@@ -964,6 +1028,78 @@ func _Seaweed_LookupBlockVolume_Handler(srv interface{}, ctx context.Context, de
return interceptor(ctx, in, info, handler)
}
+func _Seaweed_CreateBlockSnapshot_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+ in := new(CreateBlockSnapshotRequest)
+ if err := dec(in); err != nil {
+ return nil, err
+ }
+ if interceptor == nil {
+ return srv.(SeaweedServer).CreateBlockSnapshot(ctx, in)
+ }
+ info := &grpc.UnaryServerInfo{
+ Server: srv,
+ FullMethod: Seaweed_CreateBlockSnapshot_FullMethodName,
+ }
+ handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+ return srv.(SeaweedServer).CreateBlockSnapshot(ctx, req.(*CreateBlockSnapshotRequest))
+ }
+ return interceptor(ctx, in, info, handler)
+}
+
+func _Seaweed_DeleteBlockSnapshot_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+ in := new(DeleteBlockSnapshotRequest)
+ if err := dec(in); err != nil {
+ return nil, err
+ }
+ if interceptor == nil {
+ return srv.(SeaweedServer).DeleteBlockSnapshot(ctx, in)
+ }
+ info := &grpc.UnaryServerInfo{
+ Server: srv,
+ FullMethod: Seaweed_DeleteBlockSnapshot_FullMethodName,
+ }
+ handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+ return srv.(SeaweedServer).DeleteBlockSnapshot(ctx, req.(*DeleteBlockSnapshotRequest))
+ }
+ return interceptor(ctx, in, info, handler)
+}
+
+func _Seaweed_ListBlockSnapshots_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+ in := new(ListBlockSnapshotsRequest)
+ if err := dec(in); err != nil {
+ return nil, err
+ }
+ if interceptor == nil {
+ return srv.(SeaweedServer).ListBlockSnapshots(ctx, in)
+ }
+ info := &grpc.UnaryServerInfo{
+ Server: srv,
+ FullMethod: Seaweed_ListBlockSnapshots_FullMethodName,
+ }
+ handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+ return srv.(SeaweedServer).ListBlockSnapshots(ctx, req.(*ListBlockSnapshotsRequest))
+ }
+ return interceptor(ctx, in, info, handler)
+}
+
+func _Seaweed_ExpandBlockVolume_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+ in := new(ExpandBlockVolumeRequest)
+ if err := dec(in); err != nil {
+ return nil, err
+ }
+ if interceptor == nil {
+ return srv.(SeaweedServer).ExpandBlockVolume(ctx, in)
+ }
+ info := &grpc.UnaryServerInfo{
+ Server: srv,
+ FullMethod: Seaweed_ExpandBlockVolume_FullMethodName,
+ }
+ handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+ return srv.(SeaweedServer).ExpandBlockVolume(ctx, req.(*ExpandBlockVolumeRequest))
+ }
+ return interceptor(ctx, in, info, handler)
+}
+
// Seaweed_ServiceDesc is the grpc.ServiceDesc for Seaweed service.
// It's only intended for direct use with grpc.RegisterService,
// and not to be introspected or modified (even as a copy)
@@ -1067,6 +1203,22 @@ var Seaweed_ServiceDesc = grpc.ServiceDesc{
MethodName: "LookupBlockVolume",
Handler: _Seaweed_LookupBlockVolume_Handler,
},
+ {
+ MethodName: "CreateBlockSnapshot",
+ Handler: _Seaweed_CreateBlockSnapshot_Handler,
+ },
+ {
+ MethodName: "DeleteBlockSnapshot",
+ Handler: _Seaweed_DeleteBlockSnapshot_Handler,
+ },
+ {
+ MethodName: "ListBlockSnapshots",
+ Handler: _Seaweed_ListBlockSnapshots_Handler,
+ },
+ {
+ MethodName: "ExpandBlockVolume",
+ Handler: _Seaweed_ExpandBlockVolume_Handler,
+ },
},
Streams: []grpc.StreamDesc{
{
diff --git a/weed/pb/volume_server.proto b/weed/pb/volume_server.proto
index ea55374966..28e1b67408 100644
--- a/weed/pb/volume_server.proto
+++ b/weed/pb/volume_server.proto
@@ -143,6 +143,14 @@ service VolumeServer {
}
rpc VolumeServerDeleteBlockVolume (VolumeServerDeleteBlockVolumeRequest) returns (VolumeServerDeleteBlockVolumeResponse) {
}
+ rpc SnapshotBlockVolume (SnapshotBlockVolumeRequest) returns (SnapshotBlockVolumeResponse) {
+ }
+ rpc DeleteBlockSnapshot (DeleteBlockSnapshotRequest) returns (DeleteBlockSnapshotResponse) {
+ }
+ rpc ListBlockSnapshots (ListBlockSnapshotsRequest) returns (ListBlockSnapshotsResponse) {
+ }
+ rpc ExpandBlockVolume (ExpandBlockVolumeRequest) returns (ExpandBlockVolumeResponse) {
+ }
}
@@ -786,3 +794,40 @@ message VolumeServerDeleteBlockVolumeRequest {
}
message VolumeServerDeleteBlockVolumeResponse {
}
+
+message SnapshotBlockVolumeRequest {
+ string name = 1;
+ uint32 snapshot_id = 2;
+}
+message SnapshotBlockVolumeResponse {
+ uint32 snapshot_id = 1;
+ int64 created_at = 2; // Unix timestamp in seconds
+ uint64 size_bytes = 3;
+}
+
+message DeleteBlockSnapshotRequest {
+ string name = 1;
+ uint32 snapshot_id = 2;
+}
+message DeleteBlockSnapshotResponse {
+}
+
+message ListBlockSnapshotsRequest {
+ string name = 1;
+}
+message ListBlockSnapshotsResponse {
+ repeated BlockSnapshotInfo snapshots = 1;
+}
+message BlockSnapshotInfo {
+ uint32 snapshot_id = 1;
+ int64 created_at = 2; // Unix timestamp in seconds
+ uint64 volume_size_bytes = 3;
+}
+
+message ExpandBlockVolumeRequest {
+ string name = 1;
+ uint64 new_size_bytes = 2;
+}
+message ExpandBlockVolumeResponse {
+ uint64 capacity_bytes = 1;
+}
diff --git a/weed/pb/volume_server_pb/volume_server.pb.go b/weed/pb/volume_server_pb/volume_server.pb.go
index cba59a36bf..88f77f2e33 100644
--- a/weed/pb/volume_server_pb/volume_server.pb.go
+++ b/weed/pb/volume_server_pb/volume_server.pb.go
@@ -6246,15 +6246,15 @@ func (x *AllocateBlockVolumeRequest) GetDiskType() string {
}
type AllocateBlockVolumeResponse struct {
- state protoimpl.MessageState `protogen:"open.v1"`
- Path string `protobuf:"bytes,1,opt,name=path,proto3" json:"path,omitempty"`
- Iqn string `protobuf:"bytes,2,opt,name=iqn,proto3" json:"iqn,omitempty"`
- IscsiAddr string `protobuf:"bytes,3,opt,name=iscsi_addr,json=iscsiAddr,proto3" json:"iscsi_addr,omitempty"`
- ReplicaDataAddr string `protobuf:"bytes,4,opt,name=replica_data_addr,json=replicaDataAddr,proto3" json:"replica_data_addr,omitempty"`
- ReplicaCtrlAddr string `protobuf:"bytes,5,opt,name=replica_ctrl_addr,json=replicaCtrlAddr,proto3" json:"replica_ctrl_addr,omitempty"`
- RebuildListenAddr string `protobuf:"bytes,6,opt,name=rebuild_listen_addr,json=rebuildListenAddr,proto3" json:"rebuild_listen_addr,omitempty"`
- unknownFields protoimpl.UnknownFields
- sizeCache protoimpl.SizeCache
+ state protoimpl.MessageState `protogen:"open.v1"`
+ Path string `protobuf:"bytes,1,opt,name=path,proto3" json:"path,omitempty"`
+ Iqn string `protobuf:"bytes,2,opt,name=iqn,proto3" json:"iqn,omitempty"`
+ IscsiAddr string `protobuf:"bytes,3,opt,name=iscsi_addr,json=iscsiAddr,proto3" json:"iscsi_addr,omitempty"`
+ ReplicaDataAddr string `protobuf:"bytes,4,opt,name=replica_data_addr,json=replicaDataAddr,proto3" json:"replica_data_addr,omitempty"`
+ ReplicaCtrlAddr string `protobuf:"bytes,5,opt,name=replica_ctrl_addr,json=replicaCtrlAddr,proto3" json:"replica_ctrl_addr,omitempty"`
+ RebuildListenAddr string `protobuf:"bytes,6,opt,name=rebuild_listen_addr,json=rebuildListenAddr,proto3" json:"rebuild_listen_addr,omitempty"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
}
func (x *AllocateBlockVolumeResponse) Reset() {
@@ -6409,6 +6409,450 @@ func (*VolumeServerDeleteBlockVolumeResponse) Descriptor() ([]byte, []int) {
return file_volume_server_proto_rawDescGZIP(), []int{110}
}
+type SnapshotBlockVolumeRequest struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
+ SnapshotId uint32 `protobuf:"varint,2,opt,name=snapshot_id,json=snapshotId,proto3" json:"snapshot_id,omitempty"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *SnapshotBlockVolumeRequest) Reset() {
+ *x = SnapshotBlockVolumeRequest{}
+ mi := &file_volume_server_proto_msgTypes[111]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *SnapshotBlockVolumeRequest) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*SnapshotBlockVolumeRequest) ProtoMessage() {}
+
+func (x *SnapshotBlockVolumeRequest) ProtoReflect() protoreflect.Message {
+ mi := &file_volume_server_proto_msgTypes[111]
+ if x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use SnapshotBlockVolumeRequest.ProtoReflect.Descriptor instead.
+func (*SnapshotBlockVolumeRequest) Descriptor() ([]byte, []int) {
+ return file_volume_server_proto_rawDescGZIP(), []int{111}
+}
+
+func (x *SnapshotBlockVolumeRequest) GetName() string {
+ if x != nil {
+ return x.Name
+ }
+ return ""
+}
+
+func (x *SnapshotBlockVolumeRequest) GetSnapshotId() uint32 {
+ if x != nil {
+ return x.SnapshotId
+ }
+ return 0
+}
+
+type SnapshotBlockVolumeResponse struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ SnapshotId uint32 `protobuf:"varint,1,opt,name=snapshot_id,json=snapshotId,proto3" json:"snapshot_id,omitempty"`
+ CreatedAt int64 `protobuf:"varint,2,opt,name=created_at,json=createdAt,proto3" json:"created_at,omitempty"` // Unix timestamp in seconds
+ SizeBytes uint64 `protobuf:"varint,3,opt,name=size_bytes,json=sizeBytes,proto3" json:"size_bytes,omitempty"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *SnapshotBlockVolumeResponse) Reset() {
+ *x = SnapshotBlockVolumeResponse{}
+ mi := &file_volume_server_proto_msgTypes[112]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *SnapshotBlockVolumeResponse) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*SnapshotBlockVolumeResponse) ProtoMessage() {}
+
+func (x *SnapshotBlockVolumeResponse) ProtoReflect() protoreflect.Message {
+ mi := &file_volume_server_proto_msgTypes[112]
+ if x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use SnapshotBlockVolumeResponse.ProtoReflect.Descriptor instead.
+func (*SnapshotBlockVolumeResponse) Descriptor() ([]byte, []int) {
+ return file_volume_server_proto_rawDescGZIP(), []int{112}
+}
+
+func (x *SnapshotBlockVolumeResponse) GetSnapshotId() uint32 {
+ if x != nil {
+ return x.SnapshotId
+ }
+ return 0
+}
+
+func (x *SnapshotBlockVolumeResponse) GetCreatedAt() int64 {
+ if x != nil {
+ return x.CreatedAt
+ }
+ return 0
+}
+
+func (x *SnapshotBlockVolumeResponse) GetSizeBytes() uint64 {
+ if x != nil {
+ return x.SizeBytes
+ }
+ return 0
+}
+
+type DeleteBlockSnapshotRequest struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
+ SnapshotId uint32 `protobuf:"varint,2,opt,name=snapshot_id,json=snapshotId,proto3" json:"snapshot_id,omitempty"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *DeleteBlockSnapshotRequest) Reset() {
+ *x = DeleteBlockSnapshotRequest{}
+ mi := &file_volume_server_proto_msgTypes[113]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *DeleteBlockSnapshotRequest) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*DeleteBlockSnapshotRequest) ProtoMessage() {}
+
+func (x *DeleteBlockSnapshotRequest) ProtoReflect() protoreflect.Message {
+ mi := &file_volume_server_proto_msgTypes[113]
+ if x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use DeleteBlockSnapshotRequest.ProtoReflect.Descriptor instead.
+func (*DeleteBlockSnapshotRequest) Descriptor() ([]byte, []int) {
+ return file_volume_server_proto_rawDescGZIP(), []int{113}
+}
+
+func (x *DeleteBlockSnapshotRequest) GetName() string {
+ if x != nil {
+ return x.Name
+ }
+ return ""
+}
+
+func (x *DeleteBlockSnapshotRequest) GetSnapshotId() uint32 {
+ if x != nil {
+ return x.SnapshotId
+ }
+ return 0
+}
+
+type DeleteBlockSnapshotResponse struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *DeleteBlockSnapshotResponse) Reset() {
+ *x = DeleteBlockSnapshotResponse{}
+ mi := &file_volume_server_proto_msgTypes[114]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *DeleteBlockSnapshotResponse) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*DeleteBlockSnapshotResponse) ProtoMessage() {}
+
+func (x *DeleteBlockSnapshotResponse) ProtoReflect() protoreflect.Message {
+ mi := &file_volume_server_proto_msgTypes[114]
+ if x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use DeleteBlockSnapshotResponse.ProtoReflect.Descriptor instead.
+func (*DeleteBlockSnapshotResponse) Descriptor() ([]byte, []int) {
+ return file_volume_server_proto_rawDescGZIP(), []int{114}
+}
+
+type ListBlockSnapshotsRequest struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *ListBlockSnapshotsRequest) Reset() {
+ *x = ListBlockSnapshotsRequest{}
+ mi := &file_volume_server_proto_msgTypes[115]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *ListBlockSnapshotsRequest) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*ListBlockSnapshotsRequest) ProtoMessage() {}
+
+func (x *ListBlockSnapshotsRequest) ProtoReflect() protoreflect.Message {
+ mi := &file_volume_server_proto_msgTypes[115]
+ if x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use ListBlockSnapshotsRequest.ProtoReflect.Descriptor instead.
+func (*ListBlockSnapshotsRequest) Descriptor() ([]byte, []int) {
+ return file_volume_server_proto_rawDescGZIP(), []int{115}
+}
+
+func (x *ListBlockSnapshotsRequest) GetName() string {
+ if x != nil {
+ return x.Name
+ }
+ return ""
+}
+
+type ListBlockSnapshotsResponse struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ Snapshots []*BlockSnapshotInfo `protobuf:"bytes,1,rep,name=snapshots,proto3" json:"snapshots,omitempty"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *ListBlockSnapshotsResponse) Reset() {
+ *x = ListBlockSnapshotsResponse{}
+ mi := &file_volume_server_proto_msgTypes[116]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *ListBlockSnapshotsResponse) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*ListBlockSnapshotsResponse) ProtoMessage() {}
+
+func (x *ListBlockSnapshotsResponse) ProtoReflect() protoreflect.Message {
+ mi := &file_volume_server_proto_msgTypes[116]
+ if x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use ListBlockSnapshotsResponse.ProtoReflect.Descriptor instead.
+func (*ListBlockSnapshotsResponse) Descriptor() ([]byte, []int) {
+ return file_volume_server_proto_rawDescGZIP(), []int{116}
+}
+
+func (x *ListBlockSnapshotsResponse) GetSnapshots() []*BlockSnapshotInfo {
+ if x != nil {
+ return x.Snapshots
+ }
+ return nil
+}
+
+type BlockSnapshotInfo struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ SnapshotId uint32 `protobuf:"varint,1,opt,name=snapshot_id,json=snapshotId,proto3" json:"snapshot_id,omitempty"`
+ CreatedAt int64 `protobuf:"varint,2,opt,name=created_at,json=createdAt,proto3" json:"created_at,omitempty"` // Unix timestamp in seconds
+ VolumeSizeBytes uint64 `protobuf:"varint,3,opt,name=volume_size_bytes,json=volumeSizeBytes,proto3" json:"volume_size_bytes,omitempty"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *BlockSnapshotInfo) Reset() {
+ *x = BlockSnapshotInfo{}
+ mi := &file_volume_server_proto_msgTypes[117]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *BlockSnapshotInfo) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*BlockSnapshotInfo) ProtoMessage() {}
+
+func (x *BlockSnapshotInfo) ProtoReflect() protoreflect.Message {
+ mi := &file_volume_server_proto_msgTypes[117]
+ if x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use BlockSnapshotInfo.ProtoReflect.Descriptor instead.
+func (*BlockSnapshotInfo) Descriptor() ([]byte, []int) {
+ return file_volume_server_proto_rawDescGZIP(), []int{117}
+}
+
+func (x *BlockSnapshotInfo) GetSnapshotId() uint32 {
+ if x != nil {
+ return x.SnapshotId
+ }
+ return 0
+}
+
+func (x *BlockSnapshotInfo) GetCreatedAt() int64 {
+ if x != nil {
+ return x.CreatedAt
+ }
+ return 0
+}
+
+func (x *BlockSnapshotInfo) GetVolumeSizeBytes() uint64 {
+ if x != nil {
+ return x.VolumeSizeBytes
+ }
+ return 0
+}
+
+type ExpandBlockVolumeRequest struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
+ NewSizeBytes uint64 `protobuf:"varint,2,opt,name=new_size_bytes,json=newSizeBytes,proto3" json:"new_size_bytes,omitempty"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *ExpandBlockVolumeRequest) Reset() {
+ *x = ExpandBlockVolumeRequest{}
+ mi := &file_volume_server_proto_msgTypes[118]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *ExpandBlockVolumeRequest) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*ExpandBlockVolumeRequest) ProtoMessage() {}
+
+func (x *ExpandBlockVolumeRequest) ProtoReflect() protoreflect.Message {
+ mi := &file_volume_server_proto_msgTypes[118]
+ if x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use ExpandBlockVolumeRequest.ProtoReflect.Descriptor instead.
+func (*ExpandBlockVolumeRequest) Descriptor() ([]byte, []int) {
+ return file_volume_server_proto_rawDescGZIP(), []int{118}
+}
+
+func (x *ExpandBlockVolumeRequest) GetName() string {
+ if x != nil {
+ return x.Name
+ }
+ return ""
+}
+
+func (x *ExpandBlockVolumeRequest) GetNewSizeBytes() uint64 {
+ if x != nil {
+ return x.NewSizeBytes
+ }
+ return 0
+}
+
+type ExpandBlockVolumeResponse struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ CapacityBytes uint64 `protobuf:"varint,1,opt,name=capacity_bytes,json=capacityBytes,proto3" json:"capacity_bytes,omitempty"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *ExpandBlockVolumeResponse) Reset() {
+ *x = ExpandBlockVolumeResponse{}
+ mi := &file_volume_server_proto_msgTypes[119]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *ExpandBlockVolumeResponse) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*ExpandBlockVolumeResponse) ProtoMessage() {}
+
+func (x *ExpandBlockVolumeResponse) ProtoReflect() protoreflect.Message {
+ mi := &file_volume_server_proto_msgTypes[119]
+ if x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use ExpandBlockVolumeResponse.ProtoReflect.Descriptor instead.
+func (*ExpandBlockVolumeResponse) Descriptor() ([]byte, []int) {
+ return file_volume_server_proto_rawDescGZIP(), []int{119}
+}
+
+func (x *ExpandBlockVolumeResponse) GetCapacityBytes() uint64 {
+ if x != nil {
+ return x.CapacityBytes
+ }
+ return 0
+}
+
type FetchAndWriteNeedleRequest_Replica struct {
state protoimpl.MessageState `protogen:"open.v1"`
Url string `protobuf:"bytes,1,opt,name=url,proto3" json:"url,omitempty"`
@@ -6420,7 +6864,7 @@ type FetchAndWriteNeedleRequest_Replica struct {
func (x *FetchAndWriteNeedleRequest_Replica) Reset() {
*x = FetchAndWriteNeedleRequest_Replica{}
- mi := &file_volume_server_proto_msgTypes[111]
+ mi := &file_volume_server_proto_msgTypes[120]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -6432,7 +6876,7 @@ func (x *FetchAndWriteNeedleRequest_Replica) String() string {
func (*FetchAndWriteNeedleRequest_Replica) ProtoMessage() {}
func (x *FetchAndWriteNeedleRequest_Replica) ProtoReflect() protoreflect.Message {
- mi := &file_volume_server_proto_msgTypes[111]
+ mi := &file_volume_server_proto_msgTypes[120]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -6480,7 +6924,7 @@ type QueryRequest_Filter struct {
func (x *QueryRequest_Filter) Reset() {
*x = QueryRequest_Filter{}
- mi := &file_volume_server_proto_msgTypes[112]
+ mi := &file_volume_server_proto_msgTypes[121]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -6492,7 +6936,7 @@ func (x *QueryRequest_Filter) String() string {
func (*QueryRequest_Filter) ProtoMessage() {}
func (x *QueryRequest_Filter) ProtoReflect() protoreflect.Message {
- mi := &file_volume_server_proto_msgTypes[112]
+ mi := &file_volume_server_proto_msgTypes[121]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -6542,7 +6986,7 @@ type QueryRequest_InputSerialization struct {
func (x *QueryRequest_InputSerialization) Reset() {
*x = QueryRequest_InputSerialization{}
- mi := &file_volume_server_proto_msgTypes[113]
+ mi := &file_volume_server_proto_msgTypes[122]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -6554,7 +6998,7 @@ func (x *QueryRequest_InputSerialization) String() string {
func (*QueryRequest_InputSerialization) ProtoMessage() {}
func (x *QueryRequest_InputSerialization) ProtoReflect() protoreflect.Message {
- mi := &file_volume_server_proto_msgTypes[113]
+ mi := &file_volume_server_proto_msgTypes[122]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -6608,7 +7052,7 @@ type QueryRequest_OutputSerialization struct {
func (x *QueryRequest_OutputSerialization) Reset() {
*x = QueryRequest_OutputSerialization{}
- mi := &file_volume_server_proto_msgTypes[114]
+ mi := &file_volume_server_proto_msgTypes[123]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -6620,7 +7064,7 @@ func (x *QueryRequest_OutputSerialization) String() string {
func (*QueryRequest_OutputSerialization) ProtoMessage() {}
func (x *QueryRequest_OutputSerialization) ProtoReflect() protoreflect.Message {
- mi := &file_volume_server_proto_msgTypes[114]
+ mi := &file_volume_server_proto_msgTypes[123]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -6666,7 +7110,7 @@ type QueryRequest_InputSerialization_CSVInput struct {
func (x *QueryRequest_InputSerialization_CSVInput) Reset() {
*x = QueryRequest_InputSerialization_CSVInput{}
- mi := &file_volume_server_proto_msgTypes[115]
+ mi := &file_volume_server_proto_msgTypes[124]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -6678,7 +7122,7 @@ func (x *QueryRequest_InputSerialization_CSVInput) String() string {
func (*QueryRequest_InputSerialization_CSVInput) ProtoMessage() {}
func (x *QueryRequest_InputSerialization_CSVInput) ProtoReflect() protoreflect.Message {
- mi := &file_volume_server_proto_msgTypes[115]
+ mi := &file_volume_server_proto_msgTypes[124]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -6752,7 +7196,7 @@ type QueryRequest_InputSerialization_JSONInput struct {
func (x *QueryRequest_InputSerialization_JSONInput) Reset() {
*x = QueryRequest_InputSerialization_JSONInput{}
- mi := &file_volume_server_proto_msgTypes[116]
+ mi := &file_volume_server_proto_msgTypes[125]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -6764,7 +7208,7 @@ func (x *QueryRequest_InputSerialization_JSONInput) String() string {
func (*QueryRequest_InputSerialization_JSONInput) ProtoMessage() {}
func (x *QueryRequest_InputSerialization_JSONInput) ProtoReflect() protoreflect.Message {
- mi := &file_volume_server_proto_msgTypes[116]
+ mi := &file_volume_server_proto_msgTypes[125]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -6795,7 +7239,7 @@ type QueryRequest_InputSerialization_ParquetInput struct {
func (x *QueryRequest_InputSerialization_ParquetInput) Reset() {
*x = QueryRequest_InputSerialization_ParquetInput{}
- mi := &file_volume_server_proto_msgTypes[117]
+ mi := &file_volume_server_proto_msgTypes[126]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -6807,7 +7251,7 @@ func (x *QueryRequest_InputSerialization_ParquetInput) String() string {
func (*QueryRequest_InputSerialization_ParquetInput) ProtoMessage() {}
func (x *QueryRequest_InputSerialization_ParquetInput) ProtoReflect() protoreflect.Message {
- mi := &file_volume_server_proto_msgTypes[117]
+ mi := &file_volume_server_proto_msgTypes[126]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -6836,7 +7280,7 @@ type QueryRequest_OutputSerialization_CSVOutput struct {
func (x *QueryRequest_OutputSerialization_CSVOutput) Reset() {
*x = QueryRequest_OutputSerialization_CSVOutput{}
- mi := &file_volume_server_proto_msgTypes[118]
+ mi := &file_volume_server_proto_msgTypes[127]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -6848,7 +7292,7 @@ func (x *QueryRequest_OutputSerialization_CSVOutput) String() string {
func (*QueryRequest_OutputSerialization_CSVOutput) ProtoMessage() {}
func (x *QueryRequest_OutputSerialization_CSVOutput) ProtoReflect() protoreflect.Message {
- mi := &file_volume_server_proto_msgTypes[118]
+ mi := &file_volume_server_proto_msgTypes[127]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -6908,7 +7352,7 @@ type QueryRequest_OutputSerialization_JSONOutput struct {
func (x *QueryRequest_OutputSerialization_JSONOutput) Reset() {
*x = QueryRequest_OutputSerialization_JSONOutput{}
- mi := &file_volume_server_proto_msgTypes[119]
+ mi := &file_volume_server_proto_msgTypes[128]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -6920,7 +7364,7 @@ func (x *QueryRequest_OutputSerialization_JSONOutput) String() string {
func (*QueryRequest_OutputSerialization_JSONOutput) ProtoMessage() {}
func (x *QueryRequest_OutputSerialization_JSONOutput) ProtoReflect() protoreflect.Message {
- mi := &file_volume_server_proto_msgTypes[119]
+ mi := &file_volume_server_proto_msgTypes[128]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -7451,20 +7895,54 @@ const file_volume_server_proto_rawDesc = "" +
"\x04name\x18\x01 \x01(\tR\x04name\x12\x1d\n" +
"\n" +
"size_bytes\x18\x02 \x01(\x04R\tsizeBytes\x12\x1b\n" +
- "\tdisk_type\x18\x03 \x01(\tR\bdiskType\"b\n" +
+ "\tdisk_type\x18\x03 \x01(\tR\bdiskType\"\xea\x01\n" +
"\x1bAllocateBlockVolumeResponse\x12\x12\n" +
"\x04path\x18\x01 \x01(\tR\x04path\x12\x10\n" +
"\x03iqn\x18\x02 \x01(\tR\x03iqn\x12\x1d\n" +
"\n" +
- "iscsi_addr\x18\x03 \x01(\tR\tiscsiAddr\":\n" +
+ "iscsi_addr\x18\x03 \x01(\tR\tiscsiAddr\x12*\n" +
+ "\x11replica_data_addr\x18\x04 \x01(\tR\x0freplicaDataAddr\x12*\n" +
+ "\x11replica_ctrl_addr\x18\x05 \x01(\tR\x0freplicaCtrlAddr\x12.\n" +
+ "\x13rebuild_listen_addr\x18\x06 \x01(\tR\x11rebuildListenAddr\":\n" +
"$VolumeServerDeleteBlockVolumeRequest\x12\x12\n" +
"\x04name\x18\x01 \x01(\tR\x04name\"'\n" +
- "%VolumeServerDeleteBlockVolumeResponse*>\n" +
+ "%VolumeServerDeleteBlockVolumeResponse\"Q\n" +
+ "\x1aSnapshotBlockVolumeRequest\x12\x12\n" +
+ "\x04name\x18\x01 \x01(\tR\x04name\x12\x1f\n" +
+ "\vsnapshot_id\x18\x02 \x01(\rR\n" +
+ "snapshotId\"|\n" +
+ "\x1bSnapshotBlockVolumeResponse\x12\x1f\n" +
+ "\vsnapshot_id\x18\x01 \x01(\rR\n" +
+ "snapshotId\x12\x1d\n" +
+ "\n" +
+ "created_at\x18\x02 \x01(\x03R\tcreatedAt\x12\x1d\n" +
+ "\n" +
+ "size_bytes\x18\x03 \x01(\x04R\tsizeBytes\"Q\n" +
+ "\x1aDeleteBlockSnapshotRequest\x12\x12\n" +
+ "\x04name\x18\x01 \x01(\tR\x04name\x12\x1f\n" +
+ "\vsnapshot_id\x18\x02 \x01(\rR\n" +
+ "snapshotId\"\x1d\n" +
+ "\x1bDeleteBlockSnapshotResponse\"/\n" +
+ "\x19ListBlockSnapshotsRequest\x12\x12\n" +
+ "\x04name\x18\x01 \x01(\tR\x04name\"_\n" +
+ "\x1aListBlockSnapshotsResponse\x12A\n" +
+ "\tsnapshots\x18\x01 \x03(\v2#.volume_server_pb.BlockSnapshotInfoR\tsnapshots\"\x7f\n" +
+ "\x11BlockSnapshotInfo\x12\x1f\n" +
+ "\vsnapshot_id\x18\x01 \x01(\rR\n" +
+ "snapshotId\x12\x1d\n" +
+ "\n" +
+ "created_at\x18\x02 \x01(\x03R\tcreatedAt\x12*\n" +
+ "\x11volume_size_bytes\x18\x03 \x01(\x04R\x0fvolumeSizeBytes\"T\n" +
+ "\x18ExpandBlockVolumeRequest\x12\x12\n" +
+ "\x04name\x18\x01 \x01(\tR\x04name\x12$\n" +
+ "\x0enew_size_bytes\x18\x02 \x01(\x04R\fnewSizeBytes\"B\n" +
+ "\x19ExpandBlockVolumeResponse\x12%\n" +
+ "\x0ecapacity_bytes\x18\x01 \x01(\x04R\rcapacityBytes*>\n" +
"\x0fVolumeScrubMode\x12\v\n" +
"\aUNKNOWN\x10\x00\x12\t\n" +
"\x05INDEX\x10\x01\x12\b\n" +
"\x04FULL\x10\x02\x12\t\n" +
- "\x05LOCAL\x10\x032\x86+\n" +
+ "\x05LOCAL\x10\x032\xd5.\n" +
"\fVolumeServer\x12\\\n" +
"\vBatchDelete\x12$.volume_server_pb.BatchDeleteRequest\x1a%.volume_server_pb.BatchDeleteResponse\"\x00\x12n\n" +
"\x11VacuumVolumeCheck\x12*.volume_server_pb.VacuumVolumeCheckRequest\x1a+.volume_server_pb.VacuumVolumeCheckResponse\"\x00\x12v\n" +
@@ -7516,7 +7994,11 @@ const file_volume_server_proto_rawDesc = "" +
"\x12VolumeNeedleStatus\x12+.volume_server_pb.VolumeNeedleStatusRequest\x1a,.volume_server_pb.VolumeNeedleStatusResponse\"\x00\x12G\n" +
"\x04Ping\x12\x1d.volume_server_pb.PingRequest\x1a\x1e.volume_server_pb.PingResponse\"\x00\x12t\n" +
"\x13AllocateBlockVolume\x12,.volume_server_pb.AllocateBlockVolumeRequest\x1a-.volume_server_pb.AllocateBlockVolumeResponse\"\x00\x12\x92\x01\n" +
- "\x1dVolumeServerDeleteBlockVolume\x126.volume_server_pb.VolumeServerDeleteBlockVolumeRequest\x1a7.volume_server_pb.VolumeServerDeleteBlockVolumeResponse\"\x00B9Z7github.com/seaweedfs/seaweedfs/weed/pb/volume_server_pbb\x06proto3"
+ "\x1dVolumeServerDeleteBlockVolume\x126.volume_server_pb.VolumeServerDeleteBlockVolumeRequest\x1a7.volume_server_pb.VolumeServerDeleteBlockVolumeResponse\"\x00\x12t\n" +
+ "\x13SnapshotBlockVolume\x12,.volume_server_pb.SnapshotBlockVolumeRequest\x1a-.volume_server_pb.SnapshotBlockVolumeResponse\"\x00\x12t\n" +
+ "\x13DeleteBlockSnapshot\x12,.volume_server_pb.DeleteBlockSnapshotRequest\x1a-.volume_server_pb.DeleteBlockSnapshotResponse\"\x00\x12q\n" +
+ "\x12ListBlockSnapshots\x12+.volume_server_pb.ListBlockSnapshotsRequest\x1a,.volume_server_pb.ListBlockSnapshotsResponse\"\x00\x12n\n" +
+ "\x11ExpandBlockVolume\x12*.volume_server_pb.ExpandBlockVolumeRequest\x1a+.volume_server_pb.ExpandBlockVolumeResponse\"\x00B9Z7github.com/seaweedfs/seaweedfs/weed/pb/volume_server_pbb\x06proto3"
var (
file_volume_server_proto_rawDescOnce sync.Once
@@ -7531,7 +8013,7 @@ func file_volume_server_proto_rawDescGZIP() []byte {
}
var file_volume_server_proto_enumTypes = make([]protoimpl.EnumInfo, 1)
-var file_volume_server_proto_msgTypes = make([]protoimpl.MessageInfo, 120)
+var file_volume_server_proto_msgTypes = make([]protoimpl.MessageInfo, 129)
var file_volume_server_proto_goTypes = []any{
(VolumeScrubMode)(0), // 0: volume_server_pb.VolumeScrubMode
(*VolumeServerState)(nil), // 1: volume_server_pb.VolumeServerState
@@ -7645,17 +8127,26 @@ var file_volume_server_proto_goTypes = []any{
(*AllocateBlockVolumeResponse)(nil), // 109: volume_server_pb.AllocateBlockVolumeResponse
(*VolumeServerDeleteBlockVolumeRequest)(nil), // 110: volume_server_pb.VolumeServerDeleteBlockVolumeRequest
(*VolumeServerDeleteBlockVolumeResponse)(nil), // 111: volume_server_pb.VolumeServerDeleteBlockVolumeResponse
- (*FetchAndWriteNeedleRequest_Replica)(nil), // 112: volume_server_pb.FetchAndWriteNeedleRequest.Replica
- (*QueryRequest_Filter)(nil), // 113: volume_server_pb.QueryRequest.Filter
- (*QueryRequest_InputSerialization)(nil), // 114: volume_server_pb.QueryRequest.InputSerialization
- (*QueryRequest_OutputSerialization)(nil), // 115: volume_server_pb.QueryRequest.OutputSerialization
- (*QueryRequest_InputSerialization_CSVInput)(nil), // 116: volume_server_pb.QueryRequest.InputSerialization.CSVInput
- (*QueryRequest_InputSerialization_JSONInput)(nil), // 117: volume_server_pb.QueryRequest.InputSerialization.JSONInput
- (*QueryRequest_InputSerialization_ParquetInput)(nil), // 118: volume_server_pb.QueryRequest.InputSerialization.ParquetInput
- (*QueryRequest_OutputSerialization_CSVOutput)(nil), // 119: volume_server_pb.QueryRequest.OutputSerialization.CSVOutput
- (*QueryRequest_OutputSerialization_JSONOutput)(nil), // 120: volume_server_pb.QueryRequest.OutputSerialization.JSONOutput
- (*remote_pb.RemoteConf)(nil), // 121: remote_pb.RemoteConf
- (*remote_pb.RemoteStorageLocation)(nil), // 122: remote_pb.RemoteStorageLocation
+ (*SnapshotBlockVolumeRequest)(nil), // 112: volume_server_pb.SnapshotBlockVolumeRequest
+ (*SnapshotBlockVolumeResponse)(nil), // 113: volume_server_pb.SnapshotBlockVolumeResponse
+ (*DeleteBlockSnapshotRequest)(nil), // 114: volume_server_pb.DeleteBlockSnapshotRequest
+ (*DeleteBlockSnapshotResponse)(nil), // 115: volume_server_pb.DeleteBlockSnapshotResponse
+ (*ListBlockSnapshotsRequest)(nil), // 116: volume_server_pb.ListBlockSnapshotsRequest
+ (*ListBlockSnapshotsResponse)(nil), // 117: volume_server_pb.ListBlockSnapshotsResponse
+ (*BlockSnapshotInfo)(nil), // 118: volume_server_pb.BlockSnapshotInfo
+ (*ExpandBlockVolumeRequest)(nil), // 119: volume_server_pb.ExpandBlockVolumeRequest
+ (*ExpandBlockVolumeResponse)(nil), // 120: volume_server_pb.ExpandBlockVolumeResponse
+ (*FetchAndWriteNeedleRequest_Replica)(nil), // 121: volume_server_pb.FetchAndWriteNeedleRequest.Replica
+ (*QueryRequest_Filter)(nil), // 122: volume_server_pb.QueryRequest.Filter
+ (*QueryRequest_InputSerialization)(nil), // 123: volume_server_pb.QueryRequest.InputSerialization
+ (*QueryRequest_OutputSerialization)(nil), // 124: volume_server_pb.QueryRequest.OutputSerialization
+ (*QueryRequest_InputSerialization_CSVInput)(nil), // 125: volume_server_pb.QueryRequest.InputSerialization.CSVInput
+ (*QueryRequest_InputSerialization_JSONInput)(nil), // 126: volume_server_pb.QueryRequest.InputSerialization.JSONInput
+ (*QueryRequest_InputSerialization_ParquetInput)(nil), // 127: volume_server_pb.QueryRequest.InputSerialization.ParquetInput
+ (*QueryRequest_OutputSerialization_CSVOutput)(nil), // 128: volume_server_pb.QueryRequest.OutputSerialization.CSVOutput
+ (*QueryRequest_OutputSerialization_JSONOutput)(nil), // 129: volume_server_pb.QueryRequest.OutputSerialization.JSONOutput
+ (*remote_pb.RemoteConf)(nil), // 130: remote_pb.RemoteConf
+ (*remote_pb.RemoteStorageLocation)(nil), // 131: remote_pb.RemoteStorageLocation
}
var file_volume_server_proto_depIdxs = []int32{
4, // 0: volume_server_pb.BatchDeleteResponse.results:type_name -> volume_server_pb.DeleteResult
@@ -7671,125 +8162,134 @@ var file_volume_server_proto_depIdxs = []int32{
82, // 10: volume_server_pb.VolumeServerStatusResponse.disk_statuses:type_name -> volume_server_pb.DiskStatus
83, // 11: volume_server_pb.VolumeServerStatusResponse.memory_status:type_name -> volume_server_pb.MemStatus
1, // 12: volume_server_pb.VolumeServerStatusResponse.state:type_name -> volume_server_pb.VolumeServerState
- 112, // 13: volume_server_pb.FetchAndWriteNeedleRequest.replicas:type_name -> volume_server_pb.FetchAndWriteNeedleRequest.Replica
- 121, // 14: volume_server_pb.FetchAndWriteNeedleRequest.remote_conf:type_name -> remote_pb.RemoteConf
- 122, // 15: volume_server_pb.FetchAndWriteNeedleRequest.remote_location:type_name -> remote_pb.RemoteStorageLocation
+ 121, // 13: volume_server_pb.FetchAndWriteNeedleRequest.replicas:type_name -> volume_server_pb.FetchAndWriteNeedleRequest.Replica
+ 130, // 14: volume_server_pb.FetchAndWriteNeedleRequest.remote_conf:type_name -> remote_pb.RemoteConf
+ 131, // 15: volume_server_pb.FetchAndWriteNeedleRequest.remote_location:type_name -> remote_pb.RemoteStorageLocation
0, // 16: volume_server_pb.ScrubVolumeRequest.mode:type_name -> volume_server_pb.VolumeScrubMode
0, // 17: volume_server_pb.ScrubEcVolumeRequest.mode:type_name -> volume_server_pb.VolumeScrubMode
79, // 18: volume_server_pb.ScrubEcVolumeResponse.broken_shard_infos:type_name -> volume_server_pb.EcShardInfo
- 113, // 19: volume_server_pb.QueryRequest.filter:type_name -> volume_server_pb.QueryRequest.Filter
- 114, // 20: volume_server_pb.QueryRequest.input_serialization:type_name -> volume_server_pb.QueryRequest.InputSerialization
- 115, // 21: volume_server_pb.QueryRequest.output_serialization:type_name -> volume_server_pb.QueryRequest.OutputSerialization
- 116, // 22: volume_server_pb.QueryRequest.InputSerialization.csv_input:type_name -> volume_server_pb.QueryRequest.InputSerialization.CSVInput
- 117, // 23: volume_server_pb.QueryRequest.InputSerialization.json_input:type_name -> volume_server_pb.QueryRequest.InputSerialization.JSONInput
- 118, // 24: volume_server_pb.QueryRequest.InputSerialization.parquet_input:type_name -> volume_server_pb.QueryRequest.InputSerialization.ParquetInput
- 119, // 25: volume_server_pb.QueryRequest.OutputSerialization.csv_output:type_name -> volume_server_pb.QueryRequest.OutputSerialization.CSVOutput
- 120, // 26: volume_server_pb.QueryRequest.OutputSerialization.json_output:type_name -> volume_server_pb.QueryRequest.OutputSerialization.JSONOutput
- 2, // 27: volume_server_pb.VolumeServer.BatchDelete:input_type -> volume_server_pb.BatchDeleteRequest
- 6, // 28: volume_server_pb.VolumeServer.VacuumVolumeCheck:input_type -> volume_server_pb.VacuumVolumeCheckRequest
- 8, // 29: volume_server_pb.VolumeServer.VacuumVolumeCompact:input_type -> volume_server_pb.VacuumVolumeCompactRequest
- 10, // 30: volume_server_pb.VolumeServer.VacuumVolumeCommit:input_type -> volume_server_pb.VacuumVolumeCommitRequest
- 12, // 31: volume_server_pb.VolumeServer.VacuumVolumeCleanup:input_type -> volume_server_pb.VacuumVolumeCleanupRequest
- 14, // 32: volume_server_pb.VolumeServer.DeleteCollection:input_type -> volume_server_pb.DeleteCollectionRequest
- 16, // 33: volume_server_pb.VolumeServer.AllocateVolume:input_type -> volume_server_pb.AllocateVolumeRequest
- 18, // 34: volume_server_pb.VolumeServer.VolumeSyncStatus:input_type -> volume_server_pb.VolumeSyncStatusRequest
- 20, // 35: volume_server_pb.VolumeServer.VolumeIncrementalCopy:input_type -> volume_server_pb.VolumeIncrementalCopyRequest
- 22, // 36: volume_server_pb.VolumeServer.VolumeMount:input_type -> volume_server_pb.VolumeMountRequest
- 24, // 37: volume_server_pb.VolumeServer.VolumeUnmount:input_type -> volume_server_pb.VolumeUnmountRequest
- 26, // 38: volume_server_pb.VolumeServer.VolumeDelete:input_type -> volume_server_pb.VolumeDeleteRequest
- 28, // 39: volume_server_pb.VolumeServer.VolumeMarkReadonly:input_type -> volume_server_pb.VolumeMarkReadonlyRequest
- 30, // 40: volume_server_pb.VolumeServer.VolumeMarkWritable:input_type -> volume_server_pb.VolumeMarkWritableRequest
- 32, // 41: volume_server_pb.VolumeServer.VolumeConfigure:input_type -> volume_server_pb.VolumeConfigureRequest
- 34, // 42: volume_server_pb.VolumeServer.VolumeStatus:input_type -> volume_server_pb.VolumeStatusRequest
- 36, // 43: volume_server_pb.VolumeServer.GetState:input_type -> volume_server_pb.GetStateRequest
- 38, // 44: volume_server_pb.VolumeServer.SetState:input_type -> volume_server_pb.SetStateRequest
- 40, // 45: volume_server_pb.VolumeServer.VolumeCopy:input_type -> volume_server_pb.VolumeCopyRequest
- 80, // 46: volume_server_pb.VolumeServer.ReadVolumeFileStatus:input_type -> volume_server_pb.ReadVolumeFileStatusRequest
- 42, // 47: volume_server_pb.VolumeServer.CopyFile:input_type -> volume_server_pb.CopyFileRequest
- 44, // 48: volume_server_pb.VolumeServer.ReceiveFile:input_type -> volume_server_pb.ReceiveFileRequest
- 47, // 49: volume_server_pb.VolumeServer.ReadNeedleBlob:input_type -> volume_server_pb.ReadNeedleBlobRequest
- 49, // 50: volume_server_pb.VolumeServer.ReadNeedleMeta:input_type -> volume_server_pb.ReadNeedleMetaRequest
- 51, // 51: volume_server_pb.VolumeServer.WriteNeedleBlob:input_type -> volume_server_pb.WriteNeedleBlobRequest
- 53, // 52: volume_server_pb.VolumeServer.ReadAllNeedles:input_type -> volume_server_pb.ReadAllNeedlesRequest
- 55, // 53: volume_server_pb.VolumeServer.VolumeTailSender:input_type -> volume_server_pb.VolumeTailSenderRequest
- 57, // 54: volume_server_pb.VolumeServer.VolumeTailReceiver:input_type -> volume_server_pb.VolumeTailReceiverRequest
- 59, // 55: volume_server_pb.VolumeServer.VolumeEcShardsGenerate:input_type -> volume_server_pb.VolumeEcShardsGenerateRequest
- 61, // 56: volume_server_pb.VolumeServer.VolumeEcShardsRebuild:input_type -> volume_server_pb.VolumeEcShardsRebuildRequest
- 63, // 57: volume_server_pb.VolumeServer.VolumeEcShardsCopy:input_type -> volume_server_pb.VolumeEcShardsCopyRequest
- 65, // 58: volume_server_pb.VolumeServer.VolumeEcShardsDelete:input_type -> volume_server_pb.VolumeEcShardsDeleteRequest
- 67, // 59: volume_server_pb.VolumeServer.VolumeEcShardsMount:input_type -> volume_server_pb.VolumeEcShardsMountRequest
- 69, // 60: volume_server_pb.VolumeServer.VolumeEcShardsUnmount:input_type -> volume_server_pb.VolumeEcShardsUnmountRequest
- 71, // 61: volume_server_pb.VolumeServer.VolumeEcShardRead:input_type -> volume_server_pb.VolumeEcShardReadRequest
- 73, // 62: volume_server_pb.VolumeServer.VolumeEcBlobDelete:input_type -> volume_server_pb.VolumeEcBlobDeleteRequest
- 75, // 63: volume_server_pb.VolumeServer.VolumeEcShardsToVolume:input_type -> volume_server_pb.VolumeEcShardsToVolumeRequest
- 77, // 64: volume_server_pb.VolumeServer.VolumeEcShardsInfo:input_type -> volume_server_pb.VolumeEcShardsInfoRequest
- 88, // 65: volume_server_pb.VolumeServer.VolumeTierMoveDatToRemote:input_type -> volume_server_pb.VolumeTierMoveDatToRemoteRequest
- 90, // 66: volume_server_pb.VolumeServer.VolumeTierMoveDatFromRemote:input_type -> volume_server_pb.VolumeTierMoveDatFromRemoteRequest
- 92, // 67: volume_server_pb.VolumeServer.VolumeServerStatus:input_type -> volume_server_pb.VolumeServerStatusRequest
- 94, // 68: volume_server_pb.VolumeServer.VolumeServerLeave:input_type -> volume_server_pb.VolumeServerLeaveRequest
- 96, // 69: volume_server_pb.VolumeServer.FetchAndWriteNeedle:input_type -> volume_server_pb.FetchAndWriteNeedleRequest
- 98, // 70: volume_server_pb.VolumeServer.ScrubVolume:input_type -> volume_server_pb.ScrubVolumeRequest
- 100, // 71: volume_server_pb.VolumeServer.ScrubEcVolume:input_type -> volume_server_pb.ScrubEcVolumeRequest
- 102, // 72: volume_server_pb.VolumeServer.Query:input_type -> volume_server_pb.QueryRequest
- 104, // 73: volume_server_pb.VolumeServer.VolumeNeedleStatus:input_type -> volume_server_pb.VolumeNeedleStatusRequest
- 106, // 74: volume_server_pb.VolumeServer.Ping:input_type -> volume_server_pb.PingRequest
- 108, // 75: volume_server_pb.VolumeServer.AllocateBlockVolume:input_type -> volume_server_pb.AllocateBlockVolumeRequest
- 110, // 76: volume_server_pb.VolumeServer.VolumeServerDeleteBlockVolume:input_type -> volume_server_pb.VolumeServerDeleteBlockVolumeRequest
- 3, // 77: volume_server_pb.VolumeServer.BatchDelete:output_type -> volume_server_pb.BatchDeleteResponse
- 7, // 78: volume_server_pb.VolumeServer.VacuumVolumeCheck:output_type -> volume_server_pb.VacuumVolumeCheckResponse
- 9, // 79: volume_server_pb.VolumeServer.VacuumVolumeCompact:output_type -> volume_server_pb.VacuumVolumeCompactResponse
- 11, // 80: volume_server_pb.VolumeServer.VacuumVolumeCommit:output_type -> volume_server_pb.VacuumVolumeCommitResponse
- 13, // 81: volume_server_pb.VolumeServer.VacuumVolumeCleanup:output_type -> volume_server_pb.VacuumVolumeCleanupResponse
- 15, // 82: volume_server_pb.VolumeServer.DeleteCollection:output_type -> volume_server_pb.DeleteCollectionResponse
- 17, // 83: volume_server_pb.VolumeServer.AllocateVolume:output_type -> volume_server_pb.AllocateVolumeResponse
- 19, // 84: volume_server_pb.VolumeServer.VolumeSyncStatus:output_type -> volume_server_pb.VolumeSyncStatusResponse
- 21, // 85: volume_server_pb.VolumeServer.VolumeIncrementalCopy:output_type -> volume_server_pb.VolumeIncrementalCopyResponse
- 23, // 86: volume_server_pb.VolumeServer.VolumeMount:output_type -> volume_server_pb.VolumeMountResponse
- 25, // 87: volume_server_pb.VolumeServer.VolumeUnmount:output_type -> volume_server_pb.VolumeUnmountResponse
- 27, // 88: volume_server_pb.VolumeServer.VolumeDelete:output_type -> volume_server_pb.VolumeDeleteResponse
- 29, // 89: volume_server_pb.VolumeServer.VolumeMarkReadonly:output_type -> volume_server_pb.VolumeMarkReadonlyResponse
- 31, // 90: volume_server_pb.VolumeServer.VolumeMarkWritable:output_type -> volume_server_pb.VolumeMarkWritableResponse
- 33, // 91: volume_server_pb.VolumeServer.VolumeConfigure:output_type -> volume_server_pb.VolumeConfigureResponse
- 35, // 92: volume_server_pb.VolumeServer.VolumeStatus:output_type -> volume_server_pb.VolumeStatusResponse
- 37, // 93: volume_server_pb.VolumeServer.GetState:output_type -> volume_server_pb.GetStateResponse
- 39, // 94: volume_server_pb.VolumeServer.SetState:output_type -> volume_server_pb.SetStateResponse
- 41, // 95: volume_server_pb.VolumeServer.VolumeCopy:output_type -> volume_server_pb.VolumeCopyResponse
- 81, // 96: volume_server_pb.VolumeServer.ReadVolumeFileStatus:output_type -> volume_server_pb.ReadVolumeFileStatusResponse
- 43, // 97: volume_server_pb.VolumeServer.CopyFile:output_type -> volume_server_pb.CopyFileResponse
- 46, // 98: volume_server_pb.VolumeServer.ReceiveFile:output_type -> volume_server_pb.ReceiveFileResponse
- 48, // 99: volume_server_pb.VolumeServer.ReadNeedleBlob:output_type -> volume_server_pb.ReadNeedleBlobResponse
- 50, // 100: volume_server_pb.VolumeServer.ReadNeedleMeta:output_type -> volume_server_pb.ReadNeedleMetaResponse
- 52, // 101: volume_server_pb.VolumeServer.WriteNeedleBlob:output_type -> volume_server_pb.WriteNeedleBlobResponse
- 54, // 102: volume_server_pb.VolumeServer.ReadAllNeedles:output_type -> volume_server_pb.ReadAllNeedlesResponse
- 56, // 103: volume_server_pb.VolumeServer.VolumeTailSender:output_type -> volume_server_pb.VolumeTailSenderResponse
- 58, // 104: volume_server_pb.VolumeServer.VolumeTailReceiver:output_type -> volume_server_pb.VolumeTailReceiverResponse
- 60, // 105: volume_server_pb.VolumeServer.VolumeEcShardsGenerate:output_type -> volume_server_pb.VolumeEcShardsGenerateResponse
- 62, // 106: volume_server_pb.VolumeServer.VolumeEcShardsRebuild:output_type -> volume_server_pb.VolumeEcShardsRebuildResponse
- 64, // 107: volume_server_pb.VolumeServer.VolumeEcShardsCopy:output_type -> volume_server_pb.VolumeEcShardsCopyResponse
- 66, // 108: volume_server_pb.VolumeServer.VolumeEcShardsDelete:output_type -> volume_server_pb.VolumeEcShardsDeleteResponse
- 68, // 109: volume_server_pb.VolumeServer.VolumeEcShardsMount:output_type -> volume_server_pb.VolumeEcShardsMountResponse
- 70, // 110: volume_server_pb.VolumeServer.VolumeEcShardsUnmount:output_type -> volume_server_pb.VolumeEcShardsUnmountResponse
- 72, // 111: volume_server_pb.VolumeServer.VolumeEcShardRead:output_type -> volume_server_pb.VolumeEcShardReadResponse
- 74, // 112: volume_server_pb.VolumeServer.VolumeEcBlobDelete:output_type -> volume_server_pb.VolumeEcBlobDeleteResponse
- 76, // 113: volume_server_pb.VolumeServer.VolumeEcShardsToVolume:output_type -> volume_server_pb.VolumeEcShardsToVolumeResponse
- 78, // 114: volume_server_pb.VolumeServer.VolumeEcShardsInfo:output_type -> volume_server_pb.VolumeEcShardsInfoResponse
- 89, // 115: volume_server_pb.VolumeServer.VolumeTierMoveDatToRemote:output_type -> volume_server_pb.VolumeTierMoveDatToRemoteResponse
- 91, // 116: volume_server_pb.VolumeServer.VolumeTierMoveDatFromRemote:output_type -> volume_server_pb.VolumeTierMoveDatFromRemoteResponse
- 93, // 117: volume_server_pb.VolumeServer.VolumeServerStatus:output_type -> volume_server_pb.VolumeServerStatusResponse
- 95, // 118: volume_server_pb.VolumeServer.VolumeServerLeave:output_type -> volume_server_pb.VolumeServerLeaveResponse
- 97, // 119: volume_server_pb.VolumeServer.FetchAndWriteNeedle:output_type -> volume_server_pb.FetchAndWriteNeedleResponse
- 99, // 120: volume_server_pb.VolumeServer.ScrubVolume:output_type -> volume_server_pb.ScrubVolumeResponse
- 101, // 121: volume_server_pb.VolumeServer.ScrubEcVolume:output_type -> volume_server_pb.ScrubEcVolumeResponse
- 103, // 122: volume_server_pb.VolumeServer.Query:output_type -> volume_server_pb.QueriedStripe
- 105, // 123: volume_server_pb.VolumeServer.VolumeNeedleStatus:output_type -> volume_server_pb.VolumeNeedleStatusResponse
- 107, // 124: volume_server_pb.VolumeServer.Ping:output_type -> volume_server_pb.PingResponse
- 109, // 125: volume_server_pb.VolumeServer.AllocateBlockVolume:output_type -> volume_server_pb.AllocateBlockVolumeResponse
- 111, // 126: volume_server_pb.VolumeServer.VolumeServerDeleteBlockVolume:output_type -> volume_server_pb.VolumeServerDeleteBlockVolumeResponse
- 77, // [77:127] is the sub-list for method output_type
- 27, // [27:77] is the sub-list for method input_type
- 27, // [27:27] is the sub-list for extension type_name
- 27, // [27:27] is the sub-list for extension extendee
- 0, // [0:27] is the sub-list for field type_name
+ 122, // 19: volume_server_pb.QueryRequest.filter:type_name -> volume_server_pb.QueryRequest.Filter
+ 123, // 20: volume_server_pb.QueryRequest.input_serialization:type_name -> volume_server_pb.QueryRequest.InputSerialization
+ 124, // 21: volume_server_pb.QueryRequest.output_serialization:type_name -> volume_server_pb.QueryRequest.OutputSerialization
+ 118, // 22: volume_server_pb.ListBlockSnapshotsResponse.snapshots:type_name -> volume_server_pb.BlockSnapshotInfo
+ 125, // 23: volume_server_pb.QueryRequest.InputSerialization.csv_input:type_name -> volume_server_pb.QueryRequest.InputSerialization.CSVInput
+ 126, // 24: volume_server_pb.QueryRequest.InputSerialization.json_input:type_name -> volume_server_pb.QueryRequest.InputSerialization.JSONInput
+ 127, // 25: volume_server_pb.QueryRequest.InputSerialization.parquet_input:type_name -> volume_server_pb.QueryRequest.InputSerialization.ParquetInput
+ 128, // 26: volume_server_pb.QueryRequest.OutputSerialization.csv_output:type_name -> volume_server_pb.QueryRequest.OutputSerialization.CSVOutput
+ 129, // 27: volume_server_pb.QueryRequest.OutputSerialization.json_output:type_name -> volume_server_pb.QueryRequest.OutputSerialization.JSONOutput
+ 2, // 28: volume_server_pb.VolumeServer.BatchDelete:input_type -> volume_server_pb.BatchDeleteRequest
+ 6, // 29: volume_server_pb.VolumeServer.VacuumVolumeCheck:input_type -> volume_server_pb.VacuumVolumeCheckRequest
+ 8, // 30: volume_server_pb.VolumeServer.VacuumVolumeCompact:input_type -> volume_server_pb.VacuumVolumeCompactRequest
+ 10, // 31: volume_server_pb.VolumeServer.VacuumVolumeCommit:input_type -> volume_server_pb.VacuumVolumeCommitRequest
+ 12, // 32: volume_server_pb.VolumeServer.VacuumVolumeCleanup:input_type -> volume_server_pb.VacuumVolumeCleanupRequest
+ 14, // 33: volume_server_pb.VolumeServer.DeleteCollection:input_type -> volume_server_pb.DeleteCollectionRequest
+ 16, // 34: volume_server_pb.VolumeServer.AllocateVolume:input_type -> volume_server_pb.AllocateVolumeRequest
+ 18, // 35: volume_server_pb.VolumeServer.VolumeSyncStatus:input_type -> volume_server_pb.VolumeSyncStatusRequest
+ 20, // 36: volume_server_pb.VolumeServer.VolumeIncrementalCopy:input_type -> volume_server_pb.VolumeIncrementalCopyRequest
+ 22, // 37: volume_server_pb.VolumeServer.VolumeMount:input_type -> volume_server_pb.VolumeMountRequest
+ 24, // 38: volume_server_pb.VolumeServer.VolumeUnmount:input_type -> volume_server_pb.VolumeUnmountRequest
+ 26, // 39: volume_server_pb.VolumeServer.VolumeDelete:input_type -> volume_server_pb.VolumeDeleteRequest
+ 28, // 40: volume_server_pb.VolumeServer.VolumeMarkReadonly:input_type -> volume_server_pb.VolumeMarkReadonlyRequest
+ 30, // 41: volume_server_pb.VolumeServer.VolumeMarkWritable:input_type -> volume_server_pb.VolumeMarkWritableRequest
+ 32, // 42: volume_server_pb.VolumeServer.VolumeConfigure:input_type -> volume_server_pb.VolumeConfigureRequest
+ 34, // 43: volume_server_pb.VolumeServer.VolumeStatus:input_type -> volume_server_pb.VolumeStatusRequest
+ 36, // 44: volume_server_pb.VolumeServer.GetState:input_type -> volume_server_pb.GetStateRequest
+ 38, // 45: volume_server_pb.VolumeServer.SetState:input_type -> volume_server_pb.SetStateRequest
+ 40, // 46: volume_server_pb.VolumeServer.VolumeCopy:input_type -> volume_server_pb.VolumeCopyRequest
+ 80, // 47: volume_server_pb.VolumeServer.ReadVolumeFileStatus:input_type -> volume_server_pb.ReadVolumeFileStatusRequest
+ 42, // 48: volume_server_pb.VolumeServer.CopyFile:input_type -> volume_server_pb.CopyFileRequest
+ 44, // 49: volume_server_pb.VolumeServer.ReceiveFile:input_type -> volume_server_pb.ReceiveFileRequest
+ 47, // 50: volume_server_pb.VolumeServer.ReadNeedleBlob:input_type -> volume_server_pb.ReadNeedleBlobRequest
+ 49, // 51: volume_server_pb.VolumeServer.ReadNeedleMeta:input_type -> volume_server_pb.ReadNeedleMetaRequest
+ 51, // 52: volume_server_pb.VolumeServer.WriteNeedleBlob:input_type -> volume_server_pb.WriteNeedleBlobRequest
+ 53, // 53: volume_server_pb.VolumeServer.ReadAllNeedles:input_type -> volume_server_pb.ReadAllNeedlesRequest
+ 55, // 54: volume_server_pb.VolumeServer.VolumeTailSender:input_type -> volume_server_pb.VolumeTailSenderRequest
+ 57, // 55: volume_server_pb.VolumeServer.VolumeTailReceiver:input_type -> volume_server_pb.VolumeTailReceiverRequest
+ 59, // 56: volume_server_pb.VolumeServer.VolumeEcShardsGenerate:input_type -> volume_server_pb.VolumeEcShardsGenerateRequest
+ 61, // 57: volume_server_pb.VolumeServer.VolumeEcShardsRebuild:input_type -> volume_server_pb.VolumeEcShardsRebuildRequest
+ 63, // 58: volume_server_pb.VolumeServer.VolumeEcShardsCopy:input_type -> volume_server_pb.VolumeEcShardsCopyRequest
+ 65, // 59: volume_server_pb.VolumeServer.VolumeEcShardsDelete:input_type -> volume_server_pb.VolumeEcShardsDeleteRequest
+ 67, // 60: volume_server_pb.VolumeServer.VolumeEcShardsMount:input_type -> volume_server_pb.VolumeEcShardsMountRequest
+ 69, // 61: volume_server_pb.VolumeServer.VolumeEcShardsUnmount:input_type -> volume_server_pb.VolumeEcShardsUnmountRequest
+ 71, // 62: volume_server_pb.VolumeServer.VolumeEcShardRead:input_type -> volume_server_pb.VolumeEcShardReadRequest
+ 73, // 63: volume_server_pb.VolumeServer.VolumeEcBlobDelete:input_type -> volume_server_pb.VolumeEcBlobDeleteRequest
+ 75, // 64: volume_server_pb.VolumeServer.VolumeEcShardsToVolume:input_type -> volume_server_pb.VolumeEcShardsToVolumeRequest
+ 77, // 65: volume_server_pb.VolumeServer.VolumeEcShardsInfo:input_type -> volume_server_pb.VolumeEcShardsInfoRequest
+ 88, // 66: volume_server_pb.VolumeServer.VolumeTierMoveDatToRemote:input_type -> volume_server_pb.VolumeTierMoveDatToRemoteRequest
+ 90, // 67: volume_server_pb.VolumeServer.VolumeTierMoveDatFromRemote:input_type -> volume_server_pb.VolumeTierMoveDatFromRemoteRequest
+ 92, // 68: volume_server_pb.VolumeServer.VolumeServerStatus:input_type -> volume_server_pb.VolumeServerStatusRequest
+ 94, // 69: volume_server_pb.VolumeServer.VolumeServerLeave:input_type -> volume_server_pb.VolumeServerLeaveRequest
+ 96, // 70: volume_server_pb.VolumeServer.FetchAndWriteNeedle:input_type -> volume_server_pb.FetchAndWriteNeedleRequest
+ 98, // 71: volume_server_pb.VolumeServer.ScrubVolume:input_type -> volume_server_pb.ScrubVolumeRequest
+ 100, // 72: volume_server_pb.VolumeServer.ScrubEcVolume:input_type -> volume_server_pb.ScrubEcVolumeRequest
+ 102, // 73: volume_server_pb.VolumeServer.Query:input_type -> volume_server_pb.QueryRequest
+ 104, // 74: volume_server_pb.VolumeServer.VolumeNeedleStatus:input_type -> volume_server_pb.VolumeNeedleStatusRequest
+ 106, // 75: volume_server_pb.VolumeServer.Ping:input_type -> volume_server_pb.PingRequest
+ 108, // 76: volume_server_pb.VolumeServer.AllocateBlockVolume:input_type -> volume_server_pb.AllocateBlockVolumeRequest
+ 110, // 77: volume_server_pb.VolumeServer.VolumeServerDeleteBlockVolume:input_type -> volume_server_pb.VolumeServerDeleteBlockVolumeRequest
+ 112, // 78: volume_server_pb.VolumeServer.SnapshotBlockVolume:input_type -> volume_server_pb.SnapshotBlockVolumeRequest
+ 114, // 79: volume_server_pb.VolumeServer.DeleteBlockSnapshot:input_type -> volume_server_pb.DeleteBlockSnapshotRequest
+ 116, // 80: volume_server_pb.VolumeServer.ListBlockSnapshots:input_type -> volume_server_pb.ListBlockSnapshotsRequest
+ 119, // 81: volume_server_pb.VolumeServer.ExpandBlockVolume:input_type -> volume_server_pb.ExpandBlockVolumeRequest
+ 3, // 82: volume_server_pb.VolumeServer.BatchDelete:output_type -> volume_server_pb.BatchDeleteResponse
+ 7, // 83: volume_server_pb.VolumeServer.VacuumVolumeCheck:output_type -> volume_server_pb.VacuumVolumeCheckResponse
+ 9, // 84: volume_server_pb.VolumeServer.VacuumVolumeCompact:output_type -> volume_server_pb.VacuumVolumeCompactResponse
+ 11, // 85: volume_server_pb.VolumeServer.VacuumVolumeCommit:output_type -> volume_server_pb.VacuumVolumeCommitResponse
+ 13, // 86: volume_server_pb.VolumeServer.VacuumVolumeCleanup:output_type -> volume_server_pb.VacuumVolumeCleanupResponse
+ 15, // 87: volume_server_pb.VolumeServer.DeleteCollection:output_type -> volume_server_pb.DeleteCollectionResponse
+ 17, // 88: volume_server_pb.VolumeServer.AllocateVolume:output_type -> volume_server_pb.AllocateVolumeResponse
+ 19, // 89: volume_server_pb.VolumeServer.VolumeSyncStatus:output_type -> volume_server_pb.VolumeSyncStatusResponse
+ 21, // 90: volume_server_pb.VolumeServer.VolumeIncrementalCopy:output_type -> volume_server_pb.VolumeIncrementalCopyResponse
+ 23, // 91: volume_server_pb.VolumeServer.VolumeMount:output_type -> volume_server_pb.VolumeMountResponse
+ 25, // 92: volume_server_pb.VolumeServer.VolumeUnmount:output_type -> volume_server_pb.VolumeUnmountResponse
+ 27, // 93: volume_server_pb.VolumeServer.VolumeDelete:output_type -> volume_server_pb.VolumeDeleteResponse
+ 29, // 94: volume_server_pb.VolumeServer.VolumeMarkReadonly:output_type -> volume_server_pb.VolumeMarkReadonlyResponse
+ 31, // 95: volume_server_pb.VolumeServer.VolumeMarkWritable:output_type -> volume_server_pb.VolumeMarkWritableResponse
+ 33, // 96: volume_server_pb.VolumeServer.VolumeConfigure:output_type -> volume_server_pb.VolumeConfigureResponse
+ 35, // 97: volume_server_pb.VolumeServer.VolumeStatus:output_type -> volume_server_pb.VolumeStatusResponse
+ 37, // 98: volume_server_pb.VolumeServer.GetState:output_type -> volume_server_pb.GetStateResponse
+ 39, // 99: volume_server_pb.VolumeServer.SetState:output_type -> volume_server_pb.SetStateResponse
+ 41, // 100: volume_server_pb.VolumeServer.VolumeCopy:output_type -> volume_server_pb.VolumeCopyResponse
+ 81, // 101: volume_server_pb.VolumeServer.ReadVolumeFileStatus:output_type -> volume_server_pb.ReadVolumeFileStatusResponse
+ 43, // 102: volume_server_pb.VolumeServer.CopyFile:output_type -> volume_server_pb.CopyFileResponse
+ 46, // 103: volume_server_pb.VolumeServer.ReceiveFile:output_type -> volume_server_pb.ReceiveFileResponse
+ 48, // 104: volume_server_pb.VolumeServer.ReadNeedleBlob:output_type -> volume_server_pb.ReadNeedleBlobResponse
+ 50, // 105: volume_server_pb.VolumeServer.ReadNeedleMeta:output_type -> volume_server_pb.ReadNeedleMetaResponse
+ 52, // 106: volume_server_pb.VolumeServer.WriteNeedleBlob:output_type -> volume_server_pb.WriteNeedleBlobResponse
+ 54, // 107: volume_server_pb.VolumeServer.ReadAllNeedles:output_type -> volume_server_pb.ReadAllNeedlesResponse
+ 56, // 108: volume_server_pb.VolumeServer.VolumeTailSender:output_type -> volume_server_pb.VolumeTailSenderResponse
+ 58, // 109: volume_server_pb.VolumeServer.VolumeTailReceiver:output_type -> volume_server_pb.VolumeTailReceiverResponse
+ 60, // 110: volume_server_pb.VolumeServer.VolumeEcShardsGenerate:output_type -> volume_server_pb.VolumeEcShardsGenerateResponse
+ 62, // 111: volume_server_pb.VolumeServer.VolumeEcShardsRebuild:output_type -> volume_server_pb.VolumeEcShardsRebuildResponse
+ 64, // 112: volume_server_pb.VolumeServer.VolumeEcShardsCopy:output_type -> volume_server_pb.VolumeEcShardsCopyResponse
+ 66, // 113: volume_server_pb.VolumeServer.VolumeEcShardsDelete:output_type -> volume_server_pb.VolumeEcShardsDeleteResponse
+ 68, // 114: volume_server_pb.VolumeServer.VolumeEcShardsMount:output_type -> volume_server_pb.VolumeEcShardsMountResponse
+ 70, // 115: volume_server_pb.VolumeServer.VolumeEcShardsUnmount:output_type -> volume_server_pb.VolumeEcShardsUnmountResponse
+ 72, // 116: volume_server_pb.VolumeServer.VolumeEcShardRead:output_type -> volume_server_pb.VolumeEcShardReadResponse
+ 74, // 117: volume_server_pb.VolumeServer.VolumeEcBlobDelete:output_type -> volume_server_pb.VolumeEcBlobDeleteResponse
+ 76, // 118: volume_server_pb.VolumeServer.VolumeEcShardsToVolume:output_type -> volume_server_pb.VolumeEcShardsToVolumeResponse
+ 78, // 119: volume_server_pb.VolumeServer.VolumeEcShardsInfo:output_type -> volume_server_pb.VolumeEcShardsInfoResponse
+ 89, // 120: volume_server_pb.VolumeServer.VolumeTierMoveDatToRemote:output_type -> volume_server_pb.VolumeTierMoveDatToRemoteResponse
+ 91, // 121: volume_server_pb.VolumeServer.VolumeTierMoveDatFromRemote:output_type -> volume_server_pb.VolumeTierMoveDatFromRemoteResponse
+ 93, // 122: volume_server_pb.VolumeServer.VolumeServerStatus:output_type -> volume_server_pb.VolumeServerStatusResponse
+ 95, // 123: volume_server_pb.VolumeServer.VolumeServerLeave:output_type -> volume_server_pb.VolumeServerLeaveResponse
+ 97, // 124: volume_server_pb.VolumeServer.FetchAndWriteNeedle:output_type -> volume_server_pb.FetchAndWriteNeedleResponse
+ 99, // 125: volume_server_pb.VolumeServer.ScrubVolume:output_type -> volume_server_pb.ScrubVolumeResponse
+ 101, // 126: volume_server_pb.VolumeServer.ScrubEcVolume:output_type -> volume_server_pb.ScrubEcVolumeResponse
+ 103, // 127: volume_server_pb.VolumeServer.Query:output_type -> volume_server_pb.QueriedStripe
+ 105, // 128: volume_server_pb.VolumeServer.VolumeNeedleStatus:output_type -> volume_server_pb.VolumeNeedleStatusResponse
+ 107, // 129: volume_server_pb.VolumeServer.Ping:output_type -> volume_server_pb.PingResponse
+ 109, // 130: volume_server_pb.VolumeServer.AllocateBlockVolume:output_type -> volume_server_pb.AllocateBlockVolumeResponse
+ 111, // 131: volume_server_pb.VolumeServer.VolumeServerDeleteBlockVolume:output_type -> volume_server_pb.VolumeServerDeleteBlockVolumeResponse
+ 113, // 132: volume_server_pb.VolumeServer.SnapshotBlockVolume:output_type -> volume_server_pb.SnapshotBlockVolumeResponse
+ 115, // 133: volume_server_pb.VolumeServer.DeleteBlockSnapshot:output_type -> volume_server_pb.DeleteBlockSnapshotResponse
+ 117, // 134: volume_server_pb.VolumeServer.ListBlockSnapshots:output_type -> volume_server_pb.ListBlockSnapshotsResponse
+ 120, // 135: volume_server_pb.VolumeServer.ExpandBlockVolume:output_type -> volume_server_pb.ExpandBlockVolumeResponse
+ 82, // [82:136] is the sub-list for method output_type
+ 28, // [28:82] is the sub-list for method input_type
+ 28, // [28:28] is the sub-list for extension type_name
+ 28, // [28:28] is the sub-list for extension extendee
+ 0, // [0:28] is the sub-list for field type_name
}
func init() { file_volume_server_proto_init() }
@@ -7807,7 +8307,7 @@ func file_volume_server_proto_init() {
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_volume_server_proto_rawDesc), len(file_volume_server_proto_rawDesc)),
NumEnums: 1,
- NumMessages: 120,
+ NumMessages: 129,
NumExtensions: 0,
NumServices: 1,
},
diff --git a/weed/pb/volume_server_pb/volume_server_grpc.pb.go b/weed/pb/volume_server_pb/volume_server_grpc.pb.go
index d4bb488021..1f76c2b285 100644
--- a/weed/pb/volume_server_pb/volume_server_grpc.pb.go
+++ b/weed/pb/volume_server_pb/volume_server_grpc.pb.go
@@ -69,6 +69,10 @@ const (
VolumeServer_Ping_FullMethodName = "/volume_server_pb.VolumeServer/Ping"
VolumeServer_AllocateBlockVolume_FullMethodName = "/volume_server_pb.VolumeServer/AllocateBlockVolume"
VolumeServer_VolumeServerDeleteBlockVolume_FullMethodName = "/volume_server_pb.VolumeServer/VolumeServerDeleteBlockVolume"
+ VolumeServer_SnapshotBlockVolume_FullMethodName = "/volume_server_pb.VolumeServer/SnapshotBlockVolume"
+ VolumeServer_DeleteBlockSnapshot_FullMethodName = "/volume_server_pb.VolumeServer/DeleteBlockSnapshot"
+ VolumeServer_ListBlockSnapshots_FullMethodName = "/volume_server_pb.VolumeServer/ListBlockSnapshots"
+ VolumeServer_ExpandBlockVolume_FullMethodName = "/volume_server_pb.VolumeServer/ExpandBlockVolume"
)
// VolumeServerClient is the client API for VolumeServer service.
@@ -133,6 +137,10 @@ type VolumeServerClient interface {
// block volumes
AllocateBlockVolume(ctx context.Context, in *AllocateBlockVolumeRequest, opts ...grpc.CallOption) (*AllocateBlockVolumeResponse, error)
VolumeServerDeleteBlockVolume(ctx context.Context, in *VolumeServerDeleteBlockVolumeRequest, opts ...grpc.CallOption) (*VolumeServerDeleteBlockVolumeResponse, error)
+ SnapshotBlockVolume(ctx context.Context, in *SnapshotBlockVolumeRequest, opts ...grpc.CallOption) (*SnapshotBlockVolumeResponse, error)
+ DeleteBlockSnapshot(ctx context.Context, in *DeleteBlockSnapshotRequest, opts ...grpc.CallOption) (*DeleteBlockSnapshotResponse, error)
+ ListBlockSnapshots(ctx context.Context, in *ListBlockSnapshotsRequest, opts ...grpc.CallOption) (*ListBlockSnapshotsResponse, error)
+ ExpandBlockVolume(ctx context.Context, in *ExpandBlockVolumeRequest, opts ...grpc.CallOption) (*ExpandBlockVolumeResponse, error)
}
type volumeServerClient struct {
@@ -736,6 +744,46 @@ func (c *volumeServerClient) VolumeServerDeleteBlockVolume(ctx context.Context,
return out, nil
}
+func (c *volumeServerClient) SnapshotBlockVolume(ctx context.Context, in *SnapshotBlockVolumeRequest, opts ...grpc.CallOption) (*SnapshotBlockVolumeResponse, error) {
+ cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
+ out := new(SnapshotBlockVolumeResponse)
+ err := c.cc.Invoke(ctx, VolumeServer_SnapshotBlockVolume_FullMethodName, in, out, cOpts...)
+ if err != nil {
+ return nil, err
+ }
+ return out, nil
+}
+
+func (c *volumeServerClient) DeleteBlockSnapshot(ctx context.Context, in *DeleteBlockSnapshotRequest, opts ...grpc.CallOption) (*DeleteBlockSnapshotResponse, error) {
+ cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
+ out := new(DeleteBlockSnapshotResponse)
+ err := c.cc.Invoke(ctx, VolumeServer_DeleteBlockSnapshot_FullMethodName, in, out, cOpts...)
+ if err != nil {
+ return nil, err
+ }
+ return out, nil
+}
+
+func (c *volumeServerClient) ListBlockSnapshots(ctx context.Context, in *ListBlockSnapshotsRequest, opts ...grpc.CallOption) (*ListBlockSnapshotsResponse, error) {
+ cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
+ out := new(ListBlockSnapshotsResponse)
+ err := c.cc.Invoke(ctx, VolumeServer_ListBlockSnapshots_FullMethodName, in, out, cOpts...)
+ if err != nil {
+ return nil, err
+ }
+ return out, nil
+}
+
+func (c *volumeServerClient) ExpandBlockVolume(ctx context.Context, in *ExpandBlockVolumeRequest, opts ...grpc.CallOption) (*ExpandBlockVolumeResponse, error) {
+ cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
+ out := new(ExpandBlockVolumeResponse)
+ err := c.cc.Invoke(ctx, VolumeServer_ExpandBlockVolume_FullMethodName, in, out, cOpts...)
+ if err != nil {
+ return nil, err
+ }
+ return out, nil
+}
+
// VolumeServerServer is the server API for VolumeServer service.
// All implementations must embed UnimplementedVolumeServerServer
// for forward compatibility.
@@ -798,6 +846,10 @@ type VolumeServerServer interface {
// block volumes
AllocateBlockVolume(context.Context, *AllocateBlockVolumeRequest) (*AllocateBlockVolumeResponse, error)
VolumeServerDeleteBlockVolume(context.Context, *VolumeServerDeleteBlockVolumeRequest) (*VolumeServerDeleteBlockVolumeResponse, error)
+ SnapshotBlockVolume(context.Context, *SnapshotBlockVolumeRequest) (*SnapshotBlockVolumeResponse, error)
+ DeleteBlockSnapshot(context.Context, *DeleteBlockSnapshotRequest) (*DeleteBlockSnapshotResponse, error)
+ ListBlockSnapshots(context.Context, *ListBlockSnapshotsRequest) (*ListBlockSnapshotsResponse, error)
+ ExpandBlockVolume(context.Context, *ExpandBlockVolumeRequest) (*ExpandBlockVolumeResponse, error)
mustEmbedUnimplementedVolumeServerServer()
}
@@ -958,6 +1010,18 @@ func (UnimplementedVolumeServerServer) AllocateBlockVolume(context.Context, *All
func (UnimplementedVolumeServerServer) VolumeServerDeleteBlockVolume(context.Context, *VolumeServerDeleteBlockVolumeRequest) (*VolumeServerDeleteBlockVolumeResponse, error) {
return nil, status.Error(codes.Unimplemented, "method VolumeServerDeleteBlockVolume not implemented")
}
+func (UnimplementedVolumeServerServer) SnapshotBlockVolume(context.Context, *SnapshotBlockVolumeRequest) (*SnapshotBlockVolumeResponse, error) {
+ return nil, status.Error(codes.Unimplemented, "method SnapshotBlockVolume not implemented")
+}
+func (UnimplementedVolumeServerServer) DeleteBlockSnapshot(context.Context, *DeleteBlockSnapshotRequest) (*DeleteBlockSnapshotResponse, error) {
+ return nil, status.Error(codes.Unimplemented, "method DeleteBlockSnapshot not implemented")
+}
+func (UnimplementedVolumeServerServer) ListBlockSnapshots(context.Context, *ListBlockSnapshotsRequest) (*ListBlockSnapshotsResponse, error) {
+ return nil, status.Error(codes.Unimplemented, "method ListBlockSnapshots not implemented")
+}
+func (UnimplementedVolumeServerServer) ExpandBlockVolume(context.Context, *ExpandBlockVolumeRequest) (*ExpandBlockVolumeResponse, error) {
+ return nil, status.Error(codes.Unimplemented, "method ExpandBlockVolume not implemented")
+}
func (UnimplementedVolumeServerServer) mustEmbedUnimplementedVolumeServerServer() {}
func (UnimplementedVolumeServerServer) testEmbeddedByValue() {}
@@ -1798,6 +1862,78 @@ func _VolumeServer_VolumeServerDeleteBlockVolume_Handler(srv interface{}, ctx co
return interceptor(ctx, in, info, handler)
}
+func _VolumeServer_SnapshotBlockVolume_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+ in := new(SnapshotBlockVolumeRequest)
+ if err := dec(in); err != nil {
+ return nil, err
+ }
+ if interceptor == nil {
+ return srv.(VolumeServerServer).SnapshotBlockVolume(ctx, in)
+ }
+ info := &grpc.UnaryServerInfo{
+ Server: srv,
+ FullMethod: VolumeServer_SnapshotBlockVolume_FullMethodName,
+ }
+ handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+ return srv.(VolumeServerServer).SnapshotBlockVolume(ctx, req.(*SnapshotBlockVolumeRequest))
+ }
+ return interceptor(ctx, in, info, handler)
+}
+
+func _VolumeServer_DeleteBlockSnapshot_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+ in := new(DeleteBlockSnapshotRequest)
+ if err := dec(in); err != nil {
+ return nil, err
+ }
+ if interceptor == nil {
+ return srv.(VolumeServerServer).DeleteBlockSnapshot(ctx, in)
+ }
+ info := &grpc.UnaryServerInfo{
+ Server: srv,
+ FullMethod: VolumeServer_DeleteBlockSnapshot_FullMethodName,
+ }
+ handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+ return srv.(VolumeServerServer).DeleteBlockSnapshot(ctx, req.(*DeleteBlockSnapshotRequest))
+ }
+ return interceptor(ctx, in, info, handler)
+}
+
+func _VolumeServer_ListBlockSnapshots_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+ in := new(ListBlockSnapshotsRequest)
+ if err := dec(in); err != nil {
+ return nil, err
+ }
+ if interceptor == nil {
+ return srv.(VolumeServerServer).ListBlockSnapshots(ctx, in)
+ }
+ info := &grpc.UnaryServerInfo{
+ Server: srv,
+ FullMethod: VolumeServer_ListBlockSnapshots_FullMethodName,
+ }
+ handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+ return srv.(VolumeServerServer).ListBlockSnapshots(ctx, req.(*ListBlockSnapshotsRequest))
+ }
+ return interceptor(ctx, in, info, handler)
+}
+
+func _VolumeServer_ExpandBlockVolume_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+ in := new(ExpandBlockVolumeRequest)
+ if err := dec(in); err != nil {
+ return nil, err
+ }
+ if interceptor == nil {
+ return srv.(VolumeServerServer).ExpandBlockVolume(ctx, in)
+ }
+ info := &grpc.UnaryServerInfo{
+ Server: srv,
+ FullMethod: VolumeServer_ExpandBlockVolume_FullMethodName,
+ }
+ handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+ return srv.(VolumeServerServer).ExpandBlockVolume(ctx, req.(*ExpandBlockVolumeRequest))
+ }
+ return interceptor(ctx, in, info, handler)
+}
+
// VolumeServer_ServiceDesc is the grpc.ServiceDesc for VolumeServer service.
// It's only intended for direct use with grpc.RegisterService,
// and not to be introspected or modified (even as a copy)
@@ -1961,6 +2097,22 @@ var VolumeServer_ServiceDesc = grpc.ServiceDesc{
MethodName: "VolumeServerDeleteBlockVolume",
Handler: _VolumeServer_VolumeServerDeleteBlockVolume_Handler,
},
+ {
+ MethodName: "SnapshotBlockVolume",
+ Handler: _VolumeServer_SnapshotBlockVolume_Handler,
+ },
+ {
+ MethodName: "DeleteBlockSnapshot",
+ Handler: _VolumeServer_DeleteBlockSnapshot_Handler,
+ },
+ {
+ MethodName: "ListBlockSnapshots",
+ Handler: _VolumeServer_ListBlockSnapshots_Handler,
+ },
+ {
+ MethodName: "ExpandBlockVolume",
+ Handler: _VolumeServer_ExpandBlockVolume_Handler,
+ },
},
Streams: []grpc.StreamDesc{
{
diff --git a/weed/server/integration_block_test.go b/weed/server/integration_block_test.go
index 5b1a970a51..d3a590e85d 100644
--- a/weed/server/integration_block_test.go
+++ b/weed/server/integration_block_test.go
@@ -641,7 +641,22 @@ func TestIntegration_DoubleFailover(t *testing.T) {
t.Fatalf("first failover epoch: got %d, want 2", e1.Epoch)
}
- // Second failover: vs2 dies → vs1 promoted (it's now the replica).
+ // CP8-2: PromoteBestReplica does NOT add old primary back as replica.
+ // Reconnect vs1 first so it becomes a replica (via recoverBlockVolumes).
+ ms.recoverBlockVolumes(vs1)
+
+ // Simulate heartbeat from vs1 that restores iSCSI addr and health score
+ // (in production this happens when the VS re-registers after reconnect).
+ e1, _ = ms.blockRegistry.Lookup("pvc-double-1")
+ for i := range e1.Replicas {
+ if e1.Replicas[i].Server == vs1 {
+ e1.Replicas[i].ISCSIAddr = vs1 + ":3260"
+ e1.Replicas[i].HealthScore = 1.0
+ }
+ }
+
+ // Now vs1 is back as replica. Second failover: vs2 dies → vs1 promoted.
+ e1, _ = ms.blockRegistry.Lookup("pvc-double-1")
e1.LastLeaseGrant = time.Now().Add(-1 * time.Minute)
ms.failoverBlockVolumes(vs2)
@@ -724,9 +739,10 @@ func TestIntegration_MultiVolumeFailoverRebuild(t *testing.T) {
}
}
_ = otherServer
- // rebuildCount should equal the number of volumes that were primary on deadServer.
- if rebuildCount != primaryCounts[deadServer] {
- t.Fatalf("expected %d rebuild assignments for %s, got %d",
- primaryCounts[deadServer], deadServer, rebuildCount)
+ // CP8-2: With 2 servers and 3 volumes, deadServer hosts all 3 volumes
+ // (as primary for some, replica for others). All need rebuild on reconnect.
+ if rebuildCount != 3 {
+ t.Fatalf("expected 3 rebuild assignments for %s (all volumes), got %d",
+ deadServer, rebuildCount)
}
}
diff --git a/weed/server/master_block_assignment_queue.go b/weed/server/master_block_assignment_queue.go
index 4b7e8fb958..d55e83cdfa 100644
--- a/weed/server/master_block_assignment_queue.go
+++ b/weed/server/master_block_assignment_queue.go
@@ -123,3 +123,14 @@ func (q *BlockAssignmentQueue) Pending(server string) int {
defer q.mu.Unlock()
return len(q.queues[server])
}
+
+// TotalPending returns the total number of pending assignments across all servers.
+func (q *BlockAssignmentQueue) TotalPending() int {
+ q.mu.Lock()
+ defer q.mu.Unlock()
+ total := 0
+ for _, queue := range q.queues {
+ total += len(queue)
+ }
+ return total
+}
diff --git a/weed/server/master_block_failover.go b/weed/server/master_block_failover.go
index 5e939ec9ce..09649af62d 100644
--- a/weed/server/master_block_failover.go
+++ b/weed/server/master_block_failover.go
@@ -32,82 +32,116 @@ func newBlockFailoverState() *blockFailoverState {
}
// failoverBlockVolumes is called when a volume server disconnects.
-// It checks each block volume on that server and promotes the replica
-// if the lease has expired (F2).
+// It checks each block volume on that server and:
+// - If dead server is primary: promote best replica (if lease expired).
+// - If dead server hosts a replica: remove from replica list, record pending rebuild.
func (ms *MasterServer) failoverBlockVolumes(deadServer string) {
if ms.blockRegistry == nil {
return
}
+ ms.blockRegistry.FailoversTotal.Add(1)
entries := ms.blockRegistry.ListByServer(deadServer)
now := time.Now()
for _, entry := range entries {
- if blockvol.RoleFromWire(entry.Role) != blockvol.RolePrimary {
+ // Case 1: Dead server is the primary.
+ if entry.VolumeServer == deadServer &&
+ blockvol.RoleFromWire(entry.Role) == blockvol.RolePrimary {
+
+ if !entry.HasReplica() {
+ glog.Warningf("failover: %q has no replica, cannot promote", entry.Name)
+ continue
+ }
+ // F2: Wait for lease expiry before promoting.
+ leaseExpiry := entry.LastLeaseGrant.Add(entry.LeaseTTL)
+ if now.Before(leaseExpiry) {
+ delay := leaseExpiry.Sub(now)
+ glog.V(0).Infof("failover: %q lease expires in %v, deferring promotion", entry.Name, delay)
+ volumeName := entry.Name
+ timer := time.AfterFunc(delay, func() {
+ ms.promoteReplica(volumeName)
+ })
+ ms.blockFailover.mu.Lock()
+ ms.blockFailover.deferredTimers[deadServer] = append(
+ ms.blockFailover.deferredTimers[deadServer], timer)
+ ms.blockFailover.mu.Unlock()
+ continue
+ }
+ // Lease already expired — promote immediately.
+ ms.promoteReplica(entry.Name)
continue
}
- // Only failover volumes whose primary is the dead server.
+
+ // Case 2: Dead server hosts a replica (not the primary).
if entry.VolumeServer != deadServer {
- continue
+ ri := entry.ReplicaByServer(deadServer)
+ if ri != nil {
+ replicaPath := ri.Path
+ // Remove dead replica from registry.
+ if err := ms.blockRegistry.RemoveReplica(entry.Name, deadServer); err != nil {
+ glog.Warningf("failover: RemoveReplica %q on %s: %v", entry.Name, deadServer, err)
+ continue
+ }
+ // Record pending rebuild for when dead server reconnects.
+ ms.recordPendingRebuild(deadServer, pendingRebuild{
+ VolumeName: entry.Name,
+ OldPath: replicaPath,
+ NewPrimary: entry.VolumeServer, // current primary (unchanged)
+ Epoch: entry.Epoch,
+ })
+ glog.V(0).Infof("failover: removed dead replica %s for %q, pending rebuild",
+ deadServer, entry.Name)
+ }
}
- if entry.ReplicaServer == "" {
- glog.Warningf("failover: %q has no replica, cannot promote", entry.Name)
- continue
- }
- // F2: Wait for lease expiry before promoting.
- leaseExpiry := entry.LastLeaseGrant.Add(entry.LeaseTTL)
- if now.Before(leaseExpiry) {
- delay := leaseExpiry.Sub(now)
- glog.V(0).Infof("failover: %q lease expires in %v, deferring promotion", entry.Name, delay)
- volumeName := entry.Name
- timer := time.AfterFunc(delay, func() {
- ms.promoteReplica(volumeName)
- })
- // R2-F2: Store timer so it can be cancelled if the server reconnects.
- ms.blockFailover.mu.Lock()
- ms.blockFailover.deferredTimers[deadServer] = append(
- ms.blockFailover.deferredTimers[deadServer], timer)
- ms.blockFailover.mu.Unlock()
- continue
- }
- // Lease already expired — promote immediately.
- ms.promoteReplica(entry.Name)
}
}
-// promoteReplica swaps primary and replica for the named volume,
+// promoteReplica promotes the best replica to primary for the named volume,
// enqueues an assignment for the new primary, and records a pending rebuild.
func (ms *MasterServer) promoteReplica(volumeName string) {
entry, ok := ms.blockRegistry.Lookup(volumeName)
if !ok {
return
}
- if entry.ReplicaServer == "" {
+ if !entry.HasReplica() {
return
}
oldPrimary := entry.VolumeServer
oldPath := entry.Path
- // R2-F5: Epoch computed atomically inside SwapPrimaryReplica (under lock).
- newEpoch, err := ms.blockRegistry.SwapPrimaryReplica(volumeName)
+ // CP8-2: Use PromoteBestReplica (picks by health score, tie-break by WALHeadLSN).
+ newEpoch, err := ms.blockRegistry.PromoteBestReplica(volumeName)
if err != nil {
- glog.Warningf("failover: SwapPrimaryReplica %q: %v", volumeName, err)
+ glog.Warningf("failover: PromoteBestReplica %q: %v", volumeName, err)
return
}
- // Re-read entry after swap.
+ // Re-read entry after promotion.
entry, ok = ms.blockRegistry.Lookup(volumeName)
if !ok {
return
}
- // Enqueue assignment for new primary.
+ // Build assignment for new primary. Include ReplicaAddrs for remaining replicas.
leaseTTLMs := blockvol.LeaseTTLToWire(30 * time.Second)
- ms.blockAssignmentQueue.Enqueue(entry.VolumeServer, blockvol.BlockVolumeAssignment{
+ assignment := blockvol.BlockVolumeAssignment{
Path: entry.Path,
Epoch: newEpoch,
Role: blockvol.RoleToWire(blockvol.RolePrimary),
LeaseTtlMs: leaseTTLMs,
- })
+ }
+ for _, ri := range entry.Replicas {
+ assignment.ReplicaAddrs = append(assignment.ReplicaAddrs, blockvol.ReplicaAddr{
+ DataAddr: ri.DataAddr,
+ CtrlAddr: ri.CtrlAddr,
+ })
+ }
+ // Backward compat: also set scalar fields if exactly 1 replica.
+ if len(entry.Replicas) == 1 {
+ assignment.ReplicaDataAddr = entry.Replicas[0].DataAddr
+ assignment.ReplicaCtrlAddr = entry.Replicas[0].CtrlAddr
+ }
+ ms.blockAssignmentQueue.Enqueue(entry.VolumeServer, assignment)
// Record pending rebuild for when dead server reconnects.
ms.recordPendingRebuild(oldPrimary, pendingRebuild{
@@ -117,6 +151,7 @@ func (ms *MasterServer) promoteReplica(volumeName string) {
Epoch: newEpoch,
})
+ ms.blockRegistry.PromotionsTotal.Add(1)
glog.V(0).Infof("failover: promoted replica for %q: new primary=%s epoch=%d (old primary=%s)",
volumeName, entry.VolumeServer, newEpoch, oldPrimary)
}
@@ -180,8 +215,11 @@ func (ms *MasterServer) recoverBlockVolumes(reconnectedServer string) {
continue
}
- // Update registry: reconnected server becomes the new replica.
- ms.blockRegistry.SetReplica(rb.VolumeName, reconnectedServer, rb.OldPath, "", "")
+ // Update registry: reconnected server becomes a replica (via AddReplica for RF≥2 support).
+ ms.blockRegistry.AddReplica(rb.VolumeName, ReplicaInfo{
+ Server: reconnectedServer,
+ Path: rb.OldPath,
+ })
// Enqueue rebuild assignment for the reconnected server.
ms.blockAssignmentQueue.Enqueue(reconnectedServer, blockvol.BlockVolumeAssignment{
@@ -191,6 +229,7 @@ func (ms *MasterServer) recoverBlockVolumes(reconnectedServer string) {
RebuildAddr: entry.RebuildListenAddr,
})
+ ms.blockRegistry.RebuildsTotal.Add(1)
glog.V(0).Infof("rebuild: enqueued rebuild for %q on %s (epoch=%d, rebuildAddr=%s)",
rb.VolumeName, reconnectedServer, entry.Epoch, entry.RebuildListenAddr)
}
diff --git a/weed/server/master_block_failover_test.go b/weed/server/master_block_failover_test.go
index d36ef2e818..1b60a2f926 100644
--- a/weed/server/master_block_failover_test.go
+++ b/weed/server/master_block_failover_test.go
@@ -50,6 +50,16 @@ func registerVolumeWithReplica(t *testing.T, ms *MasterServer, name, primary, re
ReplicaISCSIAddr: replica + ":3260",
LeaseTTL: leaseTTL,
LastLeaseGrant: time.Now().Add(-2 * leaseTTL), // expired
+ // CP8-2: also populate Replicas[] for PromoteBestReplica.
+ Replicas: []ReplicaInfo{
+ {
+ Server: replica,
+ Path: fmt.Sprintf("/data/%s.blk", name),
+ IQN: fmt.Sprintf("iqn.2024.test:%s-replica", name),
+ ISCSIAddr: replica + ":3260",
+ HealthScore: 1.0,
+ },
+ },
}
if err := ms.blockRegistry.Register(entry); err != nil {
t.Fatalf("register %s: %v", name, err)
@@ -128,12 +138,14 @@ func TestFailover_RegistryUpdated(t *testing.T) {
ms.failoverBlockVolumes("vs1")
entry, _ := ms.blockRegistry.Lookup("vol1")
- // After swap: new primary = vs2, old primary (vs1) becomes replica.
+ // After PromoteBestReplica: new primary = vs2.
+ // Old primary (vs1) is NOT added back to Replicas (needs rebuild).
if entry.VolumeServer != "vs2" {
t.Fatalf("VolumeServer: got %q, want vs2", entry.VolumeServer)
}
- if entry.ReplicaServer != "vs1" {
- t.Fatalf("ReplicaServer: got %q, want vs1 (old primary)", entry.ReplicaServer)
+ // Replicas should be empty (only had 1 replica which was promoted).
+ if len(entry.Replicas) != 0 {
+ t.Fatalf("Replicas should be empty after promotion, got %d", len(entry.Replicas))
}
}
@@ -196,6 +208,9 @@ func TestFailover_LeaseNotExpired_DeferredPromotion(t *testing.T) {
ReplicaISCSIAddr: "vs2:3260",
LeaseTTL: 200 * time.Millisecond,
LastLeaseGrant: time.Now(), // just granted, NOT expired yet
+ Replicas: []ReplicaInfo{
+ {Server: "vs2", Path: "/data/vol1.blk", IQN: "iqn:vol1-r", ISCSIAddr: "vs2:3260", HealthScore: 1.0},
+ },
}
ms.blockRegistry.Register(entry)
@@ -369,13 +384,14 @@ func TestRebuild_RegistryUpdatedWithNewReplica(t *testing.T) {
ms.failoverBlockVolumes("vs1")
ms.recoverBlockVolumes("vs1")
- // After recovery, vs1 should be the new replica for vol1.
+ // After recovery, vs1 should be a replica for vol1 (added back via AddReplica).
entry, _ := ms.blockRegistry.Lookup("vol1")
if entry.VolumeServer != "vs2" {
t.Fatalf("primary should be vs2, got %q", entry.VolumeServer)
}
- if entry.ReplicaServer != "vs1" {
- t.Fatalf("replica should be vs1 (reconnected), got %q", entry.ReplicaServer)
+ // Replicas[] should contain vs1 (added by recoverBlockVolumes).
+ if len(entry.Replicas) != 1 || entry.Replicas[0].Server != "vs1" {
+ t.Fatalf("replica should be vs1 (reconnected), got %+v", entry.Replicas)
}
}
@@ -396,6 +412,9 @@ func TestRebuild_AssignmentContainsRebuildAddr(t *testing.T) {
RebuildListenAddr: "vs1:15000",
LeaseTTL: 5 * time.Second,
LastLeaseGrant: time.Now().Add(-10 * time.Second),
+ Replicas: []ReplicaInfo{
+ {Server: "vs2", Path: "/data/vol1.blk", IQN: "iqn:vol1-r", ISCSIAddr: "vs2:3260", HealthScore: 1.0},
+ },
}
ms.blockRegistry.Register(entry)
@@ -437,6 +456,9 @@ func TestFailover_TransientDisconnect_NoPromotion(t *testing.T) {
ReplicaISCSIAddr: "vs2:3260",
LeaseTTL: 30 * time.Second,
LastLeaseGrant: time.Now(), // just granted
+ Replicas: []ReplicaInfo{
+ {Server: "vs2", Path: "/data/vol1.blk", IQN: "iqn:vol1-r", ISCSIAddr: "vs2:3260", HealthScore: 1.0},
+ },
}
ms.blockRegistry.Register(entry)
@@ -526,3 +548,343 @@ func TestLifecycle_CreateFailoverRebuild(t *testing.T) {
t.Fatal("expected rebuild assignment for reconnected server")
}
}
+
+// ============================================================
+// CP8-2 T8: RF=3 (N>2) Failover Tests
+// ============================================================
+
+// registerVolumeRF3 creates a volume entry with primary + 2 replicas for RF=3 tests.
+func registerVolumeRF3(t *testing.T, ms *MasterServer, name, primary, replica1, replica2 string, epoch uint64, leaseTTL time.Duration) {
+ t.Helper()
+ entry := &BlockVolumeEntry{
+ Name: name,
+ VolumeServer: primary,
+ Path: fmt.Sprintf("/data/%s.blk", name),
+ IQN: fmt.Sprintf("iqn.2024.test:%s", name),
+ ISCSIAddr: primary + ":3260",
+ SizeBytes: 1 << 30,
+ Epoch: epoch,
+ Role: blockvol.RoleToWire(blockvol.RolePrimary),
+ Status: StatusActive,
+ ReplicaFactor: 3,
+ LeaseTTL: leaseTTL,
+ LastLeaseGrant: time.Now().Add(-2 * leaseTTL), // expired
+ // Deprecated scalar fields (backward compat): first replica only.
+ ReplicaServer: replica1,
+ ReplicaPath: fmt.Sprintf("/data/%s.blk", name),
+ ReplicaIQN: fmt.Sprintf("iqn.2024.test:%s-r1", name),
+ ReplicaISCSIAddr: replica1 + ":3260",
+ Replicas: []ReplicaInfo{
+ {
+ Server: replica1,
+ Path: fmt.Sprintf("/data/%s.blk", name),
+ IQN: fmt.Sprintf("iqn.2024.test:%s-r1", name),
+ ISCSIAddr: replica1 + ":3260",
+ HealthScore: 1.0,
+ WALHeadLSN: 100,
+ },
+ {
+ Server: replica2,
+ Path: fmt.Sprintf("/data/%s.blk", name),
+ IQN: fmt.Sprintf("iqn.2024.test:%s-r2", name),
+ ISCSIAddr: replica2 + ":3260",
+ HealthScore: 1.0,
+ WALHeadLSN: 100,
+ },
+ },
+ }
+ if err := ms.blockRegistry.Register(entry); err != nil {
+ t.Fatalf("register %s: %v", name, err)
+ }
+}
+
+// RF3: Primary dies → best replica promoted, other replica survives.
+func TestRF3_PrimaryDies_BestReplicaPromoted(t *testing.T) {
+ ms := testMasterServerForFailover(t)
+ registerVolumeRF3(t, ms, "vol1", "vs1", "vs2", "vs3", 1, 5*time.Second)
+
+ // Give vs3 a higher health score so it should be promoted.
+ entry, _ := ms.blockRegistry.Lookup("vol1")
+ entry.Replicas[0].HealthScore = 0.8 // vs2
+ entry.Replicas[1].HealthScore = 1.0 // vs3
+
+ ms.failoverBlockVolumes("vs1")
+
+ entry, _ = ms.blockRegistry.Lookup("vol1")
+ if entry.VolumeServer != "vs3" {
+ t.Fatalf("primary should be vs3 (highest health), got %q", entry.VolumeServer)
+ }
+ if entry.Epoch != 2 {
+ t.Fatalf("epoch: got %d, want 2", entry.Epoch)
+ }
+ // vs2 should remain as the only replica.
+ if len(entry.Replicas) != 1 {
+ t.Fatalf("expected 1 remaining replica, got %d", len(entry.Replicas))
+ }
+ if entry.Replicas[0].Server != "vs2" {
+ t.Fatalf("remaining replica should be vs2, got %q", entry.Replicas[0].Server)
+ }
+}
+
+// RF3: One replica dies → primary stays, dead replica removed, pending rebuild.
+func TestRF3_OneReplicaDies_PrimaryUnchanged(t *testing.T) {
+ ms := testMasterServerForFailover(t)
+ registerVolumeRF3(t, ms, "vol1", "vs1", "vs2", "vs3", 1, 5*time.Second)
+
+ // vs3 dies (a replica, not the primary).
+ ms.failoverBlockVolumes("vs3")
+
+ entry, _ := ms.blockRegistry.Lookup("vol1")
+ // Primary should remain vs1.
+ if entry.VolumeServer != "vs1" {
+ t.Fatalf("primary should remain vs1, got %q", entry.VolumeServer)
+ }
+ // Epoch should NOT change (no promotion).
+ if entry.Epoch != 1 {
+ t.Fatalf("epoch should remain 1, got %d", entry.Epoch)
+ }
+ // vs3 should be removed from replicas, leaving only vs2.
+ if len(entry.Replicas) != 1 {
+ t.Fatalf("expected 1 replica after vs3 death, got %d", len(entry.Replicas))
+ }
+ if entry.Replicas[0].Server != "vs2" {
+ t.Fatalf("remaining replica should be vs2, got %q", entry.Replicas[0].Server)
+ }
+
+ // Pending rebuild should be recorded for vs3.
+ ms.blockFailover.mu.Lock()
+ rebuilds := ms.blockFailover.pendingRebuilds["vs3"]
+ ms.blockFailover.mu.Unlock()
+ if len(rebuilds) != 1 || rebuilds[0].VolumeName != "vol1" {
+ t.Fatalf("expected 1 pending rebuild for vs3/vol1, got %+v", rebuilds)
+ }
+}
+
+// RF3: Dead replica reconnects → rebuilds, rejoins replica list.
+func TestRF3_RecoverRebuildsDeadReplica(t *testing.T) {
+ ms := testMasterServerForFailover(t)
+ registerVolumeRF3(t, ms, "vol1", "vs1", "vs2", "vs3", 1, 5*time.Second)
+
+ // vs3 dies.
+ ms.failoverBlockVolumes("vs3")
+
+ // vs3 reconnects.
+ ms.recoverBlockVolumes("vs3")
+
+ entry, _ := ms.blockRegistry.Lookup("vol1")
+ // Primary still vs1.
+ if entry.VolumeServer != "vs1" {
+ t.Fatalf("primary should remain vs1, got %q", entry.VolumeServer)
+ }
+ // vs3 should be back in replicas (added by recoverBlockVolumes via AddReplica).
+ if len(entry.Replicas) != 2 {
+ t.Fatalf("expected 2 replicas after recovery, got %d", len(entry.Replicas))
+ }
+ // Verify rebuild assignment was queued for vs3.
+ assignments := ms.blockAssignmentQueue.Peek("vs3")
+ foundRebuild := false
+ for _, a := range assignments {
+ if blockvol.RoleFromWire(a.Role) == blockvol.RoleRebuilding {
+ foundRebuild = true
+ }
+ }
+ if !foundRebuild {
+ t.Fatal("expected rebuild assignment for vs3")
+ }
+}
+
+// RF3: Other replicas survive promotion.
+func TestRF3_OtherReplicasSurvivePromotion(t *testing.T) {
+ ms := testMasterServerForFailover(t)
+ registerVolumeRF3(t, ms, "vol1", "vs1", "vs2", "vs3", 1, 5*time.Second)
+
+ // Primary (vs1) dies → one replica promoted.
+ ms.failoverBlockVolumes("vs1")
+
+ entry, _ := ms.blockRegistry.Lookup("vol1")
+ newPrimary := entry.VolumeServer
+ // New primary should be vs2 or vs3 (both have equal health/LSN, so first wins).
+ if newPrimary != "vs2" && newPrimary != "vs3" {
+ t.Fatalf("new primary should be vs2 or vs3, got %q", newPrimary)
+ }
+ // The other replica should still be in the list.
+ if len(entry.Replicas) != 1 {
+ t.Fatalf("expected 1 surviving replica, got %d", len(entry.Replicas))
+ }
+ // The surviving replica is whichever wasn't promoted.
+ expectedSurvivor := "vs3"
+ if newPrimary == "vs3" {
+ expectedSurvivor = "vs2"
+ }
+ if entry.Replicas[0].Server != expectedSurvivor {
+ t.Fatalf("surviving replica should be %q, got %q", expectedSurvivor, entry.Replicas[0].Server)
+ }
+
+ // Assignment for new primary should include ReplicaAddrs for the surviving replica.
+ assignments := ms.blockAssignmentQueue.Peek(newPrimary)
+ foundPrimary := false
+ for _, a := range assignments {
+ if blockvol.RoleFromWire(a.Role) == blockvol.RolePrimary {
+ foundPrimary = true
+ if len(a.ReplicaAddrs) != 1 {
+ t.Fatalf("expected 1 ReplicaAddr in assignment, got %d", len(a.ReplicaAddrs))
+ }
+ }
+ }
+ if !foundPrimary {
+ t.Fatal("expected primary assignment for new primary")
+ }
+}
+
+// RF2 unchanged: verify RF=2 still works identically.
+func TestRF2_Unchanged_AfterCP82(t *testing.T) {
+ ms := testMasterServerForFailover(t)
+ registerVolumeWithReplica(t, ms, "vol1", "vs1", "vs2", 1, 5*time.Second)
+
+ ms.failoverBlockVolumes("vs1")
+
+ entry, _ := ms.blockRegistry.Lookup("vol1")
+ if entry.VolumeServer != "vs2" {
+ t.Fatalf("RF2: primary should be vs2, got %q", entry.VolumeServer)
+ }
+ if entry.Epoch != 2 {
+ t.Fatalf("RF2: epoch should be 2, got %d", entry.Epoch)
+ }
+ // No replicas left (RF=2, one promoted).
+ if len(entry.Replicas) != 0 {
+ t.Fatalf("RF2: expected 0 replicas after promotion, got %d", len(entry.Replicas))
+ }
+
+ // Rebuild works the same.
+ ms.recoverBlockVolumes("vs1")
+ entry, _ = ms.blockRegistry.Lookup("vol1")
+ if len(entry.Replicas) != 1 || entry.Replicas[0].Server != "vs1" {
+ t.Fatalf("RF2: vs1 should be back as replica after recovery, got %+v", entry.Replicas)
+ }
+}
+
+// RF3: All replicas dead → no promotion possible.
+func TestRF3_AllReplicasDead_NoPromotion(t *testing.T) {
+ ms := testMasterServerForFailover(t)
+ registerVolumeRF3(t, ms, "vol1", "vs1", "vs2", "vs3", 1, 5*time.Second)
+
+ // Both replicas die first (not primary).
+ ms.failoverBlockVolumes("vs2")
+ ms.failoverBlockVolumes("vs3")
+
+ entry, _ := ms.blockRegistry.Lookup("vol1")
+ // Primary still vs1, replicas removed.
+ if entry.VolumeServer != "vs1" {
+ t.Fatalf("primary should remain vs1, got %q", entry.VolumeServer)
+ }
+ if len(entry.Replicas) != 0 {
+ t.Fatalf("expected 0 replicas, got %d", len(entry.Replicas))
+ }
+
+ // Now primary dies → no promotion possible (no replicas left).
+ ms.failoverBlockVolumes("vs1")
+
+ entry, _ = ms.blockRegistry.Lookup("vol1")
+ if entry.VolumeServer != "vs1" {
+ t.Fatalf("should remain vs1 (no replicas to promote), got %q", entry.VolumeServer)
+ }
+}
+
+// RF3: Lease deferred promotion with RF=3.
+func TestRF3_LeaseDeferred_Promotion(t *testing.T) {
+ ms := testMasterServerForFailover(t)
+ entry := &BlockVolumeEntry{
+ Name: "vol1",
+ VolumeServer: "vs1",
+ Path: "/data/vol1.blk",
+ IQN: "iqn:vol1",
+ ISCSIAddr: "vs1:3260",
+ SizeBytes: 1 << 30,
+ Epoch: 1,
+ Role: blockvol.RoleToWire(blockvol.RolePrimary),
+ Status: StatusActive,
+ ReplicaFactor: 3,
+ LeaseTTL: 200 * time.Millisecond,
+ LastLeaseGrant: time.Now(), // just granted → NOT expired
+ Replicas: []ReplicaInfo{
+ {Server: "vs2", Path: "/data/vol1.blk", ISCSIAddr: "vs2:3260", HealthScore: 1.0, WALHeadLSN: 50},
+ {Server: "vs3", Path: "/data/vol1.blk", ISCSIAddr: "vs3:3260", HealthScore: 0.9, WALHeadLSN: 50},
+ },
+ // Deprecated scalar fields.
+ ReplicaServer: "vs2", ReplicaPath: "/data/vol1.blk", ReplicaISCSIAddr: "vs2:3260",
+ }
+ ms.blockRegistry.Register(entry)
+
+ ms.failoverBlockVolumes("vs1")
+
+ // Immediately: no promotion (lease not expired).
+ e, _ := ms.blockRegistry.Lookup("vol1")
+ if e.VolumeServer != "vs1" {
+ t.Fatalf("should NOT promote yet (lease active), got %q", e.VolumeServer)
+ }
+
+ // Wait for lease to expire + deferred timer.
+ time.Sleep(350 * time.Millisecond)
+
+ e, _ = ms.blockRegistry.Lookup("vol1")
+ if e.VolumeServer != "vs2" {
+ t.Fatalf("should promote vs2 (highest health) after lease expires, got %q", e.VolumeServer)
+ }
+ // vs3 should survive as the remaining replica.
+ if len(e.Replicas) != 1 || e.Replicas[0].Server != "vs3" {
+ t.Fatalf("vs3 should remain as replica, got %+v", e.Replicas)
+ }
+}
+
+// RF3: Cancel deferred timers on reconnect.
+func TestRF3_CancelDeferredOnReconnect(t *testing.T) {
+ ms := testMasterServerForFailover(t)
+ entry := &BlockVolumeEntry{
+ Name: "vol1",
+ VolumeServer: "vs1",
+ Path: "/data/vol1.blk",
+ IQN: "iqn:vol1",
+ ISCSIAddr: "vs1:3260",
+ SizeBytes: 1 << 30,
+ Epoch: 1,
+ Role: blockvol.RoleToWire(blockvol.RolePrimary),
+ Status: StatusActive,
+ ReplicaFactor: 3,
+ LeaseTTL: 5 * time.Second,
+ LastLeaseGrant: time.Now(), // just granted → long lease
+ Replicas: []ReplicaInfo{
+ {Server: "vs2", Path: "/data/vol1.blk", ISCSIAddr: "vs2:3260", HealthScore: 1.0},
+ {Server: "vs3", Path: "/data/vol1.blk", ISCSIAddr: "vs3:3260", HealthScore: 1.0},
+ },
+ ReplicaServer: "vs2", ReplicaPath: "/data/vol1.blk", ReplicaISCSIAddr: "vs2:3260",
+ }
+ ms.blockRegistry.Register(entry)
+
+ // vs1 disconnects → deferred timer created (lease not expired).
+ ms.failoverBlockVolumes("vs1")
+
+ ms.blockFailover.mu.Lock()
+ timerCount := len(ms.blockFailover.deferredTimers["vs1"])
+ ms.blockFailover.mu.Unlock()
+ if timerCount == 0 {
+ t.Fatal("expected deferred timer for vs1")
+ }
+
+ // vs1 reconnects before lease expires → timers cancelled.
+ ms.recoverBlockVolumes("vs1")
+
+ ms.blockFailover.mu.Lock()
+ timerCount = len(ms.blockFailover.deferredTimers["vs1"])
+ ms.blockFailover.mu.Unlock()
+ if timerCount != 0 {
+ t.Fatalf("expected 0 deferred timers after reconnect, got %d", timerCount)
+ }
+
+ // Wait past original lease time — no promotion should happen.
+ time.Sleep(200 * time.Millisecond)
+
+ e, _ := ms.blockRegistry.Lookup("vol1")
+ if e.VolumeServer != "vs1" {
+ t.Fatalf("vs1 should remain primary (timer cancelled), got %q", e.VolumeServer)
+ }
+}
diff --git a/weed/server/master_block_registry.go b/weed/server/master_block_registry.go
index 8b05d8a18c..0187ecf24f 100644
--- a/weed/server/master_block_registry.go
+++ b/weed/server/master_block_registry.go
@@ -2,9 +2,14 @@ package weed_server
import (
"fmt"
+ "path/filepath"
+ "sort"
+ "strings"
"sync"
+ "sync/atomic"
"time"
+ "github.com/seaweedfs/seaweedfs/weed/glog"
"github.com/seaweedfs/seaweedfs/weed/pb/master_pb"
"github.com/seaweedfs/seaweedfs/weed/storage/blockvol"
)
@@ -17,32 +22,102 @@ const (
StatusActive // Confirmed by heartbeat from volume server
)
+// ReplicaInfo tracks one replica of a block volume (CP8-2).
+type ReplicaInfo struct {
+ Server string // replica VS address
+ Path string // file path on replica VS
+ ISCSIAddr string // iSCSI target address
+ IQN string // iSCSI qualified name
+ DataAddr string // WAL receiver data listen addr
+ CtrlAddr string // WAL receiver ctrl listen addr
+ HealthScore float64 // from heartbeat (0.0-1.0)
+ WALHeadLSN uint64 // from heartbeat
+ WALLag uint64 // computed: primary.WALHeadLSN - replica.WALHeadLSN
+ LastHeartbeat time.Time // last heartbeat received from this replica
+ Role uint32 // replica role (RoleReplica, RoleRebuilding, etc.)
+}
+
+const (
+ // DefaultPromotionLSNTolerance is the max WAL LSN lag allowed for promotion eligibility.
+ // Configurable per-registry via SetPromotionLSNTolerance.
+ DefaultPromotionLSNTolerance uint64 = 100
+)
+
// BlockVolumeEntry tracks one block volume across the cluster.
type BlockVolumeEntry struct {
- Name string
- VolumeServer string // volume server address (ip:port or grpc addr)
- Path string // file path on volume server
- IQN string
- ISCSIAddr string
- SizeBytes uint64
- Epoch uint64
- Role uint32
- Status VolumeStatus
+ Name string
+ VolumeServer string // volume server address (ip:port or grpc addr)
+ Path string // file path on volume server
+ IQN string
+ ISCSIAddr string
+ SizeBytes uint64
+ ReplicaPlacement string // SeaweedFS placement string: "000", "001", "010", "100"
+ Epoch uint64
+ Role uint32
+ Status VolumeStatus
- // Replica tracking (CP6-3).
- ReplicaServer string // replica VS address
- ReplicaPath string // file path on replica VS
+ // Deprecated scalar replica fields (CP6-3). Use Replicas[] for new code.
+ ReplicaServer string
+ ReplicaPath string
ReplicaISCSIAddr string
ReplicaIQN string
- ReplicaDataAddr string // replica receiver data listen addr
- ReplicaCtrlAddr string // replica receiver ctrl listen addr
+ ReplicaDataAddr string
+ ReplicaCtrlAddr string
RebuildListenAddr string // rebuild server listen addr on primary
+ // CP8-2: Multi-replica support.
+ ReplicaFactor int // 2 or 3 (default 2)
+ Replicas []ReplicaInfo // one per replica (RF-1 entries)
+ HealthScore float64 // primary health score from heartbeat
+ ReplicaDegraded bool // primary reports degraded replicas
+ WALHeadLSN uint64 // primary WAL head LSN from heartbeat
+
// Lease tracking for failover (CP6-3 F2).
LastLeaseGrant time.Time
LeaseTTL time.Duration
}
+// HasReplica returns true if this volume has any replica (checks both new and deprecated fields).
+func (e *BlockVolumeEntry) HasReplica() bool {
+ return len(e.Replicas) > 0 || e.ReplicaServer != ""
+}
+
+// FirstReplica returns the first replica info, or nil if none.
+func (e *BlockVolumeEntry) FirstReplica() *ReplicaInfo {
+ if len(e.Replicas) > 0 {
+ return &e.Replicas[0]
+ }
+ return nil
+}
+
+// ReplicaByServer returns the replica hosted on the given server, or nil.
+func (e *BlockVolumeEntry) ReplicaByServer(server string) *ReplicaInfo {
+ for i := range e.Replicas {
+ if e.Replicas[i].Server == server {
+ return &e.Replicas[i]
+ }
+ }
+ return nil
+}
+
+// BestReplicaForPromotion returns the best replica for promotion, or nil if none eligible.
+// Criteria: highest HealthScore, tie-break by highest WALHeadLSN, then first in list.
+func (e *BlockVolumeEntry) BestReplicaForPromotion() *ReplicaInfo {
+ if len(e.Replicas) == 0 {
+ return nil
+ }
+ best := 0
+ for i := 1; i < len(e.Replicas); i++ {
+ if e.Replicas[i].HealthScore > e.Replicas[best].HealthScore {
+ best = i
+ } else if e.Replicas[i].HealthScore == e.Replicas[best].HealthScore &&
+ e.Replicas[i].WALHeadLSN > e.Replicas[best].WALHeadLSN {
+ best = i
+ }
+ }
+ return &e.Replicas[best]
+}
+
// BlockVolumeRegistry is the in-memory registry of block volumes.
// Rebuilt from heartbeats on master restart (no persistence).
type BlockVolumeRegistry struct {
@@ -51,8 +126,16 @@ type BlockVolumeRegistry struct {
byServer map[string]map[string]bool // server -> set of volume names
blockServers map[string]bool // servers known to support block volumes
+ // Promotion eligibility: max WAL LSN lag for replica to be promotable.
+ promotionLSNTolerance uint64
+
// inflight guards concurrent CreateBlockVolume for the same name.
inflight sync.Map // name -> *inflightEntry
+
+ // Metrics (CP8-4).
+ PromotionsTotal atomic.Uint64
+ FailoversTotal atomic.Uint64
+ RebuildsTotal atomic.Uint64
}
type inflightEntry struct{}
@@ -60,9 +143,10 @@ type inflightEntry struct{}
// NewBlockVolumeRegistry creates an empty registry.
func NewBlockVolumeRegistry() *BlockVolumeRegistry {
return &BlockVolumeRegistry{
- volumes: make(map[string]*BlockVolumeEntry),
- byServer: make(map[string]map[string]bool),
- blockServers: make(map[string]bool),
+ volumes: make(map[string]*BlockVolumeEntry),
+ byServer: make(map[string]map[string]bool),
+ blockServers: make(map[string]bool),
+ promotionLSNTolerance: DefaultPromotionLSNTolerance,
}
}
@@ -76,6 +160,10 @@ func (r *BlockVolumeRegistry) Register(entry *BlockVolumeEntry) error {
}
r.volumes[entry.Name] = entry
r.addToServer(entry.VolumeServer, entry.Name)
+ // Also index replica servers so ListByServer finds them.
+ for _, ri := range entry.Replicas {
+ r.addToServer(ri.Server, entry.Name)
+ }
return nil
}
@@ -89,9 +177,25 @@ func (r *BlockVolumeRegistry) Unregister(name string) *BlockVolumeEntry {
}
delete(r.volumes, name)
r.removeFromServer(entry.VolumeServer, name)
+ for _, ri := range entry.Replicas {
+ r.removeFromServer(ri.Server, name)
+ }
return entry
}
+// UpdateSize updates the size of a registered volume.
+// Called only after a successful VS expand to keep registry in sync.
+func (r *BlockVolumeRegistry) UpdateSize(name string, newSizeBytes uint64) error {
+ r.mu.Lock()
+ defer r.mu.Unlock()
+ entry, ok := r.volumes[name]
+ if !ok {
+ return fmt.Errorf("block volume %q not found in registry", name)
+ }
+ entry.SizeBytes = newSizeBytes
+ return nil
+}
+
// Lookup returns the entry for the given name.
func (r *BlockVolumeRegistry) Lookup(name string) (*BlockVolumeEntry, bool) {
r.mu.RLock()
@@ -134,51 +238,172 @@ func (r *BlockVolumeRegistry) UpdateFullHeartbeat(server string, infos []*master
reported[info.Path] = info
}
- // Find entries for this server that are NOT reported -> remove them.
+ // Find entries for this server that are NOT reported -> reconcile.
if names, ok := r.byServer[server]; ok {
for name := range names {
entry := r.volumes[name]
if entry == nil {
continue
}
- if _, found := reported[entry.Path]; !found {
- delete(r.volumes, name)
- delete(names, name)
+ if entry.VolumeServer == server {
+ // Server is the primary: check if primary path is reported.
+ if _, found := reported[entry.Path]; !found {
+ delete(r.volumes, name)
+ delete(names, name)
+ // Also clean up replica entries from byServer.
+ for _, ri := range entry.Replicas {
+ r.removeFromServer(ri.Server, name)
+ }
+ }
+ } else {
+ // Server is a replica: check if replica path is reported.
+ ri := entry.ReplicaByServer(server)
+ if ri == nil {
+ // No replica record — stale byServer index, just clean up.
+ delete(names, name)
+ continue
+ }
+ if _, found := reported[ri.Path]; !found {
+ // Replica path not reported — remove this replica, NOT the whole volume.
+ r.removeReplicaLocked(entry, server, name)
+ delete(names, name)
+ glog.V(0).Infof("block registry: removed stale replica %s for %q (path %s not in heartbeat)",
+ server, name, ri.Path)
+ }
}
}
}
// Update or add entries for reported volumes.
for _, info := range infos {
- // Find existing entry by path on this server.
+ // Find existing entry: search byServer index for matching path (primary or replica).
var existing *BlockVolumeEntry
+ var existingName string
if names, ok := r.byServer[server]; ok {
- for name := range names {
- if e := r.volumes[name]; e != nil && e.Path == info.Path {
- existing = e
- break
+ for vname := range names {
+ if e := r.volumes[vname]; e != nil {
+ if e.VolumeServer == server && e.Path == info.Path {
+ existing = e
+ existingName = vname
+ break
+ }
+ if ri := e.ReplicaByServer(server); ri != nil && ri.Path == info.Path {
+ existing = e
+ existingName = vname
+ break
+ }
}
}
}
+ // Also try lookup by name derived from path (handles post-restart).
+ if existing == nil {
+ name := nameFromPath(info.Path)
+ if name != "" {
+ if e, ok := r.volumes[name]; ok {
+ existing = e
+ existingName = name
+ }
+ }
+ }
+
if existing != nil {
- // Update fields from heartbeat.
- existing.SizeBytes = info.VolumeSize
- existing.Epoch = info.Epoch
- existing.Role = info.Role
- existing.Status = StatusActive
- // R1-5: Refresh lease on heartbeat — VS is alive and running this volume.
- existing.LastLeaseGrant = time.Now()
- // F5: update replica addresses from heartbeat info.
- if info.ReplicaDataAddr != "" {
- existing.ReplicaDataAddr = info.ReplicaDataAddr
+ isPrimary := existing.VolumeServer == server
+ isReplica := existing.ReplicaByServer(server) != nil
+
+ if isPrimary {
+ // Primary heartbeat: update primary fields.
+ existing.SizeBytes = info.VolumeSize
+ existing.Epoch = info.Epoch
+ existing.Role = info.Role
+ existing.Status = StatusActive
+ existing.LastLeaseGrant = time.Now()
+ existing.HealthScore = info.HealthScore
+ existing.ReplicaDegraded = info.ReplicaDegraded
+ existing.WALHeadLSN = info.WalHeadLsn
+ // F5: update replica addresses from heartbeat info.
+ if info.ReplicaDataAddr != "" {
+ existing.ReplicaDataAddr = info.ReplicaDataAddr
+ }
+ if info.ReplicaCtrlAddr != "" {
+ existing.ReplicaCtrlAddr = info.ReplicaCtrlAddr
+ }
+ // Sync first replica's data addrs to Replicas[].
+ if info.ReplicaDataAddr != "" && len(existing.Replicas) > 0 {
+ existing.Replicas[0].DataAddr = info.ReplicaDataAddr
+ existing.Replicas[0].CtrlAddr = info.ReplicaCtrlAddr
+ }
+ } else if isReplica {
+ // Replica heartbeat: update ReplicaInfo fields.
+ for i := range existing.Replicas {
+ if existing.Replicas[i].Server == server {
+ existing.Replicas[i].Path = info.Path
+ existing.Replicas[i].WALHeadLSN = info.WalHeadLsn
+ existing.Replicas[i].HealthScore = info.HealthScore
+ existing.Replicas[i].LastHeartbeat = time.Now()
+ existing.Replicas[i].Role = info.Role
+ if existing.WALHeadLSN > info.WalHeadLsn {
+ existing.Replicas[i].WALLag = existing.WALHeadLSN - info.WalHeadLsn
+ } else {
+ existing.Replicas[i].WALLag = 0
+ }
+ break
+ }
+ }
+ } else {
+ // Fix #3: Server reports a volume that exists but has no record of this server.
+ // This happens after master restart: primary heartbeat re-created the entry,
+ // but replica heartbeat arrives and isn't linked. Add as replica.
+ ri := ReplicaInfo{
+ Server: server,
+ Path: info.Path,
+ HealthScore: info.HealthScore,
+ WALHeadLSN: info.WalHeadLsn,
+ LastHeartbeat: time.Now(),
+ Role: info.Role,
+ }
+ existing.Replicas = append(existing.Replicas, ri)
+ r.addToServer(server, existingName)
+ // Sync deprecated scalar fields.
+ if len(existing.Replicas) == 1 {
+ existing.ReplicaServer = ri.Server
+ existing.ReplicaPath = ri.Path
+ }
+ glog.V(0).Infof("block registry: attached replica %s for %q from heartbeat (path=%s)",
+ server, existingName, info.Path)
+ }
+ } else {
+ // Auto-register volumes reported by heartbeat but not in registry.
+ // This recovers state after master restart.
+ name := nameFromPath(info.Path)
+ if name == "" {
+ continue
}
- if info.ReplicaCtrlAddr != "" {
- existing.ReplicaCtrlAddr = info.ReplicaCtrlAddr
+ if _, dup := r.volumes[name]; !dup {
+ entry := &BlockVolumeEntry{
+ Name: name,
+ VolumeServer: server,
+ Path: info.Path,
+ SizeBytes: info.VolumeSize,
+ Epoch: info.Epoch,
+ Role: info.Role,
+ Status: StatusActive,
+ LastLeaseGrant: time.Now(),
+ LeaseTTL: 30 * time.Second,
+ HealthScore: info.HealthScore,
+ WALHeadLSN: info.WalHeadLsn,
+ }
+ if info.ReplicaDataAddr != "" {
+ entry.ReplicaDataAddr = info.ReplicaDataAddr
+ }
+ if info.ReplicaCtrlAddr != "" {
+ entry.ReplicaCtrlAddr = info.ReplicaCtrlAddr
+ }
+ r.volumes[name] = entry
+ r.addToServer(server, name)
+ glog.V(0).Infof("block registry: auto-registered %q from heartbeat (server=%s, path=%s, size=%d)",
+ name, server, info.Path, info.VolumeSize)
}
}
- // If no existing entry found by path, it was created outside master
- // (e.g., manually). We don't auto-register unknown volumes — they
- // must be created via CreateBlockVolume RPC.
}
}
@@ -274,7 +499,38 @@ func (r *BlockVolumeRegistry) removeFromServer(server, name string) {
}
}
+// removeReplicaLocked removes a replica from an entry by server address.
+// Caller must hold r.mu. Also syncs deprecated scalar fields.
+func (r *BlockVolumeRegistry) removeReplicaLocked(entry *BlockVolumeEntry, server, name string) {
+ newReplicas := make([]ReplicaInfo, 0, len(entry.Replicas))
+ for _, ri := range entry.Replicas {
+ if ri.Server == server {
+ continue
+ }
+ newReplicas = append(newReplicas, ri)
+ }
+ entry.Replicas = newReplicas
+ // Sync deprecated scalar fields.
+ if len(entry.Replicas) > 0 {
+ r0 := &entry.Replicas[0]
+ entry.ReplicaServer = r0.Server
+ entry.ReplicaPath = r0.Path
+ entry.ReplicaISCSIAddr = r0.ISCSIAddr
+ entry.ReplicaIQN = r0.IQN
+ entry.ReplicaDataAddr = r0.DataAddr
+ entry.ReplicaCtrlAddr = r0.CtrlAddr
+ } else {
+ entry.ReplicaServer = ""
+ entry.ReplicaPath = ""
+ entry.ReplicaISCSIAddr = ""
+ entry.ReplicaIQN = ""
+ entry.ReplicaDataAddr = ""
+ entry.ReplicaCtrlAddr = ""
+ }
+}
+
// SetReplica sets replica info for a registered volume.
+// Deprecated: use AddReplica for new code. This method syncs both scalar and Replicas[].
func (r *BlockVolumeRegistry) SetReplica(name, server, path, iscsiAddr, iqn string) error {
r.mu.Lock()
defer r.mu.Unlock()
@@ -290,12 +546,31 @@ func (r *BlockVolumeRegistry) SetReplica(name, server, path, iscsiAddr, iqn stri
entry.ReplicaPath = path
entry.ReplicaISCSIAddr = iscsiAddr
entry.ReplicaIQN = iqn
- // Also add to byServer index for the replica server.
r.addToServer(server, name)
+
+ // CP8-2: also sync to Replicas[].
+ info := ReplicaInfo{Server: server, Path: path, ISCSIAddr: iscsiAddr, IQN: iqn}
+ replaced := false
+ for i := range entry.Replicas {
+ if entry.Replicas[i].Server == server {
+ // Preserve existing health/LSN data.
+ info.HealthScore = entry.Replicas[i].HealthScore
+ info.WALHeadLSN = entry.Replicas[i].WALHeadLSN
+ info.DataAddr = entry.Replicas[i].DataAddr
+ info.CtrlAddr = entry.Replicas[i].CtrlAddr
+ entry.Replicas[i] = info
+ replaced = true
+ break
+ }
+ }
+ if !replaced {
+ entry.Replicas = append(entry.Replicas, info)
+ }
return nil
}
-// ClearReplica removes replica info for a registered volume.
+// ClearReplica removes all replica info for a registered volume.
+// Deprecated: use RemoveReplica for new code. This method clears both scalar and Replicas[].
func (r *BlockVolumeRegistry) ClearReplica(name string) error {
r.mu.Lock()
defer r.mu.Unlock()
@@ -306,12 +581,19 @@ func (r *BlockVolumeRegistry) ClearReplica(name string) error {
if entry.ReplicaServer != "" {
r.removeFromServer(entry.ReplicaServer, name)
}
+ // Remove all replicas from byServer index.
+ for _, ri := range entry.Replicas {
+ if ri.Server != entry.ReplicaServer {
+ r.removeFromServer(ri.Server, name)
+ }
+ }
entry.ReplicaServer = ""
entry.ReplicaPath = ""
entry.ReplicaISCSIAddr = ""
entry.ReplicaIQN = ""
entry.ReplicaDataAddr = ""
entry.ReplicaCtrlAddr = ""
+ entry.Replicas = nil
return nil
}
@@ -363,6 +645,198 @@ func (r *BlockVolumeRegistry) SwapPrimaryReplica(name string) (uint64, error) {
return newEpoch, nil
}
+// AddReplica adds or replaces a replica in the Replicas slice (by server).
+// Also updates the byServer index and deprecated scalar fields for backward compat.
+func (r *BlockVolumeRegistry) AddReplica(name string, info ReplicaInfo) error {
+ r.mu.Lock()
+ defer r.mu.Unlock()
+ entry, ok := r.volumes[name]
+ if !ok {
+ return fmt.Errorf("block volume %q not found", name)
+ }
+ // Replace if same server already exists.
+ replaced := false
+ for i := range entry.Replicas {
+ if entry.Replicas[i].Server == info.Server {
+ entry.Replicas[i] = info
+ replaced = true
+ break
+ }
+ }
+ if !replaced {
+ entry.Replicas = append(entry.Replicas, info)
+ }
+ r.addToServer(info.Server, name)
+
+ // Sync deprecated scalar fields (first replica → scalar).
+ if len(entry.Replicas) > 0 {
+ r0 := &entry.Replicas[0]
+ entry.ReplicaServer = r0.Server
+ entry.ReplicaPath = r0.Path
+ entry.ReplicaISCSIAddr = r0.ISCSIAddr
+ entry.ReplicaIQN = r0.IQN
+ entry.ReplicaDataAddr = r0.DataAddr
+ entry.ReplicaCtrlAddr = r0.CtrlAddr
+ }
+ return nil
+}
+
+// RemoveReplica removes a replica by server address.
+func (r *BlockVolumeRegistry) RemoveReplica(name, server string) error {
+ r.mu.Lock()
+ defer r.mu.Unlock()
+ entry, ok := r.volumes[name]
+ if !ok {
+ return fmt.Errorf("block volume %q not found", name)
+ }
+ found := false
+ newReplicas := make([]ReplicaInfo, 0, len(entry.Replicas))
+ for _, ri := range entry.Replicas {
+ if ri.Server == server {
+ found = true
+ r.removeFromServer(server, name)
+ continue
+ }
+ newReplicas = append(newReplicas, ri)
+ }
+ if !found {
+ return fmt.Errorf("replica on %q not found for volume %q", server, name)
+ }
+ entry.Replicas = newReplicas
+
+ // Sync deprecated scalar fields.
+ if len(entry.Replicas) > 0 {
+ r0 := &entry.Replicas[0]
+ entry.ReplicaServer = r0.Server
+ entry.ReplicaPath = r0.Path
+ entry.ReplicaISCSIAddr = r0.ISCSIAddr
+ entry.ReplicaIQN = r0.IQN
+ entry.ReplicaDataAddr = r0.DataAddr
+ entry.ReplicaCtrlAddr = r0.CtrlAddr
+ } else {
+ entry.ReplicaServer = ""
+ entry.ReplicaPath = ""
+ entry.ReplicaISCSIAddr = ""
+ entry.ReplicaIQN = ""
+ entry.ReplicaDataAddr = ""
+ entry.ReplicaCtrlAddr = ""
+ }
+ return nil
+}
+
+// SetPromotionLSNTolerance configures the max WAL LSN lag for promotion eligibility.
+func (r *BlockVolumeRegistry) SetPromotionLSNTolerance(tolerance uint64) {
+ r.mu.Lock()
+ defer r.mu.Unlock()
+ r.promotionLSNTolerance = tolerance
+}
+
+// PromotionLSNTolerance returns the current promotion LSN tolerance.
+func (r *BlockVolumeRegistry) PromotionLSNTolerance() uint64 {
+ r.mu.RLock()
+ defer r.mu.RUnlock()
+ return r.promotionLSNTolerance
+}
+
+// PromoteBestReplica promotes the best eligible replica to primary.
+// Eligibility: heartbeat fresh (within 2×LeaseTTL), WALHeadLSN within tolerance of primary,
+// and role must be RoleReplica (not RoleRebuilding).
+// The promoted replica is removed from Replicas[]. Other replicas stay.
+// Old primary is NOT added to Replicas (needs rebuild).
+// Returns the new epoch.
+func (r *BlockVolumeRegistry) PromoteBestReplica(name string) (uint64, error) {
+ r.mu.Lock()
+ defer r.mu.Unlock()
+ entry, ok := r.volumes[name]
+ if !ok {
+ return 0, fmt.Errorf("block volume %q not found", name)
+ }
+ if len(entry.Replicas) == 0 {
+ return 0, fmt.Errorf("block volume %q has no replicas", name)
+ }
+
+ // Filter eligible replicas.
+ now := time.Now()
+ freshnessCutoff := 2 * entry.LeaseTTL
+ if freshnessCutoff == 0 {
+ freshnessCutoff = 60 * time.Second // default if LeaseTTL not set
+ }
+ primaryLSN := entry.WALHeadLSN
+
+ bestIdx := -1
+ for i := range entry.Replicas {
+ ri := &entry.Replicas[i]
+ // Gate 1: heartbeat freshness.
+ if !ri.LastHeartbeat.IsZero() && now.Sub(ri.LastHeartbeat) > freshnessCutoff {
+ continue
+ }
+ // Gate 2: WAL LSN recency (skip if primary LSN is 0 — no data yet, all eligible).
+ if primaryLSN > 0 && ri.WALHeadLSN+r.promotionLSNTolerance < primaryLSN {
+ continue
+ }
+ // Gate 3: role must be RoleReplica (not rebuilding/stale).
+ if ri.Role != 0 && blockvol.RoleFromWire(ri.Role) != blockvol.RoleReplica {
+ continue
+ }
+ // Eligible — pick best by health score, tie-break by WALHeadLSN.
+ if bestIdx == -1 {
+ bestIdx = i
+ } else if ri.HealthScore > entry.Replicas[bestIdx].HealthScore {
+ bestIdx = i
+ } else if ri.HealthScore == entry.Replicas[bestIdx].HealthScore &&
+ ri.WALHeadLSN > entry.Replicas[bestIdx].WALHeadLSN {
+ bestIdx = i
+ }
+ }
+
+ if bestIdx == -1 {
+ return 0, fmt.Errorf("block volume %q: no eligible replicas for promotion", name)
+ }
+
+ promoted := entry.Replicas[bestIdx]
+
+ // Remove old primary from byServer index.
+ r.removeFromServer(entry.VolumeServer, name)
+
+ // Bump epoch atomically.
+ newEpoch := entry.Epoch + 1
+
+ // Promote replica to primary.
+ entry.VolumeServer = promoted.Server
+ entry.Path = promoted.Path
+ entry.IQN = promoted.IQN
+ entry.ISCSIAddr = promoted.ISCSIAddr
+ entry.Epoch = newEpoch
+ entry.Role = blockvol.RoleToWire(blockvol.RolePrimary)
+ entry.LastLeaseGrant = time.Now()
+
+ // Remove promoted from Replicas. Others stay.
+ entry.Replicas = append(entry.Replicas[:bestIdx], entry.Replicas[bestIdx+1:]...)
+
+ // Sync deprecated scalar fields.
+ if len(entry.Replicas) > 0 {
+ r0 := &entry.Replicas[0]
+ entry.ReplicaServer = r0.Server
+ entry.ReplicaPath = r0.Path
+ entry.ReplicaISCSIAddr = r0.ISCSIAddr
+ entry.ReplicaIQN = r0.IQN
+ entry.ReplicaDataAddr = r0.DataAddr
+ entry.ReplicaCtrlAddr = r0.CtrlAddr
+ } else {
+ entry.ReplicaServer = ""
+ entry.ReplicaPath = ""
+ entry.ReplicaISCSIAddr = ""
+ entry.ReplicaIQN = ""
+ entry.ReplicaDataAddr = ""
+ entry.ReplicaCtrlAddr = ""
+ }
+
+ // Update byServer index: new primary server now hosts this volume.
+ r.addToServer(entry.VolumeServer, name)
+
+ return newEpoch, nil
+}
+
// MarkBlockCapable records that the given server supports block volumes.
func (r *BlockVolumeRegistry) MarkBlockCapable(server string) {
r.mu.Lock()
@@ -377,6 +851,45 @@ func (r *BlockVolumeRegistry) UnmarkBlockCapable(server string) {
r.mu.Unlock()
}
+// ListAll returns all registered block volume entries, sorted by name.
+func (r *BlockVolumeRegistry) ListAll() []*BlockVolumeEntry {
+ r.mu.RLock()
+ defer r.mu.RUnlock()
+ entries := make([]*BlockVolumeEntry, 0, len(r.volumes))
+ for _, e := range r.volumes {
+ entries = append(entries, e)
+ }
+ sort.Slice(entries, func(i, j int) bool { return entries[i].Name < entries[j].Name })
+ return entries
+}
+
+// BlockServerSummary summarizes a block-capable volume server.
+type BlockServerSummary struct {
+ Address string
+ VolumeCount int
+ BlockCapable bool
+}
+
+// ServerSummaries returns a summary for each block-capable server.
+func (r *BlockVolumeRegistry) ServerSummaries() []BlockServerSummary {
+ r.mu.RLock()
+ defer r.mu.RUnlock()
+ summaries := make([]BlockServerSummary, 0, len(r.blockServers))
+ for addr := range r.blockServers {
+ count := 0
+ if names, ok := r.byServer[addr]; ok {
+ count = len(names)
+ }
+ summaries = append(summaries, BlockServerSummary{
+ Address: addr,
+ VolumeCount: count,
+ BlockCapable: true,
+ })
+ }
+ sort.Slice(summaries, func(i, j int) bool { return summaries[i].Address < summaries[j].Address })
+ return summaries
+}
+
// BlockCapableServers returns the list of servers known to support block volumes.
func (r *BlockVolumeRegistry) BlockCapableServers() []string {
r.mu.RLock()
@@ -387,3 +900,38 @@ func (r *BlockVolumeRegistry) BlockCapableServers() []string {
}
return servers
}
+
+// MaxBarrierLagLSN returns the maximum WAL lag across all volumes and replicas.
+// This is the primary durability-risk metric: primary WALHeadLSN minus
+// replica's WALHeadLSN. SLO threshold: < 100 under normal load.
+func (r *BlockVolumeRegistry) MaxBarrierLagLSN() uint64 {
+ r.mu.RLock()
+ defer r.mu.RUnlock()
+ var maxLag uint64
+ for _, entry := range r.volumes {
+ for _, ri := range entry.Replicas {
+ if ri.WALLag > maxLag {
+ maxLag = ri.WALLag
+ }
+ }
+ }
+ return maxLag
+}
+
+// AssignmentQueueDepth returns the total number of pending assignments across all servers.
+func (r *BlockVolumeRegistry) AssignmentQueueDepth() int {
+ // Delegated to the assignment queue, not the registry.
+ // Placeholder: the queue tracks its own depth.
+ return 0
+}
+
+// nameFromPath extracts the volume name from a .blk file path.
+// e.g. "/opt/data/block/my-volume.blk" -> "my-volume"
+func nameFromPath(path string) string {
+ base := filepath.Base(path)
+ if strings.HasSuffix(base, ".blk") {
+ return strings.TrimSuffix(base, ".blk")
+ }
+ return base
+}
+
diff --git a/weed/server/master_block_registry_test.go b/weed/server/master_block_registry_test.go
index ec060980d6..aa28a1f112 100644
--- a/weed/server/master_block_registry_test.go
+++ b/weed/server/master_block_registry_test.go
@@ -4,8 +4,10 @@ import (
"fmt"
"sync"
"testing"
+ "time"
"github.com/seaweedfs/seaweedfs/weed/pb/master_pb"
+ "github.com/seaweedfs/seaweedfs/weed/storage/blockvol"
)
func TestRegistry_RegisterLookup(t *testing.T) {
@@ -434,3 +436,558 @@ func TestFullHeartbeat_UpdatesReplicaAddrs(t *testing.T) {
t.Fatalf("ReplicaCtrlAddr: got %q, want 10.0.0.2:14261", entry.ReplicaCtrlAddr)
}
}
+
+// --- CP8-2 new tests ---
+
+func TestRegistry_AddReplica(t *testing.T) {
+ r := NewBlockVolumeRegistry()
+ r.Register(&BlockVolumeEntry{Name: "vol1", VolumeServer: "s1", Path: "/v1.blk"})
+
+ err := r.AddReplica("vol1", ReplicaInfo{
+ Server: "s2",
+ Path: "/replica/v1.blk",
+ ISCSIAddr: "10.0.0.2:3260",
+ IQN: "iqn:vol1-r1",
+ DataAddr: "s2:14260",
+ CtrlAddr: "s2:14261",
+ })
+ if err != nil {
+ t.Fatalf("AddReplica: %v", err)
+ }
+
+ e, _ := r.Lookup("vol1")
+ if len(e.Replicas) != 1 {
+ t.Fatalf("Replicas len: got %d, want 1", len(e.Replicas))
+ }
+ if e.Replicas[0].Server != "s2" {
+ t.Fatalf("Replicas[0].Server: got %q", e.Replicas[0].Server)
+ }
+ // Deprecated scalar should be synced.
+ if e.ReplicaServer != "s2" {
+ t.Fatalf("ReplicaServer (deprecated): got %q", e.ReplicaServer)
+ }
+ // byServer index should include replica.
+ if len(r.ListByServer("s2")) != 1 {
+ t.Fatalf("ListByServer(s2): got %d, want 1", len(r.ListByServer("s2")))
+ }
+}
+
+func TestRegistry_AddReplica_TwoRF3(t *testing.T) {
+ r := NewBlockVolumeRegistry()
+ r.Register(&BlockVolumeEntry{Name: "vol1", VolumeServer: "s1", Path: "/v1.blk", ReplicaFactor: 3})
+
+ r.AddReplica("vol1", ReplicaInfo{Server: "s2", Path: "/r1.blk", IQN: "iqn:r1"})
+ r.AddReplica("vol1", ReplicaInfo{Server: "s3", Path: "/r2.blk", IQN: "iqn:r2"})
+
+ e, _ := r.Lookup("vol1")
+ if len(e.Replicas) != 2 {
+ t.Fatalf("Replicas len: got %d, want 2", len(e.Replicas))
+ }
+ if e.Replicas[0].Server != "s2" || e.Replicas[1].Server != "s3" {
+ t.Fatalf("Replicas: got %+v", e.Replicas)
+ }
+ // byServer index should include both.
+ if len(r.ListByServer("s2")) != 1 || len(r.ListByServer("s3")) != 1 {
+ t.Fatal("byServer should include both replica servers")
+ }
+}
+
+func TestRegistry_AddReplica_Upsert(t *testing.T) {
+ r := NewBlockVolumeRegistry()
+ r.Register(&BlockVolumeEntry{Name: "vol1", VolumeServer: "s1", Path: "/v1.blk"})
+
+ r.AddReplica("vol1", ReplicaInfo{Server: "s2", Path: "/r1.blk"})
+ r.AddReplica("vol1", ReplicaInfo{Server: "s2", Path: "/r1-new.blk"})
+
+ e, _ := r.Lookup("vol1")
+ if len(e.Replicas) != 1 {
+ t.Fatalf("Replicas len: got %d, want 1 (upsert, not duplicate)", len(e.Replicas))
+ }
+ if e.Replicas[0].Path != "/r1-new.blk" {
+ t.Fatalf("Replicas[0].Path: got %q, want /r1-new.blk", e.Replicas[0].Path)
+ }
+}
+
+func TestRegistry_RemoveReplica(t *testing.T) {
+ r := NewBlockVolumeRegistry()
+ r.Register(&BlockVolumeEntry{Name: "vol1", VolumeServer: "s1", Path: "/v1.blk"})
+ r.AddReplica("vol1", ReplicaInfo{Server: "s2", Path: "/r1.blk"})
+ r.AddReplica("vol1", ReplicaInfo{Server: "s3", Path: "/r2.blk"})
+
+ err := r.RemoveReplica("vol1", "s2")
+ if err != nil {
+ t.Fatalf("RemoveReplica: %v", err)
+ }
+
+ e, _ := r.Lookup("vol1")
+ if len(e.Replicas) != 1 {
+ t.Fatalf("Replicas len: got %d, want 1", len(e.Replicas))
+ }
+ if e.Replicas[0].Server != "s3" {
+ t.Fatalf("remaining replica should be s3, got %q", e.Replicas[0].Server)
+ }
+ // Deprecated scalar should sync to first remaining replica.
+ if e.ReplicaServer != "s3" {
+ t.Fatalf("ReplicaServer (deprecated): got %q, want s3", e.ReplicaServer)
+ }
+ // s2 should be removed from byServer.
+ if len(r.ListByServer("s2")) != 0 {
+ t.Fatalf("ListByServer(s2): got %d, want 0", len(r.ListByServer("s2")))
+ }
+}
+
+func TestRegistry_PromoteBestReplica_PicksHighest(t *testing.T) {
+ r := NewBlockVolumeRegistry()
+ r.Register(&BlockVolumeEntry{
+ Name: "vol1",
+ VolumeServer: "s1",
+ Path: "/v1.blk",
+ Epoch: 5,
+ Role: 1,
+ Replicas: []ReplicaInfo{
+ {Server: "s2", Path: "/r1.blk", IQN: "iqn:r1", ISCSIAddr: "s2:3260", HealthScore: 0.8, WALHeadLSN: 100},
+ {Server: "s3", Path: "/r2.blk", IQN: "iqn:r2", ISCSIAddr: "s3:3260", HealthScore: 0.95, WALHeadLSN: 90},
+ },
+ })
+ // Add to byServer for s2 and s3.
+ r.mu.Lock()
+ r.addToServer("s2", "vol1")
+ r.addToServer("s3", "vol1")
+ r.mu.Unlock()
+
+ newEpoch, err := r.PromoteBestReplica("vol1")
+ if err != nil {
+ t.Fatalf("PromoteBestReplica: %v", err)
+ }
+ if newEpoch != 6 {
+ t.Fatalf("newEpoch: got %d, want 6", newEpoch)
+ }
+
+ e, _ := r.Lookup("vol1")
+ // s3 had higher health score → promoted.
+ if e.VolumeServer != "s3" {
+ t.Fatalf("VolumeServer: got %q, want s3 (higher health)", e.VolumeServer)
+ }
+ if e.Path != "/r2.blk" {
+ t.Fatalf("Path: got %q", e.Path)
+ }
+ // s2 should remain in Replicas.
+ if len(e.Replicas) != 1 {
+ t.Fatalf("Replicas len: got %d, want 1 (s2 stays)", len(e.Replicas))
+ }
+ if e.Replicas[0].Server != "s2" {
+ t.Fatalf("remaining replica: got %q, want s2", e.Replicas[0].Server)
+ }
+}
+
+func TestRegistry_PromoteBestReplica_NoReplica(t *testing.T) {
+ r := NewBlockVolumeRegistry()
+ r.Register(&BlockVolumeEntry{Name: "vol1", VolumeServer: "s1", Path: "/v1.blk"})
+
+ _, err := r.PromoteBestReplica("vol1")
+ if err == nil {
+ t.Fatal("PromoteBestReplica with no replicas should return error")
+ }
+}
+
+func TestRegistry_PromoteBestReplica_TiebreakByLSN(t *testing.T) {
+ r := NewBlockVolumeRegistry()
+ r.Register(&BlockVolumeEntry{
+ Name: "vol1",
+ VolumeServer: "s1",
+ Path: "/v1.blk",
+ Epoch: 3,
+ Replicas: []ReplicaInfo{
+ {Server: "s2", Path: "/r1.blk", IQN: "iqn:r1", ISCSIAddr: "s2:3260", HealthScore: 0.9, WALHeadLSN: 50},
+ {Server: "s3", Path: "/r2.blk", IQN: "iqn:r2", ISCSIAddr: "s3:3260", HealthScore: 0.9, WALHeadLSN: 100},
+ },
+ })
+ r.mu.Lock()
+ r.addToServer("s2", "vol1")
+ r.addToServer("s3", "vol1")
+ r.mu.Unlock()
+
+ newEpoch, err := r.PromoteBestReplica("vol1")
+ if err != nil {
+ t.Fatalf("PromoteBestReplica: %v", err)
+ }
+ if newEpoch != 4 {
+ t.Fatalf("newEpoch: got %d, want 4", newEpoch)
+ }
+
+ e, _ := r.Lookup("vol1")
+ // Same health → tie-break by WALHeadLSN → s3 wins.
+ if e.VolumeServer != "s3" {
+ t.Fatalf("VolumeServer: got %q, want s3 (higher LSN)", e.VolumeServer)
+ }
+ if len(e.Replicas) != 1 || e.Replicas[0].Server != "s2" {
+ t.Fatalf("remaining replica: got %+v, want [s2]", e.Replicas)
+ }
+}
+
+func TestRegistry_PromoteBestReplica_KeepsOthers(t *testing.T) {
+ r := NewBlockVolumeRegistry()
+ r.Register(&BlockVolumeEntry{
+ Name: "vol1",
+ VolumeServer: "s1",
+ Path: "/v1.blk",
+ Epoch: 1,
+ Replicas: []ReplicaInfo{
+ {Server: "s2", Path: "/r1.blk", IQN: "iqn:r1", ISCSIAddr: "s2:3260", HealthScore: 1.0, WALHeadLSN: 100},
+ {Server: "s3", Path: "/r2.blk", IQN: "iqn:r2", ISCSIAddr: "s3:3260", HealthScore: 0.5, WALHeadLSN: 100},
+ },
+ })
+ r.mu.Lock()
+ r.addToServer("s2", "vol1")
+ r.addToServer("s3", "vol1")
+ r.mu.Unlock()
+
+ r.PromoteBestReplica("vol1")
+
+ e, _ := r.Lookup("vol1")
+ // s2 promoted, s3 stays.
+ if e.VolumeServer != "s2" {
+ t.Fatalf("VolumeServer: got %q, want s2", e.VolumeServer)
+ }
+ if len(e.Replicas) != 1 || e.Replicas[0].Server != "s3" {
+ t.Fatalf("remaining replicas: got %+v, want [s3]", e.Replicas)
+ }
+}
+
+func TestRegistry_BackwardCompatAccessors(t *testing.T) {
+ r := NewBlockVolumeRegistry()
+ r.Register(&BlockVolumeEntry{Name: "vol1", VolumeServer: "s1", Path: "/v1.blk"})
+
+ e, _ := r.Lookup("vol1")
+ if e.HasReplica() {
+ t.Fatal("HasReplica should be false with no replicas")
+ }
+ if e.FirstReplica() != nil {
+ t.Fatal("FirstReplica should be nil")
+ }
+ if e.BestReplicaForPromotion() != nil {
+ t.Fatal("BestReplicaForPromotion should be nil")
+ }
+
+ r.AddReplica("vol1", ReplicaInfo{Server: "s2", Path: "/r.blk", HealthScore: 0.9})
+
+ e, _ = r.Lookup("vol1")
+ if !e.HasReplica() {
+ t.Fatal("HasReplica should be true after AddReplica")
+ }
+ if e.FirstReplica() == nil || e.FirstReplica().Server != "s2" {
+ t.Fatal("FirstReplica should return s2")
+ }
+ if e.ReplicaByServer("s2") == nil {
+ t.Fatal("ReplicaByServer(s2) should not be nil")
+ }
+ if e.ReplicaByServer("s3") != nil {
+ t.Fatal("ReplicaByServer(s3) should be nil")
+ }
+}
+
+func TestRegistry_ReplicaFactorDefault(t *testing.T) {
+ r := NewBlockVolumeRegistry()
+ r.Register(&BlockVolumeEntry{Name: "vol1", VolumeServer: "s1", Path: "/v1.blk"})
+
+ e, _ := r.Lookup("vol1")
+ // ReplicaFactor defaults to 0 (zero value). API handler defaults to 2.
+ if e.ReplicaFactor != 0 {
+ t.Fatalf("default ReplicaFactor: got %d, want 0", e.ReplicaFactor)
+ }
+
+ // Explicit RF=3.
+ r.Register(&BlockVolumeEntry{Name: "vol2", VolumeServer: "s1", Path: "/v2.blk", ReplicaFactor: 3})
+ e2, _ := r.Lookup("vol2")
+ if e2.ReplicaFactor != 3 {
+ t.Fatalf("ReplicaFactor: got %d, want 3", e2.ReplicaFactor)
+ }
+}
+
+func TestRegistry_FullHeartbeat_UpdatesHealthScore(t *testing.T) {
+ r := NewBlockVolumeRegistry()
+ r.Register(&BlockVolumeEntry{
+ Name: "vol1",
+ VolumeServer: "s1",
+ Path: "/v1.blk",
+ Status: StatusPending,
+ })
+
+ r.UpdateFullHeartbeat("s1", []*master_pb.BlockVolumeInfoMessage{
+ {
+ Path: "/v1.blk",
+ VolumeSize: 1 << 30,
+ Epoch: 1,
+ Role: 1,
+ HealthScore: 0.85,
+ ScrubErrors: 2,
+ WalHeadLsn: 500,
+ },
+ })
+
+ e, _ := r.Lookup("vol1")
+ if e.HealthScore != 0.85 {
+ t.Fatalf("HealthScore: got %f, want 0.85", e.HealthScore)
+ }
+ if e.WALHeadLSN != 500 {
+ t.Fatalf("WALHeadLSN: got %d, want 500", e.WALHeadLSN)
+ }
+}
+
+// Fix #1: Replica heartbeat must NOT delete the volume.
+func TestRegistry_ReplicaHeartbeat_DoesNotDeleteVolume(t *testing.T) {
+ r := NewBlockVolumeRegistry()
+ r.Register(&BlockVolumeEntry{
+ Name: "vol1",
+ VolumeServer: "primary",
+ Path: "/data/vol1.blk",
+ Status: StatusActive,
+ Replicas: []ReplicaInfo{
+ {Server: "replica1", Path: "/data/vol1.blk"},
+ },
+ })
+
+ // Replica sends heartbeat reporting its path.
+ r.UpdateFullHeartbeat("replica1", []*master_pb.BlockVolumeInfoMessage{
+ {Path: "/data/vol1.blk", Epoch: 1, Role: 2},
+ })
+
+ // Volume must still exist with primary intact.
+ e, ok := r.Lookup("vol1")
+ if !ok {
+ t.Fatal("vol1 should not be deleted when replica sends heartbeat")
+ }
+ if e.VolumeServer != "primary" {
+ t.Fatalf("primary should remain 'primary', got %q", e.VolumeServer)
+ }
+}
+
+// Fix #1: Replica path NOT reported → replica removed, volume preserved.
+func TestRegistry_ReplicaHeartbeat_StaleReplicaRemoved(t *testing.T) {
+ r := NewBlockVolumeRegistry()
+ r.Register(&BlockVolumeEntry{
+ Name: "vol1",
+ VolumeServer: "primary",
+ Path: "/data/vol1.blk",
+ Status: StatusActive,
+ Replicas: []ReplicaInfo{
+ {Server: "replica1", Path: "/data/vol1.blk"},
+ {Server: "replica2", Path: "/data/vol1.blk"},
+ },
+ })
+
+ // replica1 heartbeat does NOT report vol1 path → stale replica.
+ r.UpdateFullHeartbeat("replica1", []*master_pb.BlockVolumeInfoMessage{})
+
+ // Volume still exists, but replica1 removed.
+ e, ok := r.Lookup("vol1")
+ if !ok {
+ t.Fatal("vol1 should exist (only replica removed)")
+ }
+ if len(e.Replicas) != 1 {
+ t.Fatalf("expected 1 replica after stale removal, got %d", len(e.Replicas))
+ }
+ if e.Replicas[0].Server != "replica2" {
+ t.Fatalf("remaining replica should be replica2, got %q", e.Replicas[0].Server)
+ }
+}
+
+// Fix #3: Replica heartbeat after master restart reconstructs ReplicaInfo.
+func TestRegistry_ReplicaHeartbeat_ReconstructsAfterRestart(t *testing.T) {
+ r := NewBlockVolumeRegistry()
+ // Simulate master restart: primary heartbeat re-created entry without replicas.
+ r.Register(&BlockVolumeEntry{
+ Name: "vol1",
+ VolumeServer: "primary",
+ Path: "/data/vol1.blk",
+ Status: StatusActive,
+ })
+
+ // Replica heartbeat arrives — vol1 exists but has no record of this server.
+ r.UpdateFullHeartbeat("replica1", []*master_pb.BlockVolumeInfoMessage{
+ {Path: "/data/vol1.blk", Epoch: 1, Role: 2, HealthScore: 0.95, WalHeadLsn: 42},
+ })
+
+ // vol1 should now have replica1 in Replicas[].
+ e, ok := r.Lookup("vol1")
+ if !ok {
+ t.Fatal("vol1 should exist")
+ }
+ if len(e.Replicas) != 1 {
+ t.Fatalf("expected 1 replica after reconstruction, got %d", len(e.Replicas))
+ }
+ ri := e.Replicas[0]
+ if ri.Server != "replica1" {
+ t.Fatalf("replica server: got %q, want replica1", ri.Server)
+ }
+ if ri.HealthScore != 0.95 {
+ t.Fatalf("replica health: got %f, want 0.95", ri.HealthScore)
+ }
+ if ri.WALHeadLSN != 42 {
+ t.Fatalf("replica WALHeadLSN: got %d, want 42", ri.WALHeadLSN)
+ }
+ // byServer index should include replica1.
+ entries := r.ListByServer("replica1")
+ if len(entries) != 1 || entries[0].Name != "vol1" {
+ t.Fatalf("ListByServer(replica1) should return vol1, got %+v", entries)
+ }
+}
+
+// Fix #2: Stale replica (old heartbeat) not eligible for promotion.
+func TestRegistry_PromoteBestReplica_StaleHeartbeatIneligible(t *testing.T) {
+ r := NewBlockVolumeRegistry()
+ r.Register(&BlockVolumeEntry{
+ Name: "vol1",
+ VolumeServer: "primary",
+ Path: "/data/vol1.blk",
+ Epoch: 1,
+ LeaseTTL: 5 * time.Second,
+ WALHeadLSN: 100,
+ Replicas: []ReplicaInfo{
+ {
+ Server: "stale-replica",
+ Path: "/data/vol1.blk",
+ HealthScore: 1.0,
+ WALHeadLSN: 100,
+ LastHeartbeat: time.Now().Add(-30 * time.Second), // stale (>2×5s)
+ },
+ },
+ })
+
+ _, err := r.PromoteBestReplica("vol1")
+ if err == nil {
+ t.Fatal("expected error: stale replica should not be eligible")
+ }
+}
+
+// Fix #2: Replica with WAL lag too large is not eligible.
+func TestRegistry_PromoteBestReplica_WALLagIneligible(t *testing.T) {
+ r := NewBlockVolumeRegistry()
+ r.Register(&BlockVolumeEntry{
+ Name: "vol1",
+ VolumeServer: "primary",
+ Path: "/data/vol1.blk",
+ Epoch: 1,
+ LeaseTTL: 30 * time.Second,
+ WALHeadLSN: 1000,
+ Replicas: []ReplicaInfo{
+ {
+ Server: "lagging",
+ Path: "/data/vol1.blk",
+ HealthScore: 1.0,
+ WALHeadLSN: 800, // lag=200, tolerance=100
+ LastHeartbeat: time.Now(),
+ },
+ },
+ })
+
+ _, err := r.PromoteBestReplica("vol1")
+ if err == nil {
+ t.Fatal("expected error: lagging replica should not be eligible")
+ }
+}
+
+// Fix #2: Rebuilding replica is not eligible for promotion.
+func TestRegistry_PromoteBestReplica_RebuildingIneligible(t *testing.T) {
+ r := NewBlockVolumeRegistry()
+ r.Register(&BlockVolumeEntry{
+ Name: "vol1",
+ VolumeServer: "primary",
+ Path: "/data/vol1.blk",
+ Epoch: 1,
+ LeaseTTL: 30 * time.Second,
+ WALHeadLSN: 100,
+ Replicas: []ReplicaInfo{
+ {
+ Server: "rebuilding",
+ Path: "/data/vol1.blk",
+ HealthScore: 1.0,
+ WALHeadLSN: 100,
+ LastHeartbeat: time.Now(),
+ Role: blockvol.RoleToWire(blockvol.RoleRebuilding),
+ },
+ },
+ })
+
+ _, err := r.PromoteBestReplica("vol1")
+ if err == nil {
+ t.Fatal("expected error: rebuilding replica should not be eligible")
+ }
+}
+
+// Fix #2: Among eligible replicas, best (health+LSN) wins.
+func TestRegistry_PromoteBestReplica_EligibilityFiltersCorrectly(t *testing.T) {
+ r := NewBlockVolumeRegistry()
+ r.Register(&BlockVolumeEntry{
+ Name: "vol1",
+ VolumeServer: "primary",
+ Path: "/data/vol1.blk",
+ Epoch: 1,
+ LeaseTTL: 30 * time.Second,
+ WALHeadLSN: 100,
+ Replicas: []ReplicaInfo{
+ {
+ Server: "stale", // ineligible: old heartbeat
+ Path: "/data/vol1.blk",
+ HealthScore: 1.0,
+ WALHeadLSN: 100,
+ LastHeartbeat: time.Now().Add(-2 * time.Minute),
+ },
+ {
+ Server: "good", // eligible
+ Path: "/data/vol1.blk",
+ HealthScore: 0.8,
+ WALHeadLSN: 95,
+ LastHeartbeat: time.Now(),
+ },
+ },
+ })
+
+ _, err := r.PromoteBestReplica("vol1")
+ if err != nil {
+ t.Fatalf("expected promotion to succeed: %v", err)
+ }
+ e, _ := r.Lookup("vol1")
+ if e.VolumeServer != "good" {
+ t.Fatalf("expected 'good' promoted (only eligible), got %q", e.VolumeServer)
+ }
+}
+
+// Configurable tolerance: widen tolerance to allow lagging replicas.
+func TestRegistry_PromoteBestReplica_ConfigurableTolerance(t *testing.T) {
+ r := NewBlockVolumeRegistry()
+ r.Register(&BlockVolumeEntry{
+ Name: "vol1",
+ VolumeServer: "primary",
+ Path: "/data/vol1.blk",
+ Epoch: 1,
+ LeaseTTL: 30 * time.Second,
+ WALHeadLSN: 1000,
+ Replicas: []ReplicaInfo{
+ {
+ Server: "lagging",
+ Path: "/data/vol1.blk",
+ HealthScore: 1.0,
+ WALHeadLSN: 800, // lag=200
+ LastHeartbeat: time.Now(),
+ },
+ },
+ })
+
+ // Default tolerance (100): lag 200 > tolerance → ineligible.
+ _, err := r.PromoteBestReplica("vol1")
+ if err == nil {
+ t.Fatal("expected error with default tolerance")
+ }
+
+ // Widen tolerance to 250: lag 200 < tolerance → eligible.
+ r.SetPromotionLSNTolerance(250)
+ _, err = r.PromoteBestReplica("vol1")
+ if err != nil {
+ t.Fatalf("expected success with widened tolerance: %v", err)
+ }
+ e, _ := r.Lookup("vol1")
+ if e.VolumeServer != "lagging" {
+ t.Fatalf("expected 'lagging' promoted, got %q", e.VolumeServer)
+ }
+}
diff --git a/weed/server/master_grpc_server_block.go b/weed/server/master_grpc_server_block.go
index e74f55387b..bbf04df0b5 100644
--- a/weed/server/master_grpc_server_block.go
+++ b/weed/server/master_grpc_server_block.go
@@ -8,8 +8,21 @@ import (
"github.com/seaweedfs/seaweedfs/weed/glog"
"github.com/seaweedfs/seaweedfs/weed/pb/master_pb"
"github.com/seaweedfs/seaweedfs/weed/storage/blockvol"
+ "github.com/seaweedfs/seaweedfs/weed/util/request_id"
)
+// blockReqID extracts a short request ID from context for log correlation.
+func blockReqID(ctx context.Context) string {
+ id := request_id.Get(ctx)
+ if len(id) > 8 {
+ return id[:8]
+ }
+ if id == "" {
+ return "no-id"
+ }
+ return id
+}
+
// CreateBlockVolume picks a volume server, delegates creation, and records
// the mapping in the block volume registry.
func (ms *MasterServer) CreateBlockVolume(ctx context.Context, req *master_pb.CreateBlockVolumeRequest) (*master_pb.CreateBlockVolumeResponse, error) {
@@ -25,13 +38,7 @@ func (ms *MasterServer) CreateBlockVolume(ctx context.Context, req *master_pb.Cr
if entry.SizeBytes < req.SizeBytes {
return nil, fmt.Errorf("block volume %q exists with size %d (requested %d)", req.Name, entry.SizeBytes, req.SizeBytes)
}
- return &master_pb.CreateBlockVolumeResponse{
- VolumeId: entry.Name,
- VolumeServer: entry.VolumeServer,
- IscsiAddr: entry.ISCSIAddr,
- Iqn: entry.IQN,
- CapacityBytes: entry.SizeBytes,
- }, nil
+ return ms.createBlockVolumeResponseFromEntry(entry), nil
}
// Per-name inflight lock prevents concurrent creates for the same name.
@@ -42,13 +49,7 @@ func (ms *MasterServer) CreateBlockVolume(ctx context.Context, req *master_pb.Cr
// Double-check after acquiring lock (another goroutine may have finished).
if entry, ok := ms.blockRegistry.Lookup(req.Name); ok {
- return &master_pb.CreateBlockVolumeResponse{
- VolumeId: entry.Name,
- VolumeServer: entry.VolumeServer,
- IscsiAddr: entry.ISCSIAddr,
- Iqn: entry.IQN,
- CapacityBytes: entry.SizeBytes,
- }, nil
+ return ms.createBlockVolumeResponseFromEntry(entry), nil
}
// Get candidate servers.
@@ -73,11 +74,17 @@ func (ms *MasterServer) CreateBlockVolume(ctx context.Context, req *master_pb.Cr
result, err := ms.blockVSAllocate(ctx, server, req.Name, req.SizeBytes, req.DiskType)
if err != nil {
lastErr = fmt.Errorf("server %s: %w", server, err)
- glog.V(0).Infof("CreateBlockVolume %q: attempt %d on %s failed: %v", req.Name, attempt+1, server, err)
+ glog.V(0).Infof("[reqID=%s] CreateBlockVolume %q: attempt %d on %s failed: %v", blockReqID(ctx), req.Name, attempt+1, server, err)
servers = removeServer(servers, server)
continue
}
+ // CP8-2: determine replica factor from request (default 2).
+ replicaFactor := 2
+ if req.ReplicaFactor > 0 && req.ReplicaFactor <= 3 {
+ replicaFactor = int(req.ReplicaFactor)
+ }
+
entry := &BlockVolumeEntry{
Name: req.Name,
VolumeServer: server,
@@ -88,66 +95,85 @@ func (ms *MasterServer) CreateBlockVolume(ctx context.Context, req *master_pb.Cr
Epoch: 1,
Role: blockvol.RoleToWire(blockvol.RolePrimary),
Status: StatusActive,
+ ReplicaFactor: replicaFactor,
LeaseTTL: 30 * time.Second,
LastLeaseGrant: time.Now(), // R2-F1: set BEFORE Register to avoid stale-lease race
}
- // Try to create replica on a different server (F4: partial create OK).
- var replicaServer string
- remainingServers := removeServer(servers, server)
- if len(remainingServers) > 0 {
- replicaServer = ms.tryCreateReplica(ctx, req, entry, result, remainingServers)
- } else {
- glog.V(0).Infof("CreateBlockVolume %q: single-copy mode (only 1 server)", req.Name)
+ // Create replicaFactor-1 replicas on different servers (F4: partial create OK).
+ remaining := removeServer(servers, server)
+ for i := 0; i < replicaFactor-1 && len(remaining) > 0; i++ {
+ replicaServer := ms.tryCreateOneReplica(ctx, req, entry, result, remaining)
+ if replicaServer != "" {
+ remaining = removeServer(remaining, replicaServer)
+ }
+ }
+ if len(entry.Replicas) == 0 && replicaFactor > 1 {
+ glog.V(0).Infof("[reqID=%s] CreateBlockVolume %q: single-copy mode (replica allocation failed)", blockReqID(ctx), req.Name)
+ }
+ // Sync deprecated scalar fields from first replica.
+ if len(entry.Replicas) > 0 {
+ r0 := &entry.Replicas[0]
+ entry.ReplicaServer = r0.Server
+ entry.ReplicaPath = r0.Path
+ entry.ReplicaIQN = r0.IQN
+ entry.ReplicaISCSIAddr = r0.ISCSIAddr
+ entry.ReplicaDataAddr = r0.DataAddr
+ entry.ReplicaCtrlAddr = r0.CtrlAddr
}
// Register in registry as Active (VS confirmed creation).
if err := ms.blockRegistry.Register(entry); err != nil {
// Already registered (race condition) — return the existing entry.
if existing, ok := ms.blockRegistry.Lookup(req.Name); ok {
- return &master_pb.CreateBlockVolumeResponse{
- VolumeId: existing.Name,
- VolumeServer: existing.VolumeServer,
- IscsiAddr: existing.ISCSIAddr,
- Iqn: existing.IQN,
- CapacityBytes: existing.SizeBytes,
- ReplicaServer: existing.ReplicaServer,
- }, nil
+ return ms.createBlockVolumeResponseFromEntry(existing), nil
}
return nil, fmt.Errorf("register block volume: %w", err)
}
- // Enqueue assignments for primary (and replica if available).
+ // Enqueue assignments for primary (and replicas if available).
leaseTTLMs := blockvol.LeaseTTLToWire(30 * time.Second)
- ms.blockAssignmentQueue.Enqueue(server, blockvol.BlockVolumeAssignment{
- Path: result.Path,
- Epoch: 1,
- Role: blockvol.RoleToWire(blockvol.RolePrimary),
- LeaseTtlMs: leaseTTLMs,
- ReplicaDataAddr: entry.ReplicaDataAddr,
- ReplicaCtrlAddr: entry.ReplicaCtrlAddr,
- })
- if entry.ReplicaServer != "" {
- ms.blockAssignmentQueue.Enqueue(entry.ReplicaServer, blockvol.BlockVolumeAssignment{
- Path: entry.ReplicaPath,
+ primaryAssignment := blockvol.BlockVolumeAssignment{
+ Path: result.Path,
+ Epoch: 1,
+ Role: blockvol.RoleToWire(blockvol.RolePrimary),
+ LeaseTtlMs: leaseTTLMs,
+ }
+ // CP8-2: populate ReplicaAddrs for multi-replica.
+ for _, ri := range entry.Replicas {
+ primaryAssignment.ReplicaAddrs = append(primaryAssignment.ReplicaAddrs, blockvol.ReplicaAddr{
+ DataAddr: ri.DataAddr,
+ CtrlAddr: ri.CtrlAddr,
+ })
+ }
+ // Backward compat: also set scalar fields if exactly 1 replica.
+ if len(entry.Replicas) == 1 {
+ primaryAssignment.ReplicaDataAddr = entry.Replicas[0].DataAddr
+ primaryAssignment.ReplicaCtrlAddr = entry.Replicas[0].CtrlAddr
+ }
+ ms.blockAssignmentQueue.Enqueue(server, primaryAssignment)
+
+ // Enqueue assignments for each replica.
+ for _, ri := range entry.Replicas {
+ ms.blockAssignmentQueue.Enqueue(ri.Server, blockvol.BlockVolumeAssignment{
+ Path: ri.Path,
Epoch: 1,
Role: blockvol.RoleToWire(blockvol.RoleReplica),
LeaseTtlMs: leaseTTLMs,
- ReplicaDataAddr: entry.ReplicaDataAddr,
- ReplicaCtrlAddr: entry.ReplicaCtrlAddr,
+ ReplicaDataAddr: ri.DataAddr,
+ ReplicaCtrlAddr: ri.CtrlAddr,
})
}
- glog.V(0).Infof("CreateBlockVolume %q: created on %s (path=%s, iqn=%s, replica=%s)",
- req.Name, server, result.Path, result.IQN, replicaServer)
- return &master_pb.CreateBlockVolumeResponse{
- VolumeId: req.Name,
- VolumeServer: server,
- IscsiAddr: result.ISCSIAddr,
- Iqn: result.IQN,
- CapacityBytes: req.SizeBytes,
- ReplicaServer: replicaServer,
- }, nil
+ // Collect replica server addresses for response.
+ var replicaServers []string
+ for _, ri := range entry.Replicas {
+ replicaServers = append(replicaServers, ri.Server)
+ }
+
+ glog.V(0).Infof("[reqID=%s] CreateBlockVolume %q: created on %s (path=%s, iqn=%s, replicas=%v)",
+ blockReqID(ctx), req.Name, server, result.Path, result.IQN, replicaServers)
+ return ms.createBlockVolumeResponseFromEntry(entry), nil
}
return nil, fmt.Errorf("all volume servers failed for %q: %v", req.Name, lastErr)
@@ -170,16 +196,16 @@ func (ms *MasterServer) DeleteBlockVolume(ctx context.Context, req *master_pb.De
return nil, fmt.Errorf("delete block volume %q on %s: %w", req.Name, entry.VolumeServer, err)
}
- // R2-F4: Also delete replica (best-effort, don't fail if replica is down).
- if entry.ReplicaServer != "" {
- if err := ms.blockVSDelete(ctx, entry.ReplicaServer, req.Name); err != nil {
- glog.Warningf("DeleteBlockVolume %q: replica delete on %s failed (best-effort): %v",
- req.Name, entry.ReplicaServer, err)
+ // CP8-2: Delete ALL replicas (best-effort, don't fail if replica is down).
+ for _, ri := range entry.Replicas {
+ if err := ms.blockVSDelete(ctx, ri.Server, req.Name); err != nil {
+ glog.Warningf("[reqID=%s] DeleteBlockVolume %q: replica delete on %s failed (best-effort): %v",
+ blockReqID(ctx), req.Name, ri.Server, err)
}
}
ms.blockRegistry.Unregister(req.Name)
- glog.V(0).Infof("DeleteBlockVolume %q: removed from %s (replica=%s)", req.Name, entry.VolumeServer, entry.ReplicaServer)
+ glog.V(0).Infof("[reqID=%s] DeleteBlockVolume %q: removed from %s (replicas=%d)", blockReqID(ctx), req.Name, entry.VolumeServer, len(entry.Replicas))
return &master_pb.DeleteBlockVolumeResponse{}, nil
}
@@ -194,37 +220,180 @@ func (ms *MasterServer) LookupBlockVolume(ctx context.Context, req *master_pb.Lo
return nil, fmt.Errorf("block volume %q not found", req.Name)
}
+ replicaServers := replicaServerList(entry)
+ rf := entry.ReplicaFactor
+ if rf == 0 {
+ rf = 2 // default for pre-CP8-2 entries
+ }
return &master_pb.LookupBlockVolumeResponse{
- VolumeServer: entry.VolumeServer,
- IscsiAddr: entry.ISCSIAddr,
- Iqn: entry.IQN,
- CapacityBytes: entry.SizeBytes,
- ReplicaServer: entry.ReplicaServer,
+ VolumeServer: entry.VolumeServer,
+ IscsiAddr: entry.ISCSIAddr,
+ Iqn: entry.IQN,
+ CapacityBytes: entry.SizeBytes,
+ ReplicaServer: entry.ReplicaServer, // backward compat
+ ReplicaFactor: uint32(rf),
+ ReplicaServers: replicaServers,
}, nil
}
-// tryCreateReplica attempts to create a replica volume on a different server.
+// tryCreateOneReplica attempts to create one replica volume on a different server.
// Returns the replica server address on success, or empty string on failure (F4).
-func (ms *MasterServer) tryCreateReplica(ctx context.Context, req *master_pb.CreateBlockVolumeRequest, entry *BlockVolumeEntry, primaryResult *blockAllocResult, candidates []string) string {
+func (ms *MasterServer) tryCreateOneReplica(ctx context.Context, req *master_pb.CreateBlockVolumeRequest, entry *BlockVolumeEntry, primaryResult *blockAllocResult, candidates []string) string {
for _, replicaServerStr := range candidates {
replicaResult, err := ms.blockVSAllocate(ctx, replicaServerStr, req.Name, req.SizeBytes, req.DiskType)
if err != nil {
- glog.V(0).Infof("CreateBlockVolume %q: replica on %s failed: %v", req.Name, replicaServerStr, err)
+ glog.V(0).Infof("[reqID=%s] CreateBlockVolume %q: replica on %s failed: %v", blockReqID(ctx), req.Name, replicaServerStr, err)
continue
}
- entry.ReplicaServer = replicaServerStr
- entry.ReplicaPath = replicaResult.Path
- entry.ReplicaIQN = replicaResult.IQN
- entry.ReplicaISCSIAddr = replicaResult.ISCSIAddr
- entry.ReplicaDataAddr = replicaResult.ReplicaDataAddr
- entry.ReplicaCtrlAddr = replicaResult.ReplicaCtrlAddr
entry.RebuildListenAddr = primaryResult.RebuildListenAddr
+ // CP8-2: populate Replicas[].
+ entry.Replicas = append(entry.Replicas, ReplicaInfo{
+ Server: replicaServerStr,
+ Path: replicaResult.Path,
+ ISCSIAddr: replicaResult.ISCSIAddr,
+ IQN: replicaResult.IQN,
+ DataAddr: replicaResult.ReplicaDataAddr,
+ CtrlAddr: replicaResult.ReplicaCtrlAddr,
+ })
return replicaServerStr
}
- glog.Warningf("CreateBlockVolume %q: created without replica (replica allocation failed)", req.Name)
+ glog.Warningf("[reqID=%s] CreateBlockVolume %q: created without replica (replica allocation failed)", blockReqID(ctx), req.Name)
return ""
}
+// CreateBlockSnapshot creates a snapshot on a block volume via the volume server.
+func (ms *MasterServer) CreateBlockSnapshot(ctx context.Context, req *master_pb.CreateBlockSnapshotRequest) (*master_pb.CreateBlockSnapshotResponse, error) {
+ if req.VolumeName == "" {
+ return nil, fmt.Errorf("volume_name is required")
+ }
+
+ entry, ok := ms.blockRegistry.Lookup(req.VolumeName)
+ if !ok {
+ return nil, fmt.Errorf("block volume %q not found", req.VolumeName)
+ }
+
+ createdAt, sizeBytes, err := ms.blockVSSnapshot(ctx, entry.VolumeServer, req.VolumeName, req.SnapshotId)
+ if err != nil {
+ return nil, fmt.Errorf("create snapshot on %s: %w", entry.VolumeServer, err)
+ }
+
+ return &master_pb.CreateBlockSnapshotResponse{
+ SnapshotId: req.SnapshotId,
+ CreatedAt: createdAt,
+ SizeBytes: sizeBytes,
+ }, nil
+}
+
+// DeleteBlockSnapshot deletes a snapshot from a block volume via the volume server.
+func (ms *MasterServer) DeleteBlockSnapshot(ctx context.Context, req *master_pb.DeleteBlockSnapshotRequest) (*master_pb.DeleteBlockSnapshotResponse, error) {
+ if req.VolumeName == "" {
+ return nil, fmt.Errorf("volume_name is required")
+ }
+
+ entry, ok := ms.blockRegistry.Lookup(req.VolumeName)
+ if !ok {
+ // Idempotent: volume not found → snapshot doesn't exist either.
+ return &master_pb.DeleteBlockSnapshotResponse{}, nil
+ }
+
+ if err := ms.blockVSDeleteSnap(ctx, entry.VolumeServer, req.VolumeName, req.SnapshotId); err != nil {
+ return nil, fmt.Errorf("delete snapshot on %s: %w", entry.VolumeServer, err)
+ }
+
+ return &master_pb.DeleteBlockSnapshotResponse{}, nil
+}
+
+// ListBlockSnapshots lists all snapshots on a block volume via the volume server.
+func (ms *MasterServer) ListBlockSnapshots(ctx context.Context, req *master_pb.ListBlockSnapshotsRequest) (*master_pb.ListBlockSnapshotsResponse, error) {
+ if req.VolumeName == "" {
+ return nil, fmt.Errorf("volume_name is required")
+ }
+
+ entry, ok := ms.blockRegistry.Lookup(req.VolumeName)
+ if !ok {
+ return nil, fmt.Errorf("block volume %q not found", req.VolumeName)
+ }
+
+ vsInfos, err := ms.blockVSListSnaps(ctx, entry.VolumeServer, req.VolumeName)
+ if err != nil {
+ return nil, fmt.Errorf("list snapshots on %s: %w", entry.VolumeServer, err)
+ }
+
+ resp := &master_pb.ListBlockSnapshotsResponse{}
+ for _, si := range vsInfos {
+ resp.Snapshots = append(resp.Snapshots, &master_pb.BlockSnapshotInfo{
+ SnapshotId: si.SnapshotId,
+ CreatedAt: si.CreatedAt,
+ VolumeSizeBytes: si.VolumeSizeBytes,
+ })
+ }
+ return resp, nil
+}
+
+// ExpandBlockVolume expands a block volume via the volume server, then updates registry.
+func (ms *MasterServer) ExpandBlockVolume(ctx context.Context, req *master_pb.ExpandBlockVolumeRequest) (*master_pb.ExpandBlockVolumeResponse, error) {
+ if req.Name == "" {
+ return nil, fmt.Errorf("name is required")
+ }
+ if req.NewSizeBytes == 0 {
+ return nil, fmt.Errorf("new_size_bytes must be > 0")
+ }
+
+ entry, ok := ms.blockRegistry.Lookup(req.Name)
+ if !ok {
+ return nil, fmt.Errorf("block volume %q not found", req.Name)
+ }
+
+ // Expand primary first; only update registry on success.
+ capacity, err := ms.blockVSExpand(ctx, entry.VolumeServer, req.Name, req.NewSizeBytes)
+ if err != nil {
+ return nil, fmt.Errorf("expand on %s: %w", entry.VolumeServer, err)
+ }
+
+ // CP8-2: Expand ALL replicas (best-effort, log warning on failure).
+ for _, ri := range entry.Replicas {
+ if _, err := ms.blockVSExpand(ctx, ri.Server, req.Name, req.NewSizeBytes); err != nil {
+ glog.Warningf("[reqID=%s] ExpandBlockVolume %q: replica expand on %s failed (best-effort): %v",
+ blockReqID(ctx), req.Name, ri.Server, err)
+ }
+ }
+
+ // Update registry with actual new size.
+ if uerr := ms.blockRegistry.UpdateSize(req.Name, capacity); uerr != nil {
+ glog.Warningf("[reqID=%s] ExpandBlockVolume %q: registry update failed (VS succeeded): %v", blockReqID(ctx), req.Name, uerr)
+ }
+
+ return &master_pb.ExpandBlockVolumeResponse{
+ CapacityBytes: capacity,
+ }, nil
+}
+
+// createBlockVolumeResponseFromEntry builds a CreateBlockVolumeResponse from a registry entry.
+func (ms *MasterServer) createBlockVolumeResponseFromEntry(entry *BlockVolumeEntry) *master_pb.CreateBlockVolumeResponse {
+ return &master_pb.CreateBlockVolumeResponse{
+ VolumeId: entry.Name,
+ VolumeServer: entry.VolumeServer,
+ IscsiAddr: entry.ISCSIAddr,
+ Iqn: entry.IQN,
+ CapacityBytes: entry.SizeBytes,
+ ReplicaServer: entry.ReplicaServer, // backward compat
+ ReplicaServers: replicaServerList(entry),
+ }
+}
+
+// replicaServerList returns the list of replica server addresses.
+// Order matches Replicas[] (append-order), ensuring ReplicaServers[0] == ReplicaServer (legacy).
+func replicaServerList(entry *BlockVolumeEntry) []string {
+ if len(entry.Replicas) == 0 {
+ return nil
+ }
+ servers := make([]string, len(entry.Replicas))
+ for i, ri := range entry.Replicas {
+ servers[i] = ri.Server
+ }
+ return servers
+}
+
// removeServer returns a new slice without the specified server.
func removeServer(servers []string, server string) []string {
result := make([]string, 0, len(servers)-1)
diff --git a/weed/server/master_grpc_server_block_test.go b/weed/server/master_grpc_server_block_test.go
index 2c932d7641..c7b4f750c2 100644
--- a/weed/server/master_grpc_server_block_test.go
+++ b/weed/server/master_grpc_server_block_test.go
@@ -8,6 +8,7 @@ import (
"testing"
"github.com/seaweedfs/seaweedfs/weed/pb/master_pb"
+ "github.com/seaweedfs/seaweedfs/weed/pb/volume_server_pb"
)
// testMasterServer creates a minimal MasterServer with mock VS calls for testing.
@@ -492,6 +493,142 @@ func TestMaster_LookupReturnsReplicaServer(t *testing.T) {
}
}
+func TestMaster_CreateBlockSnapshot(t *testing.T) {
+ ms := testMasterServer(t)
+ ms.blockRegistry.MarkBlockCapable("vs1:9333")
+ ms.blockVSSnapshot = func(ctx context.Context, server string, name string, snapID uint32) (int64, uint64, error) {
+ return 1709654400, 1 << 30, nil
+ }
+
+ // Create volume first.
+ ms.CreateBlockVolume(context.Background(), &master_pb.CreateBlockVolumeRequest{
+ Name: "snap-vol", SizeBytes: 1 << 30,
+ })
+
+ resp, err := ms.CreateBlockSnapshot(context.Background(), &master_pb.CreateBlockSnapshotRequest{
+ VolumeName: "snap-vol", SnapshotId: 42,
+ })
+ if err != nil {
+ t.Fatalf("CreateBlockSnapshot: %v", err)
+ }
+ if resp.SnapshotId != 42 {
+ t.Fatalf("SnapshotId: got %d, want 42", resp.SnapshotId)
+ }
+ if resp.CreatedAt != 1709654400 {
+ t.Fatalf("CreatedAt: got %d, want 1709654400", resp.CreatedAt)
+ }
+}
+
+func TestMaster_CreateBlockSnapshot_VolumeNotFound(t *testing.T) {
+ ms := testMasterServer(t)
+ _, err := ms.CreateBlockSnapshot(context.Background(), &master_pb.CreateBlockSnapshotRequest{
+ VolumeName: "nonexistent", SnapshotId: 1,
+ })
+ if err == nil {
+ t.Fatal("expected error for nonexistent volume")
+ }
+}
+
+func TestMaster_DeleteBlockSnapshot(t *testing.T) {
+ ms := testMasterServer(t)
+ ms.blockRegistry.MarkBlockCapable("vs1:9333")
+ ms.blockVSDeleteSnap = func(ctx context.Context, server string, name string, snapID uint32) error {
+ return nil
+ }
+
+ ms.CreateBlockVolume(context.Background(), &master_pb.CreateBlockVolumeRequest{
+ Name: "snap-vol", SizeBytes: 1 << 30,
+ })
+
+ _, err := ms.DeleteBlockSnapshot(context.Background(), &master_pb.DeleteBlockSnapshotRequest{
+ VolumeName: "snap-vol", SnapshotId: 42,
+ })
+ if err != nil {
+ t.Fatalf("DeleteBlockSnapshot: %v", err)
+ }
+}
+
+func TestMaster_ListBlockSnapshots(t *testing.T) {
+ ms := testMasterServer(t)
+ ms.blockRegistry.MarkBlockCapable("vs1:9333")
+ ms.blockVSListSnaps = func(ctx context.Context, server string, name string) ([]*volume_server_pb.BlockSnapshotInfo, error) {
+ return []*volume_server_pb.BlockSnapshotInfo{
+ {SnapshotId: 1, CreatedAt: 100},
+ {SnapshotId: 2, CreatedAt: 200},
+ }, nil
+ }
+
+ ms.CreateBlockVolume(context.Background(), &master_pb.CreateBlockVolumeRequest{
+ Name: "snap-vol", SizeBytes: 1 << 30,
+ })
+
+ resp, err := ms.ListBlockSnapshots(context.Background(), &master_pb.ListBlockSnapshotsRequest{
+ VolumeName: "snap-vol",
+ })
+ if err != nil {
+ t.Fatalf("ListBlockSnapshots: %v", err)
+ }
+ if len(resp.Snapshots) != 2 {
+ t.Fatalf("expected 2 snapshots, got %d", len(resp.Snapshots))
+ }
+}
+
+func TestMaster_ExpandBlockVolume(t *testing.T) {
+ ms := testMasterServer(t)
+ ms.blockRegistry.MarkBlockCapable("vs1:9333")
+ ms.blockVSExpand = func(ctx context.Context, server string, name string, newSize uint64) (uint64, error) {
+ return newSize, nil
+ }
+
+ ms.CreateBlockVolume(context.Background(), &master_pb.CreateBlockVolumeRequest{
+ Name: "expand-vol", SizeBytes: 1 << 30,
+ })
+
+ resp, err := ms.ExpandBlockVolume(context.Background(), &master_pb.ExpandBlockVolumeRequest{
+ Name: "expand-vol", NewSizeBytes: 2 << 30,
+ })
+ if err != nil {
+ t.Fatalf("ExpandBlockVolume: %v", err)
+ }
+ if resp.CapacityBytes != 2<<30 {
+ t.Fatalf("CapacityBytes: got %d, want %d", resp.CapacityBytes, 2<<30)
+ }
+
+ // Verify registry was updated.
+ entry, ok := ms.blockRegistry.Lookup("expand-vol")
+ if !ok {
+ t.Fatal("volume not found in registry")
+ }
+ if entry.SizeBytes != 2<<30 {
+ t.Fatalf("registry size: got %d, want %d", entry.SizeBytes, 2<<30)
+ }
+}
+
+func TestMaster_ExpandBlockVolume_VSFailure(t *testing.T) {
+ ms := testMasterServer(t)
+ ms.blockRegistry.MarkBlockCapable("vs1:9333")
+ ms.blockVSExpand = func(ctx context.Context, server string, name string, newSize uint64) (uint64, error) {
+ return 0, fmt.Errorf("disk full")
+ }
+
+ ms.CreateBlockVolume(context.Background(), &master_pb.CreateBlockVolumeRequest{
+ Name: "expand-vol", SizeBytes: 1 << 30,
+ })
+
+ _, err := ms.ExpandBlockVolume(context.Background(), &master_pb.ExpandBlockVolumeRequest{
+ Name: "expand-vol", NewSizeBytes: 2 << 30,
+ })
+ if err == nil {
+ t.Fatal("expected error when VS expand fails")
+ }
+
+ // Registry should NOT have been updated.
+ entry, _ := ms.blockRegistry.Lookup("expand-vol")
+ if entry.SizeBytes != 1<<30 {
+ t.Fatalf("registry size should be unchanged: got %d, want %d", entry.SizeBytes, 1<<30)
+ }
+}
+
func TestMaster_LookupBlockVolume(t *testing.T) {
ms := testMasterServer(t)
ms.blockRegistry.MarkBlockCapable("vs1:9333")
@@ -525,3 +662,354 @@ func TestMaster_LookupBlockVolume(t *testing.T) {
t.Fatal("lookup nonexistent should return error")
}
}
+
+// ============================================================
+// CP8-2 T9: Multi-Replica Create/Delete/Assign Tests
+// ============================================================
+
+func testMasterServerRF3(t *testing.T) *MasterServer {
+ t.Helper()
+ ms := testMasterServer(t)
+ ms.blockVSAllocate = func(ctx context.Context, server string, name string, sizeBytes uint64, diskType string) (*blockAllocResult, error) {
+ return &blockAllocResult{
+ Path: fmt.Sprintf("/data/%s.blk", name),
+ IQN: fmt.Sprintf("iqn.2024.test:%s", name),
+ ISCSIAddr: server + ":3260",
+ ReplicaDataAddr: server + ":14260",
+ ReplicaCtrlAddr: server + ":14261",
+ }, nil
+ }
+ ms.blockRegistry.MarkBlockCapable("vs1:9333")
+ ms.blockRegistry.MarkBlockCapable("vs2:9333")
+ ms.blockRegistry.MarkBlockCapable("vs3:9333")
+ return ms
+}
+
+// RF=3 with 3 servers: should create 2 replicas.
+func TestMaster_CreateRF3_ThreeServers(t *testing.T) {
+ ms := testMasterServerRF3(t)
+
+ resp, err := ms.CreateBlockVolume(context.Background(), &master_pb.CreateBlockVolumeRequest{
+ Name: "vol1",
+ SizeBytes: 1 << 30,
+ ReplicaFactor: 3,
+ })
+ if err != nil {
+ t.Fatalf("CreateBlockVolume: %v", err)
+ }
+
+ entry, ok := ms.blockRegistry.Lookup("vol1")
+ if !ok {
+ t.Fatal("vol1 not in registry")
+ }
+ if entry.ReplicaFactor != 3 {
+ t.Fatalf("ReplicaFactor: got %d, want 3", entry.ReplicaFactor)
+ }
+ if len(entry.Replicas) != 2 {
+ t.Fatalf("expected 2 replicas, got %d", len(entry.Replicas))
+ }
+
+ // All 3 servers should be different.
+ servers := map[string]bool{resp.VolumeServer: true}
+ for _, ri := range entry.Replicas {
+ if servers[ri.Server] {
+ t.Fatalf("duplicate server: %q", ri.Server)
+ }
+ servers[ri.Server] = true
+ }
+ if len(servers) != 3 {
+ t.Fatalf("expected 3 distinct servers, got %d", len(servers))
+ }
+}
+
+// RF=3 with only 2 servers: should create 1 replica (partial).
+func TestMaster_CreateRF3_TwoServers(t *testing.T) {
+ ms := testMasterServer(t)
+ ms.blockVSAllocate = func(ctx context.Context, server string, name string, sizeBytes uint64, diskType string) (*blockAllocResult, error) {
+ return &blockAllocResult{
+ Path: fmt.Sprintf("/data/%s.blk", name),
+ IQN: fmt.Sprintf("iqn.2024.test:%s", name),
+ ISCSIAddr: server + ":3260",
+ ReplicaDataAddr: server + ":14260",
+ ReplicaCtrlAddr: server + ":14261",
+ }, nil
+ }
+ ms.blockRegistry.MarkBlockCapable("vs1:9333")
+ ms.blockRegistry.MarkBlockCapable("vs2:9333")
+
+ _, err := ms.CreateBlockVolume(context.Background(), &master_pb.CreateBlockVolumeRequest{
+ Name: "vol1",
+ SizeBytes: 1 << 30,
+ ReplicaFactor: 3,
+ })
+ if err != nil {
+ t.Fatalf("CreateBlockVolume: %v", err)
+ }
+
+ entry, _ := ms.blockRegistry.Lookup("vol1")
+ // Only 2 servers available, so only 1 replica (not 2).
+ if len(entry.Replicas) != 1 {
+ t.Fatalf("expected 1 replica (only 2 servers), got %d", len(entry.Replicas))
+ }
+}
+
+// RF=2 unchanged after CP8-2 changes.
+func TestMaster_CreateRF2_Unchanged(t *testing.T) {
+ ms := testMasterServerRF3(t)
+
+ _, err := ms.CreateBlockVolume(context.Background(), &master_pb.CreateBlockVolumeRequest{
+ Name: "vol1",
+ SizeBytes: 1 << 30,
+ // No ReplicaFactor → defaults to 2.
+ })
+ if err != nil {
+ t.Fatalf("CreateBlockVolume: %v", err)
+ }
+
+ entry, _ := ms.blockRegistry.Lookup("vol1")
+ if entry.ReplicaFactor != 2 {
+ t.Fatalf("ReplicaFactor: got %d, want 2 (default)", entry.ReplicaFactor)
+ }
+ if len(entry.Replicas) != 1 {
+ t.Fatalf("expected 1 replica for RF=2, got %d", len(entry.Replicas))
+ }
+ // Backward compat scalar fields should be set.
+ if entry.ReplicaServer == "" {
+ t.Fatal("ReplicaServer (deprecated) should still be set for backward compat")
+ }
+}
+
+// DeleteBlockVolume RF=3 deletes all replicas.
+func TestMaster_DeleteRF3_DeletesAllReplicas(t *testing.T) {
+ ms := testMasterServerRF3(t)
+
+ var deletedServers []string
+ ms.blockVSDelete = func(ctx context.Context, server string, name string) error {
+ deletedServers = append(deletedServers, server)
+ return nil
+ }
+
+ _, err := ms.CreateBlockVolume(context.Background(), &master_pb.CreateBlockVolumeRequest{
+ Name: "vol1",
+ SizeBytes: 1 << 30,
+ ReplicaFactor: 3,
+ })
+ if err != nil {
+ t.Fatalf("create: %v", err)
+ }
+
+ _, err = ms.DeleteBlockVolume(context.Background(), &master_pb.DeleteBlockVolumeRequest{
+ Name: "vol1",
+ })
+ if err != nil {
+ t.Fatalf("delete: %v", err)
+ }
+
+ // Should have deleted on primary + 2 replicas = 3 servers.
+ if len(deletedServers) != 3 {
+ t.Fatalf("expected 3 delete calls (primary + 2 replicas), got %d: %v",
+ len(deletedServers), deletedServers)
+ }
+}
+
+// ExpandBlockVolume RF=3 expands all replicas.
+func TestMaster_ExpandRF3_ExpandsAllReplicas(t *testing.T) {
+ ms := testMasterServerRF3(t)
+
+ var expandedServers []string
+ ms.blockVSExpand = func(ctx context.Context, server string, name string, newSize uint64) (uint64, error) {
+ expandedServers = append(expandedServers, server)
+ return newSize, nil
+ }
+
+ _, err := ms.CreateBlockVolume(context.Background(), &master_pb.CreateBlockVolumeRequest{
+ Name: "vol1",
+ SizeBytes: 1 << 30,
+ ReplicaFactor: 3,
+ })
+ if err != nil {
+ t.Fatalf("create: %v", err)
+ }
+
+ _, err = ms.ExpandBlockVolume(context.Background(), &master_pb.ExpandBlockVolumeRequest{
+ Name: "vol1", NewSizeBytes: 2 << 30,
+ })
+ if err != nil {
+ t.Fatalf("expand: %v", err)
+ }
+
+ // Should have expanded on primary + 2 replicas = 3 servers.
+ if len(expandedServers) != 3 {
+ t.Fatalf("expected 3 expand calls (primary + 2 replicas), got %d: %v",
+ len(expandedServers), expandedServers)
+ }
+}
+
+// ProcessAssignments with multi-replica addrs.
+func TestMaster_CreateRF3_AssignmentsIncludeReplicaAddrs(t *testing.T) {
+ ms := testMasterServerRF3(t)
+
+ resp, err := ms.CreateBlockVolume(context.Background(), &master_pb.CreateBlockVolumeRequest{
+ Name: "vol1",
+ SizeBytes: 1 << 30,
+ ReplicaFactor: 3,
+ })
+ if err != nil {
+ t.Fatalf("create: %v", err)
+ }
+
+ // Primary assignment should have ReplicaAddrs with 2 entries.
+ assignments := ms.blockAssignmentQueue.Peek(resp.VolumeServer)
+ if len(assignments) != 1 {
+ t.Fatalf("expected 1 primary assignment, got %d", len(assignments))
+ }
+ pa := assignments[0]
+ if len(pa.ReplicaAddrs) != 2 {
+ t.Fatalf("primary assignment ReplicaAddrs: got %d, want 2", len(pa.ReplicaAddrs))
+ }
+ // Each replica addr should have non-empty data/ctrl.
+ for i, ra := range pa.ReplicaAddrs {
+ if ra.DataAddr == "" || ra.CtrlAddr == "" {
+ t.Fatalf("ReplicaAddrs[%d] has empty addr: %+v", i, ra)
+ }
+ }
+}
+
+// Fix #4: CreateBlockVolumeResponse includes ReplicaServers.
+func TestMaster_CreateResponse_IncludesReplicaServers(t *testing.T) {
+ ms := testMasterServerRF3(t)
+ resp, err := ms.CreateBlockVolume(context.Background(), &master_pb.CreateBlockVolumeRequest{
+ Name: "vol1",
+ SizeBytes: 1 << 30,
+ ReplicaFactor: 3,
+ })
+ if err != nil {
+ t.Fatalf("CreateBlockVolume: %v", err)
+ }
+ if len(resp.ReplicaServers) != 2 {
+ t.Fatalf("ReplicaServers: got %d, want 2", len(resp.ReplicaServers))
+ }
+ // ReplicaServers should match registry entry Replicas.
+ entry, _ := ms.blockRegistry.Lookup("vol1")
+ for i, ri := range entry.Replicas {
+ if resp.ReplicaServers[i] != ri.Server {
+ t.Errorf("ReplicaServers[%d]: got %q, want %q", i, resp.ReplicaServers[i], ri.Server)
+ }
+ }
+ // Backward compat: ReplicaServer (scalar) should be first replica.
+ if resp.ReplicaServer != entry.ReplicaServer {
+ t.Errorf("ReplicaServer: got %q, want %q", resp.ReplicaServer, entry.ReplicaServer)
+ }
+}
+
+// Fix #4: LookupBlockVolumeResponse includes ReplicaFactor and ReplicaServers.
+func TestMaster_LookupResponse_IncludesReplicaFields(t *testing.T) {
+ ms := testMasterServerRF3(t)
+ _, err := ms.CreateBlockVolume(context.Background(), &master_pb.CreateBlockVolumeRequest{
+ Name: "vol1",
+ SizeBytes: 1 << 30,
+ ReplicaFactor: 3,
+ })
+ if err != nil {
+ t.Fatalf("CreateBlockVolume: %v", err)
+ }
+ resp, err := ms.LookupBlockVolume(context.Background(), &master_pb.LookupBlockVolumeRequest{
+ Name: "vol1",
+ })
+ if err != nil {
+ t.Fatalf("LookupBlockVolume: %v", err)
+ }
+ if resp.ReplicaFactor != 3 {
+ t.Fatalf("ReplicaFactor: got %d, want 3", resp.ReplicaFactor)
+ }
+ if len(resp.ReplicaServers) != 2 {
+ t.Fatalf("ReplicaServers: got %d, want 2", len(resp.ReplicaServers))
+ }
+ // Backward compat: ReplicaServer (scalar).
+ if resp.ReplicaServer == "" {
+ t.Error("ReplicaServer (scalar) should be non-empty for backward compat")
+ }
+}
+
+// Fix #4: Idempotent create returns ReplicaServers.
+func TestMaster_CreateIdempotent_IncludesReplicaServers(t *testing.T) {
+ ms := testMasterServerRF3(t)
+ _, err := ms.CreateBlockVolume(context.Background(), &master_pb.CreateBlockVolumeRequest{
+ Name: "vol1",
+ SizeBytes: 1 << 30,
+ ReplicaFactor: 3,
+ })
+ if err != nil {
+ t.Fatalf("first create: %v", err)
+ }
+ // Second create (idempotent) should also include ReplicaServers.
+ resp, err := ms.CreateBlockVolume(context.Background(), &master_pb.CreateBlockVolumeRequest{
+ Name: "vol1",
+ SizeBytes: 1 << 30,
+ ReplicaFactor: 3,
+ })
+ if err != nil {
+ t.Fatalf("idempotent create: %v", err)
+ }
+ if len(resp.ReplicaServers) != 2 {
+ t.Fatalf("idempotent ReplicaServers: got %d, want 2", len(resp.ReplicaServers))
+ }
+}
+
+// Lookup returns ReplicaFactor=2 for pre-CP8-2 entries where ReplicaFactor is 0.
+func TestMaster_LookupResponse_ReplicaFactorDefault(t *testing.T) {
+ ms := testMasterServer(t)
+ ms.blockRegistry.MarkBlockCapable("vs1:9333")
+
+ // Manually register entry with ReplicaFactor=0 (pre-CP8-2 legacy).
+ ms.blockRegistry.Register(&BlockVolumeEntry{
+ Name: "legacy-vol",
+ VolumeServer: "vs1:9333",
+ Path: "/data/legacy-vol.blk",
+ SizeBytes: 1 << 30,
+ Status: StatusActive,
+ // ReplicaFactor intentionally 0 (zero value).
+ })
+
+ resp, err := ms.LookupBlockVolume(context.Background(), &master_pb.LookupBlockVolumeRequest{
+ Name: "legacy-vol",
+ })
+ if err != nil {
+ t.Fatalf("LookupBlockVolume: %v", err)
+ }
+ if resp.ReplicaFactor != 2 {
+ t.Fatalf("ReplicaFactor: got %d, want 2 (default for pre-CP8-2)", resp.ReplicaFactor)
+ }
+}
+
+// ReplicaServers[0] matches ReplicaServer (legacy scalar).
+func TestMaster_ResponseConsistency_ReplicaServerVsReplicaServers(t *testing.T) {
+ ms := testMasterServerRF3(t)
+ resp, err := ms.CreateBlockVolume(context.Background(), &master_pb.CreateBlockVolumeRequest{
+ Name: "vol1",
+ SizeBytes: 1 << 30,
+ ReplicaFactor: 3,
+ })
+ if err != nil {
+ t.Fatalf("CreateBlockVolume: %v", err)
+ }
+ if len(resp.ReplicaServers) < 1 {
+ t.Fatal("expected at least 1 replica server")
+ }
+ if resp.ReplicaServer != resp.ReplicaServers[0] {
+ t.Fatalf("ReplicaServer=%q != ReplicaServers[0]=%q",
+ resp.ReplicaServer, resp.ReplicaServers[0])
+ }
+
+ // Same for Lookup.
+ lresp, err := ms.LookupBlockVolume(context.Background(), &master_pb.LookupBlockVolumeRequest{
+ Name: "vol1",
+ })
+ if err != nil {
+ t.Fatalf("LookupBlockVolume: %v", err)
+ }
+ if lresp.ReplicaServer != lresp.ReplicaServers[0] {
+ t.Fatalf("Lookup: ReplicaServer=%q != ReplicaServers[0]=%q",
+ lresp.ReplicaServer, lresp.ReplicaServers[0])
+ }
+}
diff --git a/weed/server/master_server.go b/weed/server/master_server.go
index 88c67ae996..35d1766e45 100644
--- a/weed/server/master_server.go
+++ b/weed/server/master_server.go
@@ -60,9 +60,10 @@ type MasterOption struct {
MetricsAddress string
MetricsIntervalSec int
IsFollower bool
- TelemetryUrl string
- TelemetryEnabled bool
- VolumeGrowthDisabled bool
+ TelemetryUrl string
+ TelemetryEnabled bool
+ VolumeGrowthDisabled bool
+ BlockPromotionLSNTolerance int
}
type MasterServer struct {
@@ -97,8 +98,12 @@ type MasterServer struct {
blockRegistry *BlockVolumeRegistry
blockAssignmentQueue *BlockAssignmentQueue
blockFailover *blockFailoverState
- blockVSAllocate func(ctx context.Context, server string, name string, sizeBytes uint64, diskType string) (*blockAllocResult, error)
- blockVSDelete func(ctx context.Context, server string, name string) error
+ blockVSAllocate func(ctx context.Context, server string, name string, sizeBytes uint64, diskType string) (*blockAllocResult, error)
+ blockVSDelete func(ctx context.Context, server string, name string) error
+ blockVSSnapshot func(ctx context.Context, server string, name string, snapID uint32) (int64, uint64, error)
+ blockVSDeleteSnap func(ctx context.Context, server string, name string, snapID uint32) error
+ blockVSListSnaps func(ctx context.Context, server string, name string) ([]*volume_server_pb.BlockSnapshotInfo, error)
+ blockVSExpand func(ctx context.Context, server string, name string, newSize uint64) (uint64, error)
}
func NewMasterServer(r *mux.Router, option *MasterOption, peers map[string]pb.ServerAddress) *MasterServer {
@@ -148,10 +153,17 @@ func NewMasterServer(r *mux.Router, option *MasterOption, peers map[string]pb.Se
}
ms.blockRegistry = NewBlockVolumeRegistry()
+ if option.BlockPromotionLSNTolerance > 0 {
+ ms.blockRegistry.SetPromotionLSNTolerance(uint64(option.BlockPromotionLSNTolerance))
+ }
ms.blockAssignmentQueue = NewBlockAssignmentQueue()
ms.blockFailover = newBlockFailoverState()
ms.blockVSAllocate = ms.defaultBlockVSAllocate
ms.blockVSDelete = ms.defaultBlockVSDelete
+ ms.blockVSSnapshot = ms.defaultBlockVSSnapshot
+ ms.blockVSDeleteSnap = ms.defaultBlockVSDeleteSnap
+ ms.blockVSListSnaps = ms.defaultBlockVSListSnaps
+ ms.blockVSExpand = ms.defaultBlockVSExpand
ms.MasterClient.SetOnPeerUpdateFn(ms.OnPeerUpdate)
@@ -197,6 +209,18 @@ func NewMasterServer(r *mux.Router, option *MasterOption, peers map[string]pb.Se
r.HandleFunc("/stats/counter", ms.guard.WhiteList(statsCounterHandler))
r.HandleFunc("/stats/memory", ms.guard.WhiteList(statsMemoryHandler))
*/
+
+ // Block volume management routes.
+ r.HandleFunc("/block/volume", ms.proxyToLeader(ms.guard.WhiteList(requestIDMiddleware(ms.blockVolumeCreateHandler)))).Methods("POST")
+ r.HandleFunc("/block/volume/{name}", ms.proxyToLeader(ms.guard.WhiteList(requestIDMiddleware(ms.blockVolumeDeleteHandler)))).Methods("DELETE")
+ r.HandleFunc("/block/volume/{name}", ms.guard.WhiteList(requestIDMiddleware(ms.blockVolumeLookupHandler))).Methods("GET")
+ r.HandleFunc("/block/volumes", ms.guard.WhiteList(requestIDMiddleware(ms.blockVolumeListHandler))).Methods("GET")
+ r.HandleFunc("/block/assign", ms.proxyToLeader(ms.guard.WhiteList(requestIDMiddleware(ms.blockAssignHandler)))).Methods("POST")
+ r.HandleFunc("/block/servers", ms.guard.WhiteList(requestIDMiddleware(ms.blockServersHandler))).Methods("GET")
+ r.HandleFunc("/block/status", ms.guard.WhiteList(requestIDMiddleware(ms.blockStatusHandler))).Methods("GET")
+ r.HandleFunc("/block/ops", requestIDMiddleware(ms.blockOpsHandler))
+ r.HandleFunc("/block/", requestIDMiddleware(ms.blockUIHandler))
+
r.HandleFunc("/{fileId}", requestIDMiddleware(ms.redirectHandler))
}
@@ -560,3 +584,62 @@ func (ms *MasterServer) defaultBlockVSDelete(ctx context.Context, server string,
return err
})
}
+
+func (ms *MasterServer) defaultBlockVSSnapshot(ctx context.Context, server string, name string, snapID uint32) (int64, uint64, error) {
+ var createdAt int64
+ var sizeBytes uint64
+ err := operation.WithVolumeServerClient(false, pb.ServerAddress(server), ms.grpcDialOption, func(client volume_server_pb.VolumeServerClient) error {
+ resp, rerr := client.SnapshotBlockVolume(ctx, &volume_server_pb.SnapshotBlockVolumeRequest{
+ Name: name,
+ SnapshotId: snapID,
+ })
+ if rerr != nil {
+ return rerr
+ }
+ createdAt = resp.CreatedAt
+ sizeBytes = resp.SizeBytes
+ return nil
+ })
+ return createdAt, sizeBytes, err
+}
+
+func (ms *MasterServer) defaultBlockVSDeleteSnap(ctx context.Context, server string, name string, snapID uint32) error {
+ return operation.WithVolumeServerClient(false, pb.ServerAddress(server), ms.grpcDialOption, func(client volume_server_pb.VolumeServerClient) error {
+ _, err := client.DeleteBlockSnapshot(ctx, &volume_server_pb.DeleteBlockSnapshotRequest{
+ Name: name,
+ SnapshotId: snapID,
+ })
+ return err
+ })
+}
+
+func (ms *MasterServer) defaultBlockVSListSnaps(ctx context.Context, server string, name string) ([]*volume_server_pb.BlockSnapshotInfo, error) {
+ var infos []*volume_server_pb.BlockSnapshotInfo
+ err := operation.WithVolumeServerClient(false, pb.ServerAddress(server), ms.grpcDialOption, func(client volume_server_pb.VolumeServerClient) error {
+ resp, rerr := client.ListBlockSnapshots(ctx, &volume_server_pb.ListBlockSnapshotsRequest{
+ Name: name,
+ })
+ if rerr != nil {
+ return rerr
+ }
+ infos = resp.Snapshots
+ return nil
+ })
+ return infos, err
+}
+
+func (ms *MasterServer) defaultBlockVSExpand(ctx context.Context, server string, name string, newSize uint64) (uint64, error) {
+ var capacity uint64
+ err := operation.WithVolumeServerClient(false, pb.ServerAddress(server), ms.grpcDialOption, func(client volume_server_pb.VolumeServerClient) error {
+ resp, rerr := client.ExpandBlockVolume(ctx, &volume_server_pb.ExpandBlockVolumeRequest{
+ Name: name,
+ NewSizeBytes: newSize,
+ })
+ if rerr != nil {
+ return rerr
+ }
+ capacity = resp.CapacityBytes
+ return nil
+ })
+ return capacity, err
+}
diff --git a/weed/server/master_server_handlers_block.go b/weed/server/master_server_handlers_block.go
new file mode 100644
index 0000000000..7d1285724a
--- /dev/null
+++ b/weed/server/master_server_handlers_block.go
@@ -0,0 +1,209 @@
+package weed_server
+
+import (
+ "encoding/json"
+ "fmt"
+ "net/http"
+
+ "github.com/gorilla/mux"
+
+ "github.com/seaweedfs/seaweedfs/weed/pb/master_pb"
+ "github.com/seaweedfs/seaweedfs/weed/storage/blockvol"
+ "github.com/seaweedfs/seaweedfs/weed/storage/blockvol/blockapi"
+)
+
+// blockVolumeCreateHandler handles POST /block/volume.
+func (ms *MasterServer) blockVolumeCreateHandler(w http.ResponseWriter, r *http.Request) {
+ var req blockapi.CreateVolumeRequest
+ if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
+ writeJsonError(w, r, http.StatusBadRequest, fmt.Errorf("invalid request body: %w", err))
+ return
+ }
+
+ // Store replica_placement in registry after creation.
+ replicaPlacement := req.ReplicaPlacement
+ if replicaPlacement == "" {
+ replicaPlacement = "000"
+ }
+
+ resp, err := ms.CreateBlockVolume(r.Context(), &master_pb.CreateBlockVolumeRequest{
+ Name: req.Name,
+ SizeBytes: req.SizeBytes,
+ DiskType: req.DiskType,
+ })
+ if err != nil {
+ writeJsonError(w, r, http.StatusInternalServerError, err)
+ return
+ }
+
+ // Store replica_placement on the registry entry.
+ if entry, ok := ms.blockRegistry.Lookup(resp.VolumeId); ok {
+ entry.ReplicaPlacement = replicaPlacement
+ }
+
+ // Look up the full entry to populate all fields.
+ info := blockapi.VolumeInfo{
+ Name: resp.VolumeId,
+ VolumeServer: resp.VolumeServer,
+ SizeBytes: resp.CapacityBytes,
+ ReplicaPlacement: replicaPlacement,
+ ISCSIAddr: resp.IscsiAddr,
+ IQN: resp.Iqn,
+ }
+ if entry, ok := ms.blockRegistry.Lookup(resp.VolumeId); ok {
+ info = entryToVolumeInfo(entry)
+ }
+ writeJsonQuiet(w, r, http.StatusOK, info)
+}
+
+// blockVolumeDeleteHandler handles DELETE /block/volume/{name}.
+func (ms *MasterServer) blockVolumeDeleteHandler(w http.ResponseWriter, r *http.Request) {
+ name := mux.Vars(r)["name"]
+ if name == "" {
+ writeJsonError(w, r, http.StatusBadRequest, fmt.Errorf("name is required"))
+ return
+ }
+
+ _, err := ms.DeleteBlockVolume(r.Context(), &master_pb.DeleteBlockVolumeRequest{
+ Name: name,
+ })
+ if err != nil {
+ writeJsonError(w, r, http.StatusInternalServerError, err)
+ return
+ }
+ writeJsonQuiet(w, r, http.StatusOK, map[string]string{"status": "deleted"})
+}
+
+// blockVolumeLookupHandler handles GET /block/volume/{name}.
+func (ms *MasterServer) blockVolumeLookupHandler(w http.ResponseWriter, r *http.Request) {
+ name := mux.Vars(r)["name"]
+ if name == "" {
+ writeJsonError(w, r, http.StatusBadRequest, fmt.Errorf("name is required"))
+ return
+ }
+
+ entry, ok := ms.blockRegistry.Lookup(name)
+ if !ok {
+ writeJsonError(w, r, http.StatusNotFound, fmt.Errorf("block volume %q not found", name))
+ return
+ }
+ writeJsonQuiet(w, r, http.StatusOK, entryToVolumeInfo(entry))
+}
+
+// blockVolumeListHandler handles GET /block/volumes.
+func (ms *MasterServer) blockVolumeListHandler(w http.ResponseWriter, r *http.Request) {
+ entries := ms.blockRegistry.ListAll()
+ infos := make([]blockapi.VolumeInfo, len(entries))
+ for i, e := range entries {
+ infos[i] = entryToVolumeInfo(e)
+ }
+ writeJsonQuiet(w, r, http.StatusOK, infos)
+}
+
+// blockAssignHandler handles POST /block/assign.
+func (ms *MasterServer) blockAssignHandler(w http.ResponseWriter, r *http.Request) {
+ var req blockapi.AssignRequest
+ if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
+ writeJsonError(w, r, http.StatusBadRequest, fmt.Errorf("invalid request body: %w", err))
+ return
+ }
+ if req.Name == "" {
+ writeJsonError(w, r, http.StatusBadRequest, fmt.Errorf("name is required"))
+ return
+ }
+
+ // Resolve name → registry entry.
+ entry, ok := ms.blockRegistry.Lookup(req.Name)
+ if !ok {
+ writeJsonError(w, r, http.StatusNotFound, fmt.Errorf("block volume %q not found", req.Name))
+ return
+ }
+
+ // Determine target server + path based on role.
+ server, path := entry.VolumeServer, entry.Path
+ if req.Role == "replica" {
+ if entry.ReplicaServer == "" {
+ writeJsonError(w, r, http.StatusBadRequest, fmt.Errorf("block volume %q has no replica", req.Name))
+ return
+ }
+ server, path = entry.ReplicaServer, entry.ReplicaPath
+ }
+
+ ms.blockAssignmentQueue.Enqueue(server, blockvol.BlockVolumeAssignment{
+ Path: path,
+ Epoch: req.Epoch,
+ Role: blockapi.RoleFromString(req.Role),
+ LeaseTtlMs: uint32(req.LeaseTTLMs),
+ })
+ writeJsonQuiet(w, r, http.StatusOK, map[string]string{"status": "queued"})
+}
+
+// blockServersHandler handles GET /block/servers.
+func (ms *MasterServer) blockServersHandler(w http.ResponseWriter, r *http.Request) {
+ summaries := ms.blockRegistry.ServerSummaries()
+ infos := make([]blockapi.ServerInfo, len(summaries))
+ for i, s := range summaries {
+ infos[i] = blockapi.ServerInfo{
+ Address: s.Address,
+ VolumeCount: s.VolumeCount,
+ BlockCapable: s.BlockCapable,
+ }
+ }
+ writeJsonQuiet(w, r, http.StatusOK, infos)
+}
+
+// blockStatusHandler handles GET /block/status — returns registry configuration for debugging.
+func (ms *MasterServer) blockStatusHandler(w http.ResponseWriter, r *http.Request) {
+ status := map[string]interface{}{
+ "promotion_lsn_tolerance": ms.blockRegistry.PromotionLSNTolerance(),
+ "volume_count": len(ms.blockRegistry.ListAll()),
+ "server_count": len(ms.blockRegistry.BlockCapableServers()),
+ "barrier_lag_lsn": ms.blockRegistry.MaxBarrierLagLSN(),
+ "promotions_total": ms.blockRegistry.PromotionsTotal.Load(),
+ "failovers_total": ms.blockRegistry.FailoversTotal.Load(),
+ "rebuilds_total": ms.blockRegistry.RebuildsTotal.Load(),
+ "assignment_queue_depth": ms.blockAssignmentQueue.TotalPending(),
+ }
+ writeJsonQuiet(w, r, http.StatusOK, status)
+}
+
+// entryToVolumeInfo converts a BlockVolumeEntry to a blockapi.VolumeInfo.
+func entryToVolumeInfo(e *BlockVolumeEntry) blockapi.VolumeInfo {
+ status := "pending"
+ if e.Status == StatusActive {
+ status = "active"
+ }
+ rf := e.ReplicaFactor
+ if rf == 0 {
+ rf = 2 // default
+ }
+ info := blockapi.VolumeInfo{
+ Name: e.Name,
+ VolumeServer: e.VolumeServer,
+ SizeBytes: e.SizeBytes,
+ ReplicaPlacement: e.ReplicaPlacement,
+ Epoch: e.Epoch,
+ Role: blockvol.RoleFromWire(e.Role).String(),
+ Status: status,
+ ISCSIAddr: e.ISCSIAddr,
+ IQN: e.IQN,
+ ReplicaServer: e.ReplicaServer,
+ ReplicaISCSIAddr: e.ReplicaISCSIAddr,
+ ReplicaIQN: e.ReplicaIQN,
+ ReplicaDataAddr: e.ReplicaDataAddr,
+ ReplicaCtrlAddr: e.ReplicaCtrlAddr,
+ ReplicaFactor: rf,
+ HealthScore: e.HealthScore,
+ ReplicaDegraded: e.ReplicaDegraded,
+ }
+ for _, ri := range e.Replicas {
+ info.Replicas = append(info.Replicas, blockapi.ReplicaDetail{
+ Server: ri.Server,
+ ISCSIAddr: ri.ISCSIAddr,
+ IQN: ri.IQN,
+ HealthScore: ri.HealthScore,
+ WALLag: ri.WALLag,
+ })
+ }
+ return info
+}
diff --git a/weed/server/master_server_handlers_block_test.go b/weed/server/master_server_handlers_block_test.go
new file mode 100644
index 0000000000..c2f3096506
--- /dev/null
+++ b/weed/server/master_server_handlers_block_test.go
@@ -0,0 +1,288 @@
+package weed_server
+
+import (
+ "bytes"
+ "context"
+ "encoding/json"
+ "fmt"
+ "net/http"
+ "net/http/httptest"
+ "testing"
+
+ "github.com/gorilla/mux"
+ "github.com/seaweedfs/seaweedfs/weed/storage/blockvol/blockapi"
+)
+
+// blockTestServer creates a MasterServer + httptest.Server with block routes registered.
+func blockTestServer(t *testing.T) (*MasterServer, *httptest.Server) {
+ t.Helper()
+ ms := &MasterServer{
+ blockRegistry: NewBlockVolumeRegistry(),
+ blockAssignmentQueue: NewBlockAssignmentQueue(),
+ }
+ ms.blockVSAllocate = func(ctx context.Context, server string, name string, sizeBytes uint64, diskType string) (*blockAllocResult, error) {
+ return &blockAllocResult{
+ Path: fmt.Sprintf("/data/%s.blk", name),
+ IQN: fmt.Sprintf("iqn.2024.test:%s", name),
+ ISCSIAddr: server + ":3260",
+ }, nil
+ }
+ ms.blockVSDelete = func(ctx context.Context, server string, name string) error {
+ return nil
+ }
+ ms.blockRegistry.MarkBlockCapable("vs1:9333")
+
+ r := mux.NewRouter()
+ r.HandleFunc("/block/volume", ms.blockVolumeCreateHandler).Methods("POST")
+ r.HandleFunc("/block/volume/{name}", ms.blockVolumeDeleteHandler).Methods("DELETE")
+ r.HandleFunc("/block/volume/{name}", ms.blockVolumeLookupHandler).Methods("GET")
+ r.HandleFunc("/block/volumes", ms.blockVolumeListHandler).Methods("GET")
+ r.HandleFunc("/block/assign", ms.blockAssignHandler).Methods("POST")
+ r.HandleFunc("/block/servers", ms.blockServersHandler).Methods("GET")
+
+ ts := httptest.NewServer(r)
+ t.Cleanup(ts.Close)
+ return ms, ts
+}
+
+func TestBlockVolumeCreateHandler(t *testing.T) {
+ _, ts := blockTestServer(t)
+
+ body, _ := json.Marshal(blockapi.CreateVolumeRequest{
+ Name: "vol1",
+ SizeBytes: 1 << 30,
+ })
+ resp, err := http.Post(ts.URL+"/block/volume", "application/json", bytes.NewReader(body))
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer resp.Body.Close()
+ if resp.StatusCode != http.StatusOK {
+ t.Fatalf("expected 200, got %d", resp.StatusCode)
+ }
+
+ var info blockapi.VolumeInfo
+ if err := json.NewDecoder(resp.Body).Decode(&info); err != nil {
+ t.Fatal(err)
+ }
+ if info.Name != "vol1" {
+ t.Errorf("expected name vol1, got %s", info.Name)
+ }
+ if info.VolumeServer == "" {
+ t.Error("expected non-empty volume server")
+ }
+}
+
+func TestBlockVolumeListHandler(t *testing.T) {
+ ms, ts := blockTestServer(t)
+
+ // Create two volumes via gRPC.
+ for _, name := range []string{"alpha", "beta"} {
+ ms.blockRegistry.Register(&BlockVolumeEntry{
+ Name: name,
+ VolumeServer: "vs1:9333",
+ SizeBytes: 1 << 20,
+ Status: StatusActive,
+ })
+ }
+
+ resp, err := http.Get(ts.URL + "/block/volumes")
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer resp.Body.Close()
+ if resp.StatusCode != http.StatusOK {
+ t.Fatalf("expected 200, got %d", resp.StatusCode)
+ }
+
+ var infos []blockapi.VolumeInfo
+ if err := json.NewDecoder(resp.Body).Decode(&infos); err != nil {
+ t.Fatal(err)
+ }
+ if len(infos) != 2 {
+ t.Fatalf("expected 2 volumes, got %d", len(infos))
+ }
+ if infos[0].Name != "alpha" || infos[1].Name != "beta" {
+ t.Errorf("expected sorted [alpha, beta], got [%s, %s]", infos[0].Name, infos[1].Name)
+ }
+}
+
+func TestBlockVolumeLookupHandler(t *testing.T) {
+ ms, ts := blockTestServer(t)
+
+ ms.blockRegistry.Register(&BlockVolumeEntry{
+ Name: "vol1",
+ VolumeServer: "vs1:9333",
+ SizeBytes: 1 << 30,
+ Epoch: 3,
+ Role: 1, // RolePrimary
+ Status: StatusActive,
+ ReplicaServer: "vs2:9333",
+ })
+
+ resp, err := http.Get(ts.URL + "/block/volume/vol1")
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer resp.Body.Close()
+ if resp.StatusCode != http.StatusOK {
+ t.Fatalf("expected 200, got %d", resp.StatusCode)
+ }
+
+ var info blockapi.VolumeInfo
+ if err := json.NewDecoder(resp.Body).Decode(&info); err != nil {
+ t.Fatal(err)
+ }
+ if info.Epoch != 3 {
+ t.Errorf("expected epoch 3, got %d", info.Epoch)
+ }
+ if info.Role != "primary" {
+ t.Errorf("expected role primary, got %s", info.Role)
+ }
+ if info.ReplicaServer != "vs2:9333" {
+ t.Errorf("expected replica vs2:9333, got %s", info.ReplicaServer)
+ }
+
+ // Not found case.
+ resp2, err := http.Get(ts.URL + "/block/volume/nonexistent")
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer resp2.Body.Close()
+ if resp2.StatusCode != http.StatusNotFound {
+ t.Fatalf("expected 404, got %d", resp2.StatusCode)
+ }
+}
+
+func TestBlockVolumeDeleteHandler(t *testing.T) {
+ ms, ts := blockTestServer(t)
+
+ ms.blockRegistry.Register(&BlockVolumeEntry{
+ Name: "vol1",
+ VolumeServer: "vs1:9333",
+ SizeBytes: 1 << 30,
+ Status: StatusActive,
+ })
+
+ req, _ := http.NewRequest("DELETE", ts.URL+"/block/volume/vol1", nil)
+ resp, err := http.DefaultClient.Do(req)
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer resp.Body.Close()
+ if resp.StatusCode != http.StatusOK {
+ t.Fatalf("expected 200, got %d", resp.StatusCode)
+ }
+
+ // Verify it's gone.
+ if _, ok := ms.blockRegistry.Lookup("vol1"); ok {
+ t.Error("expected vol1 to be unregistered")
+ }
+}
+
+func TestBlockAssignHandler(t *testing.T) {
+ ms, ts := blockTestServer(t)
+
+ ms.blockRegistry.Register(&BlockVolumeEntry{
+ Name: "vol1",
+ VolumeServer: "vs1:9333",
+ Path: "/data/vol1.blk",
+ SizeBytes: 1 << 30,
+ Status: StatusActive,
+ })
+
+ body, _ := json.Marshal(blockapi.AssignRequest{
+ Name: "vol1",
+ Epoch: 5,
+ Role: "primary",
+ LeaseTTLMs: 30000,
+ })
+ resp, err := http.Post(ts.URL+"/block/assign", "application/json", bytes.NewReader(body))
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer resp.Body.Close()
+ if resp.StatusCode != http.StatusOK {
+ t.Fatalf("expected 200, got %d", resp.StatusCode)
+ }
+
+ // Verify assignment was queued.
+ pending := ms.blockAssignmentQueue.Peek("vs1:9333")
+ if len(pending) != 1 {
+ t.Fatalf("expected 1 pending assignment, got %d", len(pending))
+ }
+ if pending[0].Epoch != 5 {
+ t.Errorf("expected epoch 5, got %d", pending[0].Epoch)
+ }
+
+ // Not-found case.
+ body2, _ := json.Marshal(blockapi.AssignRequest{Name: "missing", Epoch: 1, Role: "primary"})
+ resp2, err := http.Post(ts.URL+"/block/assign", "application/json", bytes.NewReader(body2))
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer resp2.Body.Close()
+ if resp2.StatusCode != http.StatusNotFound {
+ t.Fatalf("expected 404, got %d", resp2.StatusCode)
+ }
+}
+
+func TestBlockServersHandler(t *testing.T) {
+ _, ts := blockTestServer(t)
+
+ resp, err := http.Get(ts.URL + "/block/servers")
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer resp.Body.Close()
+ if resp.StatusCode != http.StatusOK {
+ t.Fatalf("expected 200, got %d", resp.StatusCode)
+ }
+
+ var servers []blockapi.ServerInfo
+ if err := json.NewDecoder(resp.Body).Decode(&servers); err != nil {
+ t.Fatal(err)
+ }
+ if len(servers) != 1 {
+ t.Fatalf("expected 1 server, got %d", len(servers))
+ }
+ if servers[0].Address != "vs1:9333" {
+ t.Errorf("expected vs1:9333, got %s", servers[0].Address)
+ }
+}
+
+func TestListAll(t *testing.T) {
+ r := NewBlockVolumeRegistry()
+ r.Register(&BlockVolumeEntry{Name: "charlie", VolumeServer: "vs1:9333"})
+ r.Register(&BlockVolumeEntry{Name: "alpha", VolumeServer: "vs1:9333"})
+ r.Register(&BlockVolumeEntry{Name: "bravo", VolumeServer: "vs1:9333"})
+
+ entries := r.ListAll()
+ if len(entries) != 3 {
+ t.Fatalf("expected 3 entries, got %d", len(entries))
+ }
+ if entries[0].Name != "alpha" || entries[1].Name != "bravo" || entries[2].Name != "charlie" {
+ t.Errorf("expected sorted order, got %s %s %s", entries[0].Name, entries[1].Name, entries[2].Name)
+ }
+}
+
+func TestServerSummaries(t *testing.T) {
+ r := NewBlockVolumeRegistry()
+ r.MarkBlockCapable("vs1:9333")
+ r.MarkBlockCapable("vs2:9333")
+ r.Register(&BlockVolumeEntry{Name: "vol1", VolumeServer: "vs1:9333"})
+ r.Register(&BlockVolumeEntry{Name: "vol2", VolumeServer: "vs1:9333"})
+ r.Register(&BlockVolumeEntry{Name: "vol3", VolumeServer: "vs2:9333"})
+
+ summaries := r.ServerSummaries()
+ if len(summaries) != 2 {
+ t.Fatalf("expected 2 summaries, got %d", len(summaries))
+ }
+ // Sorted by address.
+ if summaries[0].Address != "vs1:9333" || summaries[0].VolumeCount != 2 {
+ t.Errorf("vs1: got %+v", summaries[0])
+ }
+ if summaries[1].Address != "vs2:9333" || summaries[1].VolumeCount != 1 {
+ t.Errorf("vs2: got %+v", summaries[1])
+ }
+}
diff --git a/weed/server/master_server_handlers_block_ui.go b/weed/server/master_server_handlers_block_ui.go
new file mode 100644
index 0000000000..be0ca0ae58
--- /dev/null
+++ b/weed/server/master_server_handlers_block_ui.go
@@ -0,0 +1,304 @@
+package weed_server
+
+import (
+ "html/template"
+ "net/http"
+
+ "github.com/seaweedfs/seaweedfs/weed/glog"
+ "github.com/seaweedfs/seaweedfs/weed/storage/blockvol/blockapi"
+)
+
+var blockDashTemplate = template.Must(template.New("blockDash").Parse(blockLayoutHTML + blockDashContentHTML))
+var blockOpsTemplate = template.Must(template.New("blockOps").Parse(blockLayoutHTML + blockOpsContentHTML))
+
+type blockUIVolume struct {
+ blockapi.VolumeInfo
+ SizeMB uint64
+}
+
+type blockUIData struct {
+ Tab string // "dashboard" or "ops"
+ Volumes []blockUIVolume
+ Servers []blockapi.ServerInfo
+ // Dashboard stats
+ TotalVolumes int
+ ActiveCount int
+ PendingCount int
+ TotalSizeMB uint64
+}
+
+func (ms *MasterServer) buildBlockUIData(tab string) blockUIData {
+ entries := ms.blockRegistry.ListAll()
+ volumes := make([]blockUIVolume, len(entries))
+ var totalSizeMB uint64
+ var activeCount, pendingCount int
+ for i, e := range entries {
+ info := entryToVolumeInfo(e)
+ mb := info.SizeBytes / (1024 * 1024)
+ volumes[i] = blockUIVolume{
+ VolumeInfo: info,
+ SizeMB: mb,
+ }
+ totalSizeMB += mb
+ if e.Status == StatusActive {
+ activeCount++
+ } else {
+ pendingCount++
+ }
+ }
+
+ summaries := ms.blockRegistry.ServerSummaries()
+ servers := make([]blockapi.ServerInfo, len(summaries))
+ for i, s := range summaries {
+ servers[i] = blockapi.ServerInfo{
+ Address: s.Address,
+ VolumeCount: s.VolumeCount,
+ BlockCapable: s.BlockCapable,
+ }
+ }
+
+ return blockUIData{
+ Tab: tab,
+ Volumes: volumes,
+ Servers: servers,
+ TotalVolumes: len(entries),
+ ActiveCount: activeCount,
+ PendingCount: pendingCount,
+ TotalSizeMB: totalSizeMB,
+ }
+}
+
+// blockUIHandler serves the /block/ dashboard page.
+func (ms *MasterServer) blockUIHandler(w http.ResponseWriter, r *http.Request) {
+ data := ms.buildBlockUIData("dashboard")
+ w.Header().Set("Content-Type", "text/html; charset=utf-8")
+ if err := blockDashTemplate.Execute(w, data); err != nil {
+ glog.V(0).Infof("block UI template error: %v", err)
+ }
+}
+
+// blockOpsHandler serves the /block/ops operations page.
+func (ms *MasterServer) blockOpsHandler(w http.ResponseWriter, r *http.Request) {
+ data := ms.buildBlockUIData("ops")
+ w.Header().Set("Content-Type", "text/html; charset=utf-8")
+ if err := blockOpsTemplate.Execute(w, data); err != nil {
+ glog.V(0).Infof("block UI template error: %v", err)
+ }
+}
+
+// blockLayoutHTML is the shared HTML layout with tab navigation.
+const blockLayoutHTML = `
+
+
+SeaweedFS Block Storage
+
+
+
+
+
+
+`
+
+// blockDashContentHTML is the Dashboard tab content.
+const blockDashContentHTML = `
+
+
+
Block Servers
+
{{len .Servers}}
+
+
+
Total Volumes
+
{{.TotalVolumes}}
+
+
+
Active
+
{{.ActiveCount}}
+
+
+
Pending
+
{{.PendingCount}}
+
+
+
Total Capacity
+
{{.TotalSizeMB}} MB
+
+
+
+
Servers
+{{if .Servers}}
+
+ | Address | Volume Count | Block Capable |
+ {{range .Servers}}
+
+ | {{.Address}} |
+ {{.VolumeCount}} |
+ {{if .BlockCapable}}Yes{{else}}No{{end}} |
+
+ {{end}}
+
+{{else}}
+
No block servers connected
+{{end}}
+
+
Volumes
+{{if .Volumes}}
+
+
+ | Name | Server | Size | Placement |
+ Epoch | Role | Status | iSCSI | Replica |
+
+ {{range .Volumes}}
+
+ | {{.Name}} |
+ {{.VolumeServer}} |
+ {{.SizeMB}} MB |
+ {{.ReplicaPlacement}} |
+ {{.Epoch}} |
+ {{if eq .Role "primary"}}primary{{else if eq .Role "replica"}}replica{{else}}{{.Role}}{{end}} |
+ {{if eq .Status "active"}}active{{else}}{{.Status}}{{end}} |
+ {{.ISCSIAddr}} |
+ {{.ReplicaServer}} |
+
+ {{end}}
+
+{{else}}
+
No block volumes
+{{end}}
+
+
`
+
+// blockOpsContentHTML is the Operations tab content.
+const blockOpsContentHTML = `
+
+
+Volumes
+{{if .Volumes}}
+
+
+ | Name | Server | Size | Placement |
+ Epoch | Role | Status | iSCSI | Replica | Action |
+
+ {{range .Volumes}}
+
+ | {{.Name}} |
+ {{.VolumeServer}} |
+ {{.SizeMB}} MB |
+ {{.ReplicaPlacement}} |
+ {{.Epoch}} |
+ {{if eq .Role "primary"}}primary{{else if eq .Role "replica"}}replica{{else}}{{.Role}}{{end}} |
+ {{if eq .Status "active"}}active{{else}}{{.Status}}{{end}} |
+ {{.ISCSIAddr}} |
+ {{.ReplicaServer}} |
+ |
+
+ {{end}}
+
+{{else}}
+No block volumes
+{{end}}
+
+
+