snapshot of https://github.com/seaweedfs/seaweedfs.git (master, 2026-09-16; blobs > 131072B omitted)
Some checks failed
go: build dev binaries / cleanup (push) Successful in 4s
Codespell / Check for spelling errors (push) Failing after 3s
docker: build dev containers / build-rust-binaries (amd64, x86_64-unknown-linux-musl) (push) Failing after 3s
docker: build dev containers / build-dev-containers (push) Has been skipped
docker: build dev containers / build-rust-binaries (arm64, true, aarch64-unknown-linux-musl) (push) Failing after 3s
End to End / FUSE Mount (push) Failing after 2s
EC Integration Tests / EC Integration Tests (push) Failing after 30s
FUSE DLM Integration Tests / FUSE DLM Integration Tests (push) Failing after 3s
FUSE Volume Server Failover Tests / FUSE Volume Server Failover (push) Failing after 3s
FUSE Integration Tests / FUSE Integration Testing (push) Failing after 2s
FUSE P2P Peer Chunk Sharing Integration Tests / FUSE P2P Peer Chunk Sharing (push) Failing after 3s
go: build binary / Go Vet (push) Failing after 2s
go: build binary / Go Vet 32-bit (push) Failing after 3s
go: build binary / Build (push) Failing after 4s
go: build binary / Build cross-platform (freebsd/amd64) (push) Failing after 3s
go: build binary / Build cross-platform (windows/amd64) (push) Failing after 3s
go: build binary / Build cross-platform (darwin/arm64) (push) Failing after 3s
go: build binary / Build cross-platform (windows/arm64) (push) Failing after 3s
go: build binary / Test (push) Failing after 2s
go: build binary / Test 32-bit (push) Failing after 3s
helm: lint and test charts / lint-test (push) Failing after 3s
Java Client Integration Tests / Java Integration Tests (11) (push) Failing after 3s
Java Client Integration Tests / Java Integration Tests (17) (push) Failing after 3s
go: build dev binaries / build_dev_linux_windows (amd64, linux) (push) Failing after 2s
go: build dev binaries / build_dev_linux_windows (amd64, windows) (push) Failing after 3s
go: build dev binaries / build_dev_darwin (amd64, darwin) (push) Has been skipped
go: build dev binaries / build_dev_darwin (arm64, darwin) (push) Has been skipped
Java Client Unit Tests / Java Unit Tests (11) (push) Failing after 3s
Java Client Unit Tests / Java Unit Tests (17) (push) Failing after 2s
Java Client Unit Tests / Java Unit Tests (21) (push) Failing after 3s
Java Client Unit Tests / Java Unit Tests (8) (push) Failing after 3s
Kafka Quick Test (Load Test with Schema Registry) / Kafka Client Load Test (Quick) (push) Failing after 3s
Kafka Gateway Tests / Kafka Unit Tests (unit-tests-1) (push) Failing after 11s
Kafka Gateway Tests / Kafka Integration Tests (Critical) (integration-1) (push) Failing after 3s
Kafka Gateway Tests / Kafka End-to-End Tests (with SMQ) (e2e-1) (push) Failing after 3s
Kafka Gateway Tests / Kafka Consumer Group Tests (Highly Isolated) (consumer-group-1) (push) Failing after 2s
Kafka Gateway Tests / Kafka Client Compatibility (with SMQ) (client-compat-1) (push) Failing after 3s
Kafka Gateway Tests / Kafka SMQ Integration Tests (Full Stack) (smq-integration-1) (push) Failing after 3s
Kafka Gateway Tests / Kafka Protocol Tests (Isolated) (protocol-1) (push) Failing after 3s
KMS Tests / KMS Provider Integration Tests (push) Failing after 2s
KMS Tests / S3 KMS End-to-End Tests (push) Failing after 3s
Master Cold Start Tests / Master Cold Start Tests (push) Failing after 4s
Metadata Subscribe Integration Tests / Metadata Subscribe Integration Tests (push) Failing after 3s
Multi-Master Tests / Multi-Master Failover Tests (push) Failing after 3s
Performance / CPU and Heap Profile (push) Failing after 2s
Performance / S3 Read/Write Benchmark (rust volume) (push) Failing after 3s
pjdfstest POSIX Compliance / pjdfstest (push) Failing after 2s
Plugin Worker Integration Tests / Plugin Worker: erasure_coding (push) Failing after 3s
Plugin Worker Integration Tests / Plugin Worker: vacuum (push) Failing after 3s
Rust Volume Server Tests / Rust Integration Tests (push) Failing after 3s
Rust Volume Server Tests / Go Tests with Rust Volume (grpc - Shard 1) (push) Failing after 2s
Rust Volume Server Tests / Go Tests with Rust Volume (http - Shard 1) (push) Failing after 3s
Rust Volume Server Tests / Go Tests with Rust Volume (grpc - Shard 2) (push) Failing after 3s
Rust Volume Server Tests / Go Tests with Rust Volume (http - Shard 2) (push) Failing after 3s
Rust Volume Server Tests / Go Tests with Rust Volume (grpc - Shard 3) (push) Failing after 3s
S3 IAM Integration Tests / S3 IAM Distributed Tests (push) Failing after 2s
S3 IAM Integration Tests / S3 IAM Performance Tests (push) Failing after 3s
Performance / Throughput Benchmark (go volume) (push) Failing after 3s
Performance / Throughput Benchmark (rust volume) (push) Failing after 2s
Performance / S3 Read/Write Benchmark (go volume) (push) Failing after 3s
Plugin Worker Integration Tests / Plugin Worker: volume_balance (push) Failing after 3s
PostgreSQL Gateway Tests / PostgreSQL Basic Tests (push) Failing after 3s
Rust Volume Server Tests / Rust Unit Tests (push) Failing after 2s
Rust Volume Server Tests / Go Tests with Rust Volume (http - Shard 3) (push) Failing after 3s
Rust Plugin Worker Tests / Rust Plugin Worker Build and Unit Tests (push) Failing after 3s
rust: build dev volume server binaries / cleanup (push) Successful in 3s
S3 ETag and ACL Tests / S3 ETag + ACL Integration Tests (push) Failing after 3s
S3 IAM Integration Tests / IAM Unit Tests (push) Failing after 3s
S3 IAM Integration Tests / S3 IAM Integration Tests (advanced) (push) Failing after 3s
S3 IAM Integration Tests / S3 IAM Integration Tests (basic) (push) Failing after 2s
S3 IAM Integration Tests / S3 IAM Integration Tests (group) (push) Failing after 3s
S3 IAM Integration Tests / S3 IAM Integration Tests (policy-enforcement) (push) Failing after 2s
S3 IAM Integration Tests / S3 IAM Integration Tests (sts) (push) Failing after 4s
S3 Keycloak Integration Tests / S3 Keycloak Integration Tests (push) Failing after 3s
S3 PyArrow Parquet Tests / PyArrow Parquet Tests (Python 3.11) (push) Failing after 3s
S3 PyArrow Parquet Tests / Go Unit Tests (Implicit Directory) (push) Has been cancelled
S3 PyArrow Parquet Tests / PyArrow Parquet Tests (Python 3.12) (push) Has been cancelled
rust: build dev volume server binaries / build-rust-volume-dev-darwin (darwin-amd64, x86_64-apple-darwin) (push) Has been cancelled
rust: build dev volume server binaries / build-rust-volume-dev-darwin (darwin-arm64, aarch64-apple-darwin) (push) Has been cancelled
rust: build dev volume server binaries / build-rust-volume-dev-linux (linux-amd64, x86_64-unknown-linux-gnu) (push) Has been cancelled
S3 PyArrow Parquet Tests / PyArrow Parquet Tests (Python 3.9) (push) Has been cancelled
S3 Proxy Signature Tests / S3 Proxy Signature Verification Tests (push) Has been cancelled
S3 SDK V2 Route Disambiguation Tests / S3 SDK V2 Routing Tests (push) Has been cancelled
S3 SSE Tests / S3 SSE Integration Tests (quick) (push) Has been cancelled
S3 SSE Tests / S3 SSE Compatibility Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Metadata Persistence Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Copy Operations Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Multipart Upload Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Performance Test (push) Has been cancelled
S3 SSE Tests / S3 Volume Encryption Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Integration Tests (comprehensive) (push) Has been cancelled
Spark Integration Tests / Spark Integration Tests (push) Failing after 2s
Telemetry Integration Tests / telemetry-integration-test (push) Failing after 3s
Volume Server Integration Tests / Volume Server Integration Tests (grpc - Shard 1) (push) Failing after 3s
Volume Server Integration Tests / Volume Server Integration Tests (http - Shard 1) (push) Failing after 3s
Ceph S3 tests / Basic S3 tests (KV store) (push) Has been cancelled
Ceph S3 tests / S3 Versioning & Object Lock tests (push) Has been cancelled
S3 Policy Integration Tests / S3 Policy Unit Tests (push) Failing after 3s
Ceph S3 tests / S3 CORS tests (push) Has been cancelled
Ceph S3 tests / SeaweedFS Custom S3 Copy tests (push) Has been cancelled
Ceph S3 tests / Basic S3 tests (SQL store) (push) Has been cancelled
test s3 over https using aws-cli / awscli-tests (push) Has been cancelled
TLS Rotation Integration Tests / TLS Rotation Integration Tests (push) Has been cancelled
Vacuum Integration Tests / Vacuum Integration Tests (push) Has been cancelled
S3 Policy Integration Tests / S3 Policy Variables Integration Tests (push) Failing after 3s
S3 Policy Integration Tests / S3 Policy Enforcement Integration Tests (advanced-policy) (push) Failing after 3s
S3 Policy Integration Tests / S3 Policy Enforcement Integration Tests (basic-policy) (push) Failing after 2s
S3 Policy Integration Tests / S3 Policy Enforcement Integration Tests (contextual-policy) (push) Failing after 4s
S3 Policy Integration Tests / Trusted Proxy Detection Tests (push) Failing after 2s
S3 Policy Integration Tests / S3 Policy Shell Integration Tests (push) Failing after 3s
Samba on FUSE Integration / samba-integration (push) Failing after 2s
SFTP Integration Tests / SFTP Integration Testing (push) Failing after 3s
terraform: validate and test modules / fmt, validate, plan-level tests (push) Failing after 3s
terraform: validate and test modules / local cluster smoke test (real weed) (push) Failing after 2s
Volume Server Integration Tests / Volume Server Integration Tests (grpc - Shard 3) (push) Failing after 3s
Volume Server Integration Tests / Volume Server Integration Tests (grpc - Shard 2) (push) Failing after 3s
Volume Server Integration Tests / Volume Server Integration Tests (http - Shard 2) (push) Failing after 2s
Volume Server Integration Tests / Volume Server Integration Tests (http - Shard 3) (push) Failing after 5s
mount: windows conformance / WinFsp conformance (push) Has been cancelled
mount: windows / Mount on Windows (push) Has been cancelled
Some checks failed
go: build dev binaries / cleanup (push) Successful in 4s
Codespell / Check for spelling errors (push) Failing after 3s
docker: build dev containers / build-rust-binaries (amd64, x86_64-unknown-linux-musl) (push) Failing after 3s
docker: build dev containers / build-dev-containers (push) Has been skipped
docker: build dev containers / build-rust-binaries (arm64, true, aarch64-unknown-linux-musl) (push) Failing after 3s
End to End / FUSE Mount (push) Failing after 2s
EC Integration Tests / EC Integration Tests (push) Failing after 30s
FUSE DLM Integration Tests / FUSE DLM Integration Tests (push) Failing after 3s
FUSE Volume Server Failover Tests / FUSE Volume Server Failover (push) Failing after 3s
FUSE Integration Tests / FUSE Integration Testing (push) Failing after 2s
FUSE P2P Peer Chunk Sharing Integration Tests / FUSE P2P Peer Chunk Sharing (push) Failing after 3s
go: build binary / Go Vet (push) Failing after 2s
go: build binary / Go Vet 32-bit (push) Failing after 3s
go: build binary / Build (push) Failing after 4s
go: build binary / Build cross-platform (freebsd/amd64) (push) Failing after 3s
go: build binary / Build cross-platform (windows/amd64) (push) Failing after 3s
go: build binary / Build cross-platform (darwin/arm64) (push) Failing after 3s
go: build binary / Build cross-platform (windows/arm64) (push) Failing after 3s
go: build binary / Test (push) Failing after 2s
go: build binary / Test 32-bit (push) Failing after 3s
helm: lint and test charts / lint-test (push) Failing after 3s
Java Client Integration Tests / Java Integration Tests (11) (push) Failing after 3s
Java Client Integration Tests / Java Integration Tests (17) (push) Failing after 3s
go: build dev binaries / build_dev_linux_windows (amd64, linux) (push) Failing after 2s
go: build dev binaries / build_dev_linux_windows (amd64, windows) (push) Failing after 3s
go: build dev binaries / build_dev_darwin (amd64, darwin) (push) Has been skipped
go: build dev binaries / build_dev_darwin (arm64, darwin) (push) Has been skipped
Java Client Unit Tests / Java Unit Tests (11) (push) Failing after 3s
Java Client Unit Tests / Java Unit Tests (17) (push) Failing after 2s
Java Client Unit Tests / Java Unit Tests (21) (push) Failing after 3s
Java Client Unit Tests / Java Unit Tests (8) (push) Failing after 3s
Kafka Quick Test (Load Test with Schema Registry) / Kafka Client Load Test (Quick) (push) Failing after 3s
Kafka Gateway Tests / Kafka Unit Tests (unit-tests-1) (push) Failing after 11s
Kafka Gateway Tests / Kafka Integration Tests (Critical) (integration-1) (push) Failing after 3s
Kafka Gateway Tests / Kafka End-to-End Tests (with SMQ) (e2e-1) (push) Failing after 3s
Kafka Gateway Tests / Kafka Consumer Group Tests (Highly Isolated) (consumer-group-1) (push) Failing after 2s
Kafka Gateway Tests / Kafka Client Compatibility (with SMQ) (client-compat-1) (push) Failing after 3s
Kafka Gateway Tests / Kafka SMQ Integration Tests (Full Stack) (smq-integration-1) (push) Failing after 3s
Kafka Gateway Tests / Kafka Protocol Tests (Isolated) (protocol-1) (push) Failing after 3s
KMS Tests / KMS Provider Integration Tests (push) Failing after 2s
KMS Tests / S3 KMS End-to-End Tests (push) Failing after 3s
Master Cold Start Tests / Master Cold Start Tests (push) Failing after 4s
Metadata Subscribe Integration Tests / Metadata Subscribe Integration Tests (push) Failing after 3s
Multi-Master Tests / Multi-Master Failover Tests (push) Failing after 3s
Performance / CPU and Heap Profile (push) Failing after 2s
Performance / S3 Read/Write Benchmark (rust volume) (push) Failing after 3s
pjdfstest POSIX Compliance / pjdfstest (push) Failing after 2s
Plugin Worker Integration Tests / Plugin Worker: erasure_coding (push) Failing after 3s
Plugin Worker Integration Tests / Plugin Worker: vacuum (push) Failing after 3s
Rust Volume Server Tests / Rust Integration Tests (push) Failing after 3s
Rust Volume Server Tests / Go Tests with Rust Volume (grpc - Shard 1) (push) Failing after 2s
Rust Volume Server Tests / Go Tests with Rust Volume (http - Shard 1) (push) Failing after 3s
Rust Volume Server Tests / Go Tests with Rust Volume (grpc - Shard 2) (push) Failing after 3s
Rust Volume Server Tests / Go Tests with Rust Volume (http - Shard 2) (push) Failing after 3s
Rust Volume Server Tests / Go Tests with Rust Volume (grpc - Shard 3) (push) Failing after 3s
S3 IAM Integration Tests / S3 IAM Distributed Tests (push) Failing after 2s
S3 IAM Integration Tests / S3 IAM Performance Tests (push) Failing after 3s
Performance / Throughput Benchmark (go volume) (push) Failing after 3s
Performance / Throughput Benchmark (rust volume) (push) Failing after 2s
Performance / S3 Read/Write Benchmark (go volume) (push) Failing after 3s
Plugin Worker Integration Tests / Plugin Worker: volume_balance (push) Failing after 3s
PostgreSQL Gateway Tests / PostgreSQL Basic Tests (push) Failing after 3s
Rust Volume Server Tests / Rust Unit Tests (push) Failing after 2s
Rust Volume Server Tests / Go Tests with Rust Volume (http - Shard 3) (push) Failing after 3s
Rust Plugin Worker Tests / Rust Plugin Worker Build and Unit Tests (push) Failing after 3s
rust: build dev volume server binaries / cleanup (push) Successful in 3s
S3 ETag and ACL Tests / S3 ETag + ACL Integration Tests (push) Failing after 3s
S3 IAM Integration Tests / IAM Unit Tests (push) Failing after 3s
S3 IAM Integration Tests / S3 IAM Integration Tests (advanced) (push) Failing after 3s
S3 IAM Integration Tests / S3 IAM Integration Tests (basic) (push) Failing after 2s
S3 IAM Integration Tests / S3 IAM Integration Tests (group) (push) Failing after 3s
S3 IAM Integration Tests / S3 IAM Integration Tests (policy-enforcement) (push) Failing after 2s
S3 IAM Integration Tests / S3 IAM Integration Tests (sts) (push) Failing after 4s
S3 Keycloak Integration Tests / S3 Keycloak Integration Tests (push) Failing after 3s
S3 PyArrow Parquet Tests / PyArrow Parquet Tests (Python 3.11) (push) Failing after 3s
S3 PyArrow Parquet Tests / Go Unit Tests (Implicit Directory) (push) Has been cancelled
S3 PyArrow Parquet Tests / PyArrow Parquet Tests (Python 3.12) (push) Has been cancelled
rust: build dev volume server binaries / build-rust-volume-dev-darwin (darwin-amd64, x86_64-apple-darwin) (push) Has been cancelled
rust: build dev volume server binaries / build-rust-volume-dev-darwin (darwin-arm64, aarch64-apple-darwin) (push) Has been cancelled
rust: build dev volume server binaries / build-rust-volume-dev-linux (linux-amd64, x86_64-unknown-linux-gnu) (push) Has been cancelled
S3 PyArrow Parquet Tests / PyArrow Parquet Tests (Python 3.9) (push) Has been cancelled
S3 Proxy Signature Tests / S3 Proxy Signature Verification Tests (push) Has been cancelled
S3 SDK V2 Route Disambiguation Tests / S3 SDK V2 Routing Tests (push) Has been cancelled
S3 SSE Tests / S3 SSE Integration Tests (quick) (push) Has been cancelled
S3 SSE Tests / S3 SSE Compatibility Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Metadata Persistence Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Copy Operations Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Multipart Upload Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Performance Test (push) Has been cancelled
S3 SSE Tests / S3 Volume Encryption Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Integration Tests (comprehensive) (push) Has been cancelled
Spark Integration Tests / Spark Integration Tests (push) Failing after 2s
Telemetry Integration Tests / telemetry-integration-test (push) Failing after 3s
Volume Server Integration Tests / Volume Server Integration Tests (grpc - Shard 1) (push) Failing after 3s
Volume Server Integration Tests / Volume Server Integration Tests (http - Shard 1) (push) Failing after 3s
Ceph S3 tests / Basic S3 tests (KV store) (push) Has been cancelled
Ceph S3 tests / S3 Versioning & Object Lock tests (push) Has been cancelled
S3 Policy Integration Tests / S3 Policy Unit Tests (push) Failing after 3s
Ceph S3 tests / S3 CORS tests (push) Has been cancelled
Ceph S3 tests / SeaweedFS Custom S3 Copy tests (push) Has been cancelled
Ceph S3 tests / Basic S3 tests (SQL store) (push) Has been cancelled
test s3 over https using aws-cli / awscli-tests (push) Has been cancelled
TLS Rotation Integration Tests / TLS Rotation Integration Tests (push) Has been cancelled
Vacuum Integration Tests / Vacuum Integration Tests (push) Has been cancelled
S3 Policy Integration Tests / S3 Policy Variables Integration Tests (push) Failing after 3s
S3 Policy Integration Tests / S3 Policy Enforcement Integration Tests (advanced-policy) (push) Failing after 3s
S3 Policy Integration Tests / S3 Policy Enforcement Integration Tests (basic-policy) (push) Failing after 2s
S3 Policy Integration Tests / S3 Policy Enforcement Integration Tests (contextual-policy) (push) Failing after 4s
S3 Policy Integration Tests / Trusted Proxy Detection Tests (push) Failing after 2s
S3 Policy Integration Tests / S3 Policy Shell Integration Tests (push) Failing after 3s
Samba on FUSE Integration / samba-integration (push) Failing after 2s
SFTP Integration Tests / SFTP Integration Testing (push) Failing after 3s
terraform: validate and test modules / fmt, validate, plan-level tests (push) Failing after 3s
terraform: validate and test modules / local cluster smoke test (real weed) (push) Failing after 2s
Volume Server Integration Tests / Volume Server Integration Tests (grpc - Shard 3) (push) Failing after 3s
Volume Server Integration Tests / Volume Server Integration Tests (grpc - Shard 2) (push) Failing after 3s
Volume Server Integration Tests / Volume Server Integration Tests (http - Shard 2) (push) Failing after 2s
Volume Server Integration Tests / Volume Server Integration Tests (http - Shard 3) (push) Failing after 5s
mount: windows conformance / WinFsp conformance (push) Has been cancelled
mount: windows / Mount on Windows (push) Has been cancelled
This commit is contained in:
113
weed/mq/sub_coordinator/consumer_group.go
Normal file
113
weed/mq/sub_coordinator/consumer_group.go
Normal file
@@ -0,0 +1,113 @@
|
||||
package sub_coordinator
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
cmap "github.com/orcaman/concurrent-map/v2"
|
||||
"github.com/seaweedfs/seaweedfs/weed/filer_client"
|
||||
"github.com/seaweedfs/seaweedfs/weed/glog"
|
||||
"github.com/seaweedfs/seaweedfs/weed/mq/topic"
|
||||
"github.com/seaweedfs/seaweedfs/weed/pb/mq_pb"
|
||||
"github.com/seaweedfs/seaweedfs/weed/pb/schema_pb"
|
||||
)
|
||||
|
||||
type ConsumerGroup struct {
|
||||
topic topic.Topic
|
||||
// map a consumer group instance id to a consumer group instance
|
||||
ConsumerGroupInstances cmap.ConcurrentMap[string, *ConsumerGroupInstance]
|
||||
Market *Market
|
||||
reBalanceTimer *time.Timer
|
||||
filerClientAccessor *filer_client.FilerClientAccessor
|
||||
stopCh chan struct{}
|
||||
}
|
||||
|
||||
func NewConsumerGroup(t *schema_pb.Topic, reblanceSeconds int32, filerClientAccessor *filer_client.FilerClientAccessor) *ConsumerGroup {
|
||||
cg := &ConsumerGroup{
|
||||
topic: topic.FromPbTopic(t),
|
||||
ConsumerGroupInstances: cmap.New[*ConsumerGroupInstance](),
|
||||
filerClientAccessor: filerClientAccessor,
|
||||
stopCh: make(chan struct{}),
|
||||
}
|
||||
if conf, err := cg.filerClientAccessor.ReadTopicConfFromFiler(cg.topic); err == nil {
|
||||
var partitions []topic.Partition
|
||||
for _, assignment := range conf.BrokerPartitionAssignments {
|
||||
partitions = append(partitions, topic.FromPbPartition(assignment.Partition))
|
||||
}
|
||||
cg.Market = NewMarket(partitions, time.Duration(reblanceSeconds)*time.Second)
|
||||
} else {
|
||||
glog.V(0).Infof("fail to read topic conf from filer: %v", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case adjustment := <-cg.Market.AdjustmentChan:
|
||||
cgi, found := cg.ConsumerGroupInstances.Get(string(adjustment.consumer))
|
||||
if !found {
|
||||
glog.V(0).Infof("consumer group instance %s not found", adjustment.consumer)
|
||||
continue
|
||||
}
|
||||
if adjustment.isAssign {
|
||||
if conf, err := cg.filerClientAccessor.ReadTopicConfFromFiler(cg.topic); err == nil {
|
||||
for _, assignment := range conf.BrokerPartitionAssignments {
|
||||
if adjustment.partition.Equals(topic.FromPbPartition(assignment.Partition)) {
|
||||
cgi.ResponseChan <- &mq_pb.SubscriberToSubCoordinatorResponse{
|
||||
Message: &mq_pb.SubscriberToSubCoordinatorResponse_Assignment_{
|
||||
Assignment: &mq_pb.SubscriberToSubCoordinatorResponse_Assignment{
|
||||
PartitionAssignment: &mq_pb.BrokerPartitionAssignment{
|
||||
Partition: adjustment.partition.ToPbPartition(),
|
||||
LeaderBroker: assignment.LeaderBroker,
|
||||
FollowerBroker: assignment.FollowerBroker,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
glog.V(0).Infof("send assignment %v to %s", adjustment.partition, adjustment.consumer)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
cgi.ResponseChan <- &mq_pb.SubscriberToSubCoordinatorResponse{
|
||||
Message: &mq_pb.SubscriberToSubCoordinatorResponse_UnAssignment_{
|
||||
UnAssignment: &mq_pb.SubscriberToSubCoordinatorResponse_UnAssignment{
|
||||
Partition: adjustment.partition.ToPbPartition(),
|
||||
},
|
||||
},
|
||||
}
|
||||
glog.V(0).Infof("send unassignment %v to %s", adjustment.partition, adjustment.consumer)
|
||||
}
|
||||
case <-cg.stopCh:
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
return cg
|
||||
}
|
||||
|
||||
func (cg *ConsumerGroup) AckAssignment(cgi *ConsumerGroupInstance, assignment *mq_pb.SubscriberToSubCoordinatorRequest_AckAssignmentMessage) {
|
||||
fmt.Printf("ack assignment %v\n", assignment)
|
||||
cg.Market.ConfirmAdjustment(&Adjustment{
|
||||
consumer: cgi.InstanceId,
|
||||
partition: topic.FromPbPartition(assignment.Partition),
|
||||
isAssign: true,
|
||||
})
|
||||
}
|
||||
func (cg *ConsumerGroup) AckUnAssignment(cgi *ConsumerGroupInstance, assignment *mq_pb.SubscriberToSubCoordinatorRequest_AckUnAssignmentMessage) {
|
||||
fmt.Printf("ack unassignment %v\n", assignment)
|
||||
cg.Market.ConfirmAdjustment(&Adjustment{
|
||||
consumer: cgi.InstanceId,
|
||||
partition: topic.FromPbPartition(assignment.Partition),
|
||||
isAssign: false,
|
||||
})
|
||||
}
|
||||
|
||||
func (cg *ConsumerGroup) OnPartitionListChange(assignments []*mq_pb.BrokerPartitionAssignment) {
|
||||
}
|
||||
|
||||
func (cg *ConsumerGroup) Shutdown() {
|
||||
close(cg.stopCh)
|
||||
}
|
||||
23
weed/mq/sub_coordinator/consumer_group_instance.go
Normal file
23
weed/mq/sub_coordinator/consumer_group_instance.go
Normal file
@@ -0,0 +1,23 @@
|
||||
package sub_coordinator
|
||||
|
||||
import (
|
||||
"github.com/seaweedfs/seaweedfs/weed/mq/topic"
|
||||
"github.com/seaweedfs/seaweedfs/weed/pb/mq_pb"
|
||||
)
|
||||
|
||||
type ConsumerGroupInstanceId string
|
||||
|
||||
type ConsumerGroupInstance struct {
|
||||
InstanceId ConsumerGroupInstanceId
|
||||
AssignedPartitions []topic.Partition
|
||||
ResponseChan chan *mq_pb.SubscriberToSubCoordinatorResponse
|
||||
MaxPartitionCount int32
|
||||
}
|
||||
|
||||
func NewConsumerGroupInstance(instanceId string, maxPartitionCount int32) *ConsumerGroupInstance {
|
||||
return &ConsumerGroupInstance{
|
||||
InstanceId: ConsumerGroupInstanceId(instanceId),
|
||||
ResponseChan: make(chan *mq_pb.SubscriberToSubCoordinatorResponse, 1),
|
||||
MaxPartitionCount: maxPartitionCount,
|
||||
}
|
||||
}
|
||||
144
weed/mq/sub_coordinator/inflight_message_tracker.go
Normal file
144
weed/mq/sub_coordinator/inflight_message_tracker.go
Normal file
@@ -0,0 +1,144 @@
|
||||
package sub_coordinator
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type InflightMessageTracker struct {
|
||||
messages map[string]int64
|
||||
mu sync.Mutex
|
||||
timestamps *RingBuffer
|
||||
}
|
||||
|
||||
func NewInflightMessageTracker(capacity int) *InflightMessageTracker {
|
||||
return &InflightMessageTracker{
|
||||
messages: make(map[string]int64),
|
||||
timestamps: NewRingBuffer(capacity),
|
||||
}
|
||||
}
|
||||
|
||||
// EnflightMessage tracks the message with the key and timestamp.
|
||||
// These messages are sent to the consumer group instances and waiting for ack.
|
||||
func (imt *InflightMessageTracker) EnflightMessage(key []byte, tsNs int64) {
|
||||
// fmt.Printf("EnflightMessage(%s,%d)\n", string(key), tsNs)
|
||||
imt.mu.Lock()
|
||||
defer imt.mu.Unlock()
|
||||
imt.messages[string(key)] = tsNs
|
||||
imt.timestamps.EnflightTimestamp(tsNs)
|
||||
}
|
||||
|
||||
// AcknowledgeMessage acknowledges the message with the key and timestamp.
|
||||
func (imt *InflightMessageTracker) AcknowledgeMessage(key []byte, tsNs int64) bool {
|
||||
// fmt.Printf("AcknowledgeMessage(%s,%d)\n", string(key), tsNs)
|
||||
imt.mu.Lock()
|
||||
defer imt.mu.Unlock()
|
||||
timestamp, exists := imt.messages[string(key)]
|
||||
if !exists || timestamp != tsNs {
|
||||
return false
|
||||
}
|
||||
delete(imt.messages, string(key))
|
||||
// Remove the specific timestamp from the ring buffer.
|
||||
imt.timestamps.AckTimestamp(tsNs)
|
||||
return true
|
||||
}
|
||||
|
||||
func (imt *InflightMessageTracker) GetOldestAckedTimestamp() int64 {
|
||||
return imt.timestamps.OldestAckedTimestamp()
|
||||
}
|
||||
|
||||
// IsInflight returns true if the message with the key is inflight.
|
||||
func (imt *InflightMessageTracker) IsInflight(key []byte) bool {
|
||||
imt.mu.Lock()
|
||||
defer imt.mu.Unlock()
|
||||
_, found := imt.messages[string(key)]
|
||||
return found
|
||||
}
|
||||
|
||||
// Cleanup clears all in-flight messages. This should be called when a subscriber disconnects
|
||||
// to prevent messages from being stuck in the in-flight state indefinitely.
|
||||
func (imt *InflightMessageTracker) Cleanup() int {
|
||||
imt.mu.Lock()
|
||||
defer imt.mu.Unlock()
|
||||
count := len(imt.messages)
|
||||
// Clear all in-flight messages
|
||||
imt.messages = make(map[string]int64)
|
||||
return count
|
||||
}
|
||||
|
||||
type TimestampStatus struct {
|
||||
Timestamp int64
|
||||
Acked bool
|
||||
}
|
||||
|
||||
// RingBuffer represents a circular buffer to hold timestamps.
|
||||
type RingBuffer struct {
|
||||
buffer []*TimestampStatus
|
||||
head int
|
||||
size int
|
||||
maxTimestamp int64
|
||||
maxAllAckedTs int64
|
||||
}
|
||||
|
||||
// NewRingBuffer creates a new RingBuffer of the given capacity.
|
||||
func NewRingBuffer(capacity int) *RingBuffer {
|
||||
return &RingBuffer{
|
||||
buffer: newBuffer(capacity),
|
||||
}
|
||||
}
|
||||
|
||||
func newBuffer(capacity int) []*TimestampStatus {
|
||||
buffer := make([]*TimestampStatus, capacity)
|
||||
for i := range buffer {
|
||||
buffer[i] = &TimestampStatus{}
|
||||
}
|
||||
return buffer
|
||||
}
|
||||
|
||||
// EnflightTimestamp adds a new timestamp to the ring buffer.
|
||||
func (rb *RingBuffer) EnflightTimestamp(timestamp int64) {
|
||||
if rb.size < len(rb.buffer) {
|
||||
rb.size++
|
||||
} else {
|
||||
newBuf := newBuffer(2 * len(rb.buffer))
|
||||
for i := 0; i < rb.size; i++ {
|
||||
newBuf[i] = rb.buffer[(rb.head+len(rb.buffer)-rb.size+i)%len(rb.buffer)]
|
||||
}
|
||||
rb.buffer = newBuf
|
||||
rb.head = rb.size
|
||||
rb.size++
|
||||
}
|
||||
head := rb.buffer[rb.head]
|
||||
head.Timestamp = timestamp
|
||||
head.Acked = false
|
||||
rb.head = (rb.head + 1) % len(rb.buffer)
|
||||
if timestamp > rb.maxTimestamp {
|
||||
rb.maxTimestamp = timestamp
|
||||
}
|
||||
}
|
||||
|
||||
// AckTimestamp removes the specified timestamp from the ring buffer.
|
||||
func (rb *RingBuffer) AckTimestamp(timestamp int64) {
|
||||
// Perform binary search
|
||||
index := sort.Search(rb.size, func(i int) bool {
|
||||
return rb.buffer[(rb.head+len(rb.buffer)-rb.size+i)%len(rb.buffer)].Timestamp >= timestamp
|
||||
})
|
||||
actualIndex := (rb.head + len(rb.buffer) - rb.size + index) % len(rb.buffer)
|
||||
|
||||
rb.buffer[actualIndex].Acked = true
|
||||
|
||||
// Remove all the continuously acknowledged timestamps from the buffer
|
||||
startPos := (rb.head + len(rb.buffer) - rb.size) % len(rb.buffer)
|
||||
for i := 0; i < len(rb.buffer) && rb.buffer[(startPos+i)%len(rb.buffer)].Acked; i++ {
|
||||
t := rb.buffer[(startPos+i)%len(rb.buffer)]
|
||||
if rb.maxAllAckedTs < t.Timestamp {
|
||||
rb.size--
|
||||
rb.maxAllAckedTs = t.Timestamp
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// OldestAckedTimestamp returns the oldest that is already acked timestamp in the ring buffer.
|
||||
func (rb *RingBuffer) OldestAckedTimestamp() int64 {
|
||||
return rb.maxAllAckedTs
|
||||
}
|
||||
368
weed/mq/sub_coordinator/market.go
Normal file
368
weed/mq/sub_coordinator/market.go
Normal file
@@ -0,0 +1,368 @@
|
||||
package sub_coordinator
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/seaweedfs/seaweedfs/weed/glog"
|
||||
"github.com/seaweedfs/seaweedfs/weed/mq/topic"
|
||||
)
|
||||
|
||||
/*
|
||||
Market is a data structure that keeps track of the state of the consumer group instances and the partitions.
|
||||
|
||||
When rebalancing, the market will try to balance the load of the partitions among the consumer group instances.
|
||||
For each loop, the market will:
|
||||
* If a consumer group instance has more partitions than the average, it will unassign some partitions.
|
||||
* If a consumer group instance has less partitions than the average, it will assign some partitions.
|
||||
|
||||
Trigger rebalance when:
|
||||
* A new consumer group instance is added
|
||||
* Some partitions are unassigned from a consumer group instance.
|
||||
|
||||
If multiple rebalance requests are received, after a certain period, the market will only process the latest request.
|
||||
|
||||
However, if the number of unassigned partition is increased to exactly the total number of partitions,
|
||||
and total partitions are less than or equal to the sum of the max partition count of all consumer group instances,
|
||||
the market will process the request immediately.
|
||||
This is to ensure a partition can be migrated to another consumer group instance as soon as possible.
|
||||
|
||||
Emit these adjustments to the subscriber coordinator:
|
||||
* Assign a partition to a consumer group instance
|
||||
* Unassign a partition from a consumer group instance
|
||||
|
||||
Because the adjustment is sent to the subscriber coordinator, the market will keep track of the inflight adjustments.
|
||||
The subscriber coordinator will send back the response to the market when the adjustment is processed.
|
||||
If the adjustment is older than a certain time(inflightAdjustmentTTL), it would be considered expired.
|
||||
Otherwise, the adjustment is considered inflight, so it would be used when calculating the load.
|
||||
|
||||
Later features:
|
||||
* A consumer group instance is not keeping up with the load.
|
||||
|
||||
Since a coordinator, and thus the market, may be restarted or moved to another node, the market should be able to recover the state from the subscriber coordinator.
|
||||
The subscriber coordinator should be able to send the current state of the consumer group instances and the partitions to the market.
|
||||
|
||||
*/
|
||||
|
||||
type PartitionSlot struct {
|
||||
Partition topic.Partition
|
||||
AssignedTo *ConsumerGroupInstance // Track the consumer assigned to this partition slot
|
||||
}
|
||||
|
||||
type Adjustment struct {
|
||||
isAssign bool
|
||||
partition topic.Partition
|
||||
consumer ConsumerGroupInstanceId
|
||||
ts time.Time
|
||||
}
|
||||
|
||||
type Market struct {
|
||||
mu sync.Mutex
|
||||
partitions map[topic.Partition]*PartitionSlot
|
||||
consumerInstances map[ConsumerGroupInstanceId]*ConsumerGroupInstance
|
||||
AdjustmentChan chan *Adjustment
|
||||
inflightAdjustments []*Adjustment
|
||||
inflightAdjustmentTTL time.Duration
|
||||
lastBalancedTime time.Time
|
||||
stopChan chan struct{}
|
||||
balanceRequestChan chan struct{}
|
||||
hasBalanceRequest bool
|
||||
}
|
||||
|
||||
func NewMarket(partitions []topic.Partition, inflightAdjustmentTTL time.Duration) *Market {
|
||||
partitionMap := make(map[topic.Partition]*PartitionSlot)
|
||||
for _, partition := range partitions {
|
||||
partitionMap[partition] = &PartitionSlot{
|
||||
Partition: partition,
|
||||
}
|
||||
}
|
||||
m := &Market{
|
||||
partitions: partitionMap,
|
||||
consumerInstances: make(map[ConsumerGroupInstanceId]*ConsumerGroupInstance),
|
||||
AdjustmentChan: make(chan *Adjustment, 100),
|
||||
inflightAdjustmentTTL: inflightAdjustmentTTL,
|
||||
stopChan: make(chan struct{}),
|
||||
balanceRequestChan: make(chan struct{}),
|
||||
}
|
||||
m.lastBalancedTime = time.Now()
|
||||
go m.loopBalanceLoad()
|
||||
|
||||
return m
|
||||
}
|
||||
|
||||
func (m *Market) ShutdownMarket() {
|
||||
close(m.stopChan)
|
||||
close(m.AdjustmentChan)
|
||||
}
|
||||
|
||||
func (m *Market) AddConsumerInstance(consumer *ConsumerGroupInstance) error {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
if _, exists := m.consumerInstances[consumer.InstanceId]; exists {
|
||||
return errors.New("consumer instance already exists")
|
||||
}
|
||||
|
||||
m.consumerInstances[consumer.InstanceId] = consumer
|
||||
m.balanceRequestChan <- struct{}{}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Market) RemoveConsumerInstance(consumerId ConsumerGroupInstanceId) error {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
consumer, exists := m.consumerInstances[consumerId]
|
||||
if !exists {
|
||||
return nil
|
||||
}
|
||||
delete(m.consumerInstances, consumerId)
|
||||
|
||||
for _, partition := range consumer.AssignedPartitions {
|
||||
if partitionSlot, exists := m.partitions[partition]; exists {
|
||||
partitionSlot.AssignedTo = nil
|
||||
}
|
||||
}
|
||||
m.balanceRequestChan <- struct{}{}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Market) assignPartitionToConsumer(partition *PartitionSlot) {
|
||||
var bestConsumer *ConsumerGroupInstance
|
||||
var minLoad = int(^uint(0) >> 1) // Max int value
|
||||
|
||||
inflightConsumerAdjustments := make(map[ConsumerGroupInstanceId]int)
|
||||
for _, adjustment := range m.inflightAdjustments {
|
||||
if adjustment.isAssign {
|
||||
inflightConsumerAdjustments[adjustment.consumer]++
|
||||
} else {
|
||||
inflightConsumerAdjustments[adjustment.consumer]--
|
||||
}
|
||||
}
|
||||
for _, consumer := range m.consumerInstances {
|
||||
consumerLoad := len(consumer.AssignedPartitions)
|
||||
if inflightAdjustments, exists := inflightConsumerAdjustments[consumer.InstanceId]; exists {
|
||||
consumerLoad += inflightAdjustments
|
||||
}
|
||||
// fmt.Printf("Consumer %+v has load %d, max %d, min %d\n", consumer.InstanceId, consumerLoad, consumer.MaxPartitionCount, minLoad)
|
||||
if consumerLoad < int(consumer.MaxPartitionCount) {
|
||||
if consumerLoad < minLoad {
|
||||
bestConsumer = consumer
|
||||
minLoad = consumerLoad
|
||||
// fmt.Printf("picked: Consumer %+v has load %d, max %d, min %d\n", consumer.InstanceId, consumerLoad, consumer.MaxPartitionCount, minLoad)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if bestConsumer != nil {
|
||||
// change consumer assigned partitions later when the adjustment is confirmed
|
||||
adjustment := &Adjustment{
|
||||
isAssign: true,
|
||||
partition: partition.Partition,
|
||||
consumer: bestConsumer.InstanceId,
|
||||
ts: time.Now(),
|
||||
}
|
||||
m.AdjustmentChan <- adjustment
|
||||
m.inflightAdjustments = append(m.inflightAdjustments, adjustment)
|
||||
m.lastBalancedTime = adjustment.ts
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Market) loopBalanceLoad() {
|
||||
ticker := time.NewTicker(500 * time.Millisecond)
|
||||
defer ticker.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
if m.hasBalanceRequest {
|
||||
m.hasBalanceRequest = false
|
||||
inflightAdjustments := make([]*Adjustment, 0, len(m.inflightAdjustments))
|
||||
for _, adjustment := range m.inflightAdjustments {
|
||||
if adjustment.ts.Add(m.inflightAdjustmentTTL).After(time.Now()) {
|
||||
inflightAdjustments = append(inflightAdjustments, adjustment)
|
||||
}
|
||||
}
|
||||
m.inflightAdjustments = inflightAdjustments
|
||||
|
||||
m.doBalanceLoad()
|
||||
// println("Balance load completed.")
|
||||
m.Status()
|
||||
}
|
||||
case <-m.balanceRequestChan:
|
||||
m.hasBalanceRequest = true
|
||||
case <-m.stopChan:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// doBalanceLoad will balance the load of the partitions among the consumer group instances.
|
||||
// It will try to unassign partitions from the consumer group instances that have more partitions than the average.
|
||||
// It will try to assign partitions to the consumer group instances that have less partitions than the average.
|
||||
func (m *Market) doBalanceLoad() {
|
||||
if len(m.consumerInstances) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
// find the average load for all consumers
|
||||
averageLoad := m.findAverageLoad()
|
||||
|
||||
// find the consumers with the higher load than average
|
||||
if m.adjustBusyConsumers(averageLoad) {
|
||||
return
|
||||
}
|
||||
|
||||
// find partitions with no consumer assigned
|
||||
m.adjustUnassignedPartitions()
|
||||
}
|
||||
func (m *Market) findAverageLoad() (averageLoad float32) {
|
||||
var totalLoad int
|
||||
for _, consumer := range m.consumerInstances {
|
||||
totalLoad += len(consumer.AssignedPartitions)
|
||||
}
|
||||
for _, adjustment := range m.inflightAdjustments {
|
||||
if adjustment.isAssign {
|
||||
totalLoad++
|
||||
} else {
|
||||
totalLoad--
|
||||
}
|
||||
}
|
||||
averageLoad = float32(totalLoad) / float32(len(m.consumerInstances))
|
||||
return
|
||||
}
|
||||
|
||||
func (m *Market) adjustBusyConsumers(averageLoad float32) (hasAdjustments bool) {
|
||||
inflightConsumerAdjustments := make(map[ConsumerGroupInstanceId]int)
|
||||
for _, adjustment := range m.inflightAdjustments {
|
||||
if adjustment.isAssign {
|
||||
inflightConsumerAdjustments[adjustment.consumer]++
|
||||
} else {
|
||||
inflightConsumerAdjustments[adjustment.consumer]--
|
||||
}
|
||||
}
|
||||
for _, consumer := range m.consumerInstances {
|
||||
consumerLoad := len(consumer.AssignedPartitions)
|
||||
if inflightAdjustment, exists := inflightConsumerAdjustments[consumer.InstanceId]; exists {
|
||||
consumerLoad += inflightAdjustment
|
||||
}
|
||||
delta := int(float32(consumerLoad) - averageLoad)
|
||||
if delta <= 0 {
|
||||
continue
|
||||
}
|
||||
adjustTime := time.Now()
|
||||
for i := 0; i < delta; i++ {
|
||||
adjustment := &Adjustment{
|
||||
isAssign: false,
|
||||
partition: consumer.AssignedPartitions[i],
|
||||
consumer: consumer.InstanceId,
|
||||
ts: adjustTime,
|
||||
}
|
||||
m.AdjustmentChan <- adjustment
|
||||
m.inflightAdjustments = append(m.inflightAdjustments, adjustment)
|
||||
m.lastBalancedTime = adjustment.ts
|
||||
}
|
||||
hasAdjustments = true
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (m *Market) adjustUnassignedPartitions() {
|
||||
inflightPartitionAdjustments := make(map[topic.Partition]bool)
|
||||
for _, adjustment := range m.inflightAdjustments {
|
||||
inflightPartitionAdjustments[adjustment.partition] = true
|
||||
}
|
||||
for _, partitionSlot := range m.partitions {
|
||||
if partitionSlot.AssignedTo == nil {
|
||||
if _, exists := inflightPartitionAdjustments[partitionSlot.Partition]; exists {
|
||||
continue
|
||||
}
|
||||
// fmt.Printf("Assigning partition %+v to consumer\n", partitionSlot.Partition)
|
||||
m.assignPartitionToConsumer(partitionSlot)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Market) ConfirmAdjustment(adjustment *Adjustment) {
|
||||
if adjustment.isAssign {
|
||||
m.confirmAssignPartition(adjustment.partition, adjustment.consumer)
|
||||
} else {
|
||||
m.unassignPartitionSlot(adjustment.partition)
|
||||
}
|
||||
glog.V(1).Infof("ConfirmAdjustment %+v", adjustment)
|
||||
m.Status()
|
||||
}
|
||||
|
||||
func (m *Market) unassignPartitionSlot(partition topic.Partition) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
partitionSlot, exists := m.partitions[partition]
|
||||
if !exists {
|
||||
glog.V(0).Infof("partition %+v slot is not tracked", partition)
|
||||
return
|
||||
}
|
||||
|
||||
if partitionSlot.AssignedTo == nil {
|
||||
glog.V(0).Infof("partition %+v slot is not assigned to any consumer", partition)
|
||||
return
|
||||
}
|
||||
|
||||
consumer := partitionSlot.AssignedTo
|
||||
for i, p := range consumer.AssignedPartitions {
|
||||
if p == partition {
|
||||
consumer.AssignedPartitions = append(consumer.AssignedPartitions[:i], consumer.AssignedPartitions[i+1:]...)
|
||||
partitionSlot.AssignedTo = nil
|
||||
m.balanceRequestChan <- struct{}{}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
glog.V(0).Infof("partition %+v slot not found in assigned consumer", partition)
|
||||
|
||||
}
|
||||
|
||||
func (m *Market) confirmAssignPartition(partition topic.Partition, consumerInstanceId ConsumerGroupInstanceId) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
partitionSlot, exists := m.partitions[partition]
|
||||
if !exists {
|
||||
glog.V(0).Infof("partition %+v slot is not tracked", partition)
|
||||
return
|
||||
}
|
||||
|
||||
if partitionSlot.AssignedTo != nil {
|
||||
glog.V(0).Infof("partition %+v slot is already assigned to %+v", partition, partitionSlot.AssignedTo.InstanceId)
|
||||
return
|
||||
}
|
||||
|
||||
consumerInstance, exists := m.consumerInstances[consumerInstanceId]
|
||||
if !exists {
|
||||
glog.V(0).Infof("consumer %+v is not tracked", consumerInstanceId)
|
||||
return
|
||||
}
|
||||
|
||||
partitionSlot.AssignedTo = consumerInstance
|
||||
consumerInstance.AssignedPartitions = append(consumerInstance.AssignedPartitions, partition)
|
||||
|
||||
}
|
||||
|
||||
func (m *Market) Status() {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
glog.V(1).Infof("Market has %d partitions and %d consumer instances", len(m.partitions), len(m.consumerInstances))
|
||||
for partition, slot := range m.partitions {
|
||||
if slot.AssignedTo == nil {
|
||||
glog.V(1).Infof("Partition %+v is not assigned to any consumer", partition)
|
||||
} else {
|
||||
glog.V(1).Infof("Partition %+v is assigned to consumer %+v", partition, slot.AssignedTo.InstanceId)
|
||||
}
|
||||
}
|
||||
for _, consumer := range m.consumerInstances {
|
||||
glog.V(1).Infof("Consumer %+v has %d partitions", consumer.InstanceId, len(consumer.AssignedPartitions))
|
||||
}
|
||||
}
|
||||
103
weed/mq/sub_coordinator/market_test.go
Normal file
103
weed/mq/sub_coordinator/market_test.go
Normal file
@@ -0,0 +1,103 @@
|
||||
package sub_coordinator
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/seaweedfs/seaweedfs/weed/mq/topic"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
var partitions = []topic.Partition{
|
||||
{
|
||||
RangeStart: 0,
|
||||
RangeStop: 1,
|
||||
RingSize: 3,
|
||||
UnixTimeNs: 0,
|
||||
},
|
||||
{
|
||||
RangeStart: 1,
|
||||
RangeStop: 2,
|
||||
RingSize: 3,
|
||||
UnixTimeNs: 0,
|
||||
},
|
||||
{
|
||||
RangeStart: 2,
|
||||
RangeStop: 3,
|
||||
RingSize: 3,
|
||||
UnixTimeNs: 0,
|
||||
},
|
||||
}
|
||||
|
||||
func TestAddConsumerInstance(t *testing.T) {
|
||||
market := NewMarket(partitions, 10*time.Second)
|
||||
|
||||
consumer := &ConsumerGroupInstance{
|
||||
InstanceId: "first",
|
||||
MaxPartitionCount: 2,
|
||||
}
|
||||
err := market.AddConsumerInstance(consumer)
|
||||
|
||||
assert.Nil(t, err)
|
||||
time.Sleep(1 * time.Second) // Allow time for background rebalancing
|
||||
market.ShutdownMarket()
|
||||
for adjustment := range market.AdjustmentChan {
|
||||
fmt.Printf("%+v\n", adjustment)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMultipleConsumerInstances(t *testing.T) {
|
||||
market := NewMarket(partitions, 10*time.Second)
|
||||
|
||||
market.AddConsumerInstance(&ConsumerGroupInstance{
|
||||
InstanceId: "first",
|
||||
MaxPartitionCount: 2,
|
||||
})
|
||||
market.AddConsumerInstance(&ConsumerGroupInstance{
|
||||
InstanceId: "second",
|
||||
MaxPartitionCount: 2,
|
||||
})
|
||||
market.AddConsumerInstance(&ConsumerGroupInstance{
|
||||
InstanceId: "third",
|
||||
MaxPartitionCount: 2,
|
||||
})
|
||||
|
||||
time.Sleep(1 * time.Second) // Allow time for background rebalancing
|
||||
market.ShutdownMarket()
|
||||
for adjustment := range market.AdjustmentChan {
|
||||
fmt.Printf("%+v\n", adjustment)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfirmAdjustment(t *testing.T) {
|
||||
market := NewMarket(partitions, 1*time.Second)
|
||||
|
||||
market.AddConsumerInstance(&ConsumerGroupInstance{
|
||||
InstanceId: "first",
|
||||
MaxPartitionCount: 2,
|
||||
})
|
||||
market.AddConsumerInstance(&ConsumerGroupInstance{
|
||||
InstanceId: "second",
|
||||
MaxPartitionCount: 2,
|
||||
})
|
||||
market.AddConsumerInstance(&ConsumerGroupInstance{
|
||||
InstanceId: "third",
|
||||
MaxPartitionCount: 2,
|
||||
})
|
||||
|
||||
go func() {
|
||||
time.Sleep(5 * time.Second) // Allow time for background rebalancing
|
||||
market.ShutdownMarket()
|
||||
}()
|
||||
go func() {
|
||||
time.Sleep(2 * time.Second)
|
||||
market.RemoveConsumerInstance("third")
|
||||
}()
|
||||
|
||||
for adjustment := range market.AdjustmentChan {
|
||||
fmt.Printf("%+v\n", adjustment)
|
||||
market.ConfirmAdjustment(adjustment)
|
||||
}
|
||||
|
||||
}
|
||||
6
weed/mq/sub_coordinator/partition_consumer_mapping.go
Normal file
6
weed/mq/sub_coordinator/partition_consumer_mapping.go
Normal file
@@ -0,0 +1,6 @@
|
||||
package sub_coordinator
|
||||
|
||||
type PartitionConsumerMapping struct {
|
||||
currentMapping *PartitionSlotToConsumerInstanceList
|
||||
prevMappings []*PartitionSlotToConsumerInstanceList
|
||||
}
|
||||
16
weed/mq/sub_coordinator/partition_list.go
Normal file
16
weed/mq/sub_coordinator/partition_list.go
Normal file
@@ -0,0 +1,16 @@
|
||||
package sub_coordinator
|
||||
|
||||
type PartitionSlotToConsumerInstance struct {
|
||||
RangeStart int32
|
||||
RangeStop int32
|
||||
UnixTimeNs int64
|
||||
Broker string
|
||||
AssignedInstanceId ConsumerGroupInstanceId
|
||||
FollowerBroker string
|
||||
}
|
||||
|
||||
type PartitionSlotToConsumerInstanceList struct {
|
||||
PartitionSlots []*PartitionSlotToConsumerInstance
|
||||
RingSize int32
|
||||
Version int64
|
||||
}
|
||||
109
weed/mq/sub_coordinator/sub_coordinator.go
Normal file
109
weed/mq/sub_coordinator/sub_coordinator.go
Normal file
@@ -0,0 +1,109 @@
|
||||
package sub_coordinator
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
cmap "github.com/orcaman/concurrent-map/v2"
|
||||
"github.com/seaweedfs/seaweedfs/weed/filer_client"
|
||||
"github.com/seaweedfs/seaweedfs/weed/pb/mq_pb"
|
||||
"github.com/seaweedfs/seaweedfs/weed/pb/schema_pb"
|
||||
)
|
||||
|
||||
type TopicConsumerGroups struct {
|
||||
// map a consumer group name to a consumer group
|
||||
ConsumerGroups cmap.ConcurrentMap[string, *ConsumerGroup]
|
||||
}
|
||||
|
||||
// SubCoordinator coordinates the instances in the consumer group for one topic.
|
||||
// It is responsible for:
|
||||
// 1. (Maybe) assigning partitions when a consumer instance is up/down.
|
||||
|
||||
type SubCoordinator struct {
|
||||
// map topic name to consumer groups
|
||||
TopicSubscribers cmap.ConcurrentMap[string, *TopicConsumerGroups]
|
||||
FilerClientAccessor *filer_client.FilerClientAccessor
|
||||
}
|
||||
|
||||
func NewSubCoordinator() *SubCoordinator {
|
||||
return &SubCoordinator{
|
||||
TopicSubscribers: cmap.New[*TopicConsumerGroups](),
|
||||
}
|
||||
}
|
||||
|
||||
func (c *SubCoordinator) GetTopicConsumerGroups(topic *schema_pb.Topic, createIfMissing bool) *TopicConsumerGroups {
|
||||
topicName := toTopicName(topic)
|
||||
tcg, _ := c.TopicSubscribers.Get(topicName)
|
||||
if tcg == nil && createIfMissing {
|
||||
tcg = &TopicConsumerGroups{
|
||||
ConsumerGroups: cmap.New[*ConsumerGroup](),
|
||||
}
|
||||
if !c.TopicSubscribers.SetIfAbsent(topicName, tcg) {
|
||||
tcg, _ = c.TopicSubscribers.Get(topicName)
|
||||
}
|
||||
}
|
||||
return tcg
|
||||
}
|
||||
func (c *SubCoordinator) RemoveTopic(topic *schema_pb.Topic) {
|
||||
topicName := toTopicName(topic)
|
||||
c.TopicSubscribers.Remove(topicName)
|
||||
}
|
||||
|
||||
func toTopicName(topic *schema_pb.Topic) string {
|
||||
topicName := topic.Namespace + "." + topic.Name
|
||||
return topicName
|
||||
}
|
||||
|
||||
func (c *SubCoordinator) AddSubscriber(initMessage *mq_pb.SubscriberToSubCoordinatorRequest_InitMessage) (*ConsumerGroup, *ConsumerGroupInstance, error) {
|
||||
tcg := c.GetTopicConsumerGroups(initMessage.Topic, true)
|
||||
cg, _ := tcg.ConsumerGroups.Get(initMessage.ConsumerGroup)
|
||||
if cg == nil {
|
||||
cg = NewConsumerGroup(initMessage.Topic, initMessage.RebalanceSeconds, c.FilerClientAccessor)
|
||||
if cg != nil {
|
||||
tcg.ConsumerGroups.SetIfAbsent(initMessage.ConsumerGroup, cg)
|
||||
}
|
||||
cg, _ = tcg.ConsumerGroups.Get(initMessage.ConsumerGroup)
|
||||
}
|
||||
if cg == nil {
|
||||
return nil, nil, fmt.Errorf("fail to create consumer group %s: topic %s not found", initMessage.ConsumerGroup, initMessage.Topic)
|
||||
}
|
||||
cgi, _ := cg.ConsumerGroupInstances.Get(initMessage.ConsumerGroupInstanceId)
|
||||
if cgi == nil {
|
||||
cgi = NewConsumerGroupInstance(initMessage.ConsumerGroupInstanceId, initMessage.MaxPartitionCount)
|
||||
if !cg.ConsumerGroupInstances.SetIfAbsent(initMessage.ConsumerGroupInstanceId, cgi) {
|
||||
cgi, _ = cg.ConsumerGroupInstances.Get(initMessage.ConsumerGroupInstanceId)
|
||||
}
|
||||
}
|
||||
cgi.MaxPartitionCount = initMessage.MaxPartitionCount
|
||||
cg.Market.AddConsumerInstance(cgi)
|
||||
return cg, cgi, nil
|
||||
}
|
||||
|
||||
func (c *SubCoordinator) RemoveSubscriber(initMessage *mq_pb.SubscriberToSubCoordinatorRequest_InitMessage) {
|
||||
tcg := c.GetTopicConsumerGroups(initMessage.Topic, false)
|
||||
if tcg == nil {
|
||||
return
|
||||
}
|
||||
cg, _ := tcg.ConsumerGroups.Get(initMessage.ConsumerGroup)
|
||||
if cg == nil {
|
||||
return
|
||||
}
|
||||
cg.ConsumerGroupInstances.Remove(initMessage.ConsumerGroupInstanceId)
|
||||
cg.Market.RemoveConsumerInstance(ConsumerGroupInstanceId(initMessage.ConsumerGroupInstanceId))
|
||||
if cg.ConsumerGroupInstances.Count() == 0 {
|
||||
tcg.ConsumerGroups.Remove(initMessage.ConsumerGroup)
|
||||
cg.Shutdown()
|
||||
}
|
||||
if tcg.ConsumerGroups.Count() == 0 {
|
||||
c.RemoveTopic(initMessage.Topic)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *SubCoordinator) OnPartitionChange(topic *schema_pb.Topic, assignments []*mq_pb.BrokerPartitionAssignment) {
|
||||
tcg, _ := c.TopicSubscribers.Get(toTopicName(topic))
|
||||
if tcg == nil {
|
||||
return
|
||||
}
|
||||
for _, cg := range tcg.ConsumerGroups.Items() {
|
||||
cg.OnPartitionListChange(assignments)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user