-
Notifications
You must be signed in to change notification settings - Fork 1.8k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Introduce ConsumerGroups #1083
Closed
Closed
Introduce ConsumerGroups #1083
Changes from all commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
f4d69c2
First attempt
dim b77a34f
Consistent API
dim 06b4c79
Code standards
dim 13386af
Added session.Stop(), added example
dim f30795e
A slighly better retry
dim 3149279
Avoid math.Round
dim 7078c49
SyncGroupResponse.MemberAssignment may be empty, don't rely on it
dim File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,129 @@ | ||
package sarama | ||
|
||
import ( | ||
"math" | ||
"sort" | ||
) | ||
|
||
// BalanceStrategyPlan is the results of any BalanceStrategy.Plan attempt. | ||
// It contains an allocation of topic/partitions by memberID in the form of | ||
// a `memberID -> topic -> partitions` map. | ||
type BalanceStrategyPlan map[string]map[string][]int32 | ||
|
||
// Add assigns a topic with a number partitions to a member. | ||
func (p BalanceStrategyPlan) Add(memberID, topic string, partitions ...int32) { | ||
if len(partitions) == 0 { | ||
return | ||
} | ||
if _, ok := p[memberID]; !ok { | ||
p[memberID] = make(map[string][]int32, 1) | ||
} | ||
p[memberID][topic] = append(p[memberID][topic], partitions...) | ||
} | ||
|
||
// -------------------------------------------------------------------- | ||
|
||
// BalanceStrategy is used to balance topics and partitions | ||
// across memebers of a consumer group | ||
type BalanceStrategy interface { | ||
// Name uniquely identifies the strategy. | ||
Name() string | ||
|
||
// Plan accepts a map of `memberID -> metadata` and a map of `topic -> partitions` | ||
// and returns a distribution plan. | ||
Plan(members map[string]ConsumerGroupMemberMetadata, topics map[string][]int32) (BalanceStrategyPlan, error) | ||
} | ||
|
||
// -------------------------------------------------------------------- | ||
|
||
// BalanceStrategyRange is the default and assigns partitions as ranges to consumer group members. | ||
// Example with one topic T with six partitions (0..5) and two members (M1, M2) : | ||
// M1: {T: [0, 1, 2]} | ||
// M2: {T: [3, 4, 5]} | ||
var BalanceStrategyRange = &balanceStrategy{ | ||
name: "range", | ||
coreFn: func(plan BalanceStrategyPlan, memberIDs []string, topic string, partitions []int32) { | ||
step := float64(len(partitions)) / float64(len(memberIDs)) | ||
|
||
for i, memberID := range memberIDs { | ||
pos := float64(i) | ||
min := int(math.Floor(pos*step + 0.5)) | ||
max := int(math.Floor((pos+1)*step + 0.5)) | ||
plan.Add(memberID, topic, partitions[min:max]...) | ||
} | ||
}, | ||
} | ||
|
||
// BalanceStrategyRoundRobin assigns partitions to members in alternating order. | ||
// Example with topic T with six partitions (0..5) and two members (M1, M2): | ||
// M1: {T: [0, 2, 4]} | ||
// M2: {T: [1, 3, 5]} | ||
var BalanceStrategyRoundRobin = &balanceStrategy{ | ||
name: "roundrobin", | ||
coreFn: func(plan BalanceStrategyPlan, memberIDs []string, topic string, partitions []int32) { | ||
for i, part := range partitions { | ||
memberID := memberIDs[i%len(memberIDs)] | ||
plan.Add(memberID, topic, part) | ||
} | ||
}, | ||
} | ||
|
||
// -------------------------------------------------------------------- | ||
|
||
type balanceStrategy struct { | ||
name string | ||
coreFn func(plan BalanceStrategyPlan, memberIDs []string, topic string, partitions []int32) | ||
} | ||
|
||
// Name implements BalanceStrategy. | ||
func (s *balanceStrategy) Name() string { return s.name } | ||
|
||
// Balance implements BalanceStrategy. | ||
func (s *balanceStrategy) Plan(members map[string]ConsumerGroupMemberMetadata, topics map[string][]int32) (BalanceStrategyPlan, error) { | ||
// Build members by topic map | ||
mbt := make(map[string][]string) | ||
for memberID, meta := range members { | ||
for _, topic := range meta.Topics { | ||
mbt[topic] = append(mbt[topic], memberID) | ||
} | ||
} | ||
|
||
// Sort members for each topic | ||
for topic, memberIDs := range mbt { | ||
sort.Sort(&balanceStrategySortable{ | ||
topic: topic, | ||
memberIDs: memberIDs, | ||
}) | ||
} | ||
|
||
// Assemble plan | ||
plan := make(BalanceStrategyPlan, len(members)) | ||
for topic, memberIDs := range mbt { | ||
s.coreFn(plan, memberIDs, topic, topics[topic]) | ||
} | ||
return plan, nil | ||
} | ||
|
||
type balanceStrategySortable struct { | ||
topic string | ||
memberIDs []string | ||
} | ||
|
||
func (p balanceStrategySortable) Len() int { return len(p.memberIDs) } | ||
func (p balanceStrategySortable) Swap(i, j int) { | ||
p.memberIDs[i], p.memberIDs[j] = p.memberIDs[j], p.memberIDs[i] | ||
} | ||
func (p balanceStrategySortable) Less(i, j int) bool { | ||
return balanceStrategyHashValue(p.topic, p.memberIDs[i]) < balanceStrategyHashValue(p.topic, p.memberIDs[j]) | ||
} | ||
|
||
func balanceStrategyHashValue(vv ...string) uint32 { | ||
h := uint32(2166136261) | ||
for _, s := range vv { | ||
for _, c := range s { | ||
h ^= uint32(c) | ||
h *= 16777619 | ||
} | ||
} | ||
return h | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,102 @@ | ||
package sarama | ||
|
||
import ( | ||
"reflect" | ||
"testing" | ||
) | ||
|
||
func TestBalanceStrategyRange(t *testing.T) { | ||
tests := []struct { | ||
members map[string][]string | ||
topics map[string][]int32 | ||
expected BalanceStrategyPlan | ||
}{ | ||
{ | ||
members: map[string][]string{"M1": {"T1", "T2"}, "M2": {"T1", "T2"}}, | ||
topics: map[string][]int32{"T1": {0, 1, 2, 3}, "T2": {0, 1, 2, 3}}, | ||
expected: BalanceStrategyPlan{ | ||
"M1": map[string][]int32{"T1": {0, 1}, "T2": {2, 3}}, | ||
"M2": map[string][]int32{"T1": {2, 3}, "T2": {0, 1}}, | ||
}, | ||
}, | ||
{ | ||
members: map[string][]string{"M1": {"T1", "T2"}, "M2": {"T1", "T2"}}, | ||
topics: map[string][]int32{"T1": {0, 1, 2}, "T2": {0, 1, 2}}, | ||
expected: BalanceStrategyPlan{ | ||
"M1": map[string][]int32{"T1": {0, 1}, "T2": {2}}, | ||
"M2": map[string][]int32{"T1": {2}, "T2": {0, 1}}, | ||
}, | ||
}, | ||
{ | ||
members: map[string][]string{"M1": {"T1"}, "M2": {"T1", "T2"}}, | ||
topics: map[string][]int32{"T1": {0, 1}, "T2": {0, 1}}, | ||
expected: BalanceStrategyPlan{ | ||
"M1": map[string][]int32{"T1": {0}}, | ||
"M2": map[string][]int32{"T1": {1}, "T2": {0, 1}}, | ||
}, | ||
}, | ||
} | ||
|
||
strategy := BalanceStrategyRange | ||
if strategy.Name() != "range" { | ||
t.Errorf("Unexpected stategy name\nexpected: range\nactual: %v", strategy.Name()) | ||
} | ||
|
||
for _, test := range tests { | ||
members := make(map[string]ConsumerGroupMemberMetadata) | ||
for memberID, topics := range test.members { | ||
members[memberID] = ConsumerGroupMemberMetadata{Topics: topics} | ||
} | ||
|
||
actual, err := strategy.Plan(members, test.topics) | ||
if err != nil { | ||
t.Errorf("Unexpected error %v", err) | ||
} else if !reflect.DeepEqual(actual, test.expected) { | ||
t.Errorf("Plan does not match expectation\nexpected: %#v\nactual: %#v", test.expected, actual) | ||
} | ||
} | ||
} | ||
|
||
func TestBalanceStrategyRoundRobin(t *testing.T) { | ||
tests := []struct { | ||
members map[string][]string | ||
topics map[string][]int32 | ||
expected BalanceStrategyPlan | ||
}{ | ||
{ | ||
members: map[string][]string{"M1": {"T1", "T2"}, "M2": {"T1", "T2"}}, | ||
topics: map[string][]int32{"T1": {0, 1, 2, 3}, "T2": {0, 1, 2, 3}}, | ||
expected: BalanceStrategyPlan{ | ||
"M1": map[string][]int32{"T1": {0, 2}, "T2": {1, 3}}, | ||
"M2": map[string][]int32{"T1": {1, 3}, "T2": {0, 2}}, | ||
}, | ||
}, | ||
{ | ||
members: map[string][]string{"M1": {"T1", "T2"}, "M2": {"T1", "T2"}}, | ||
topics: map[string][]int32{"T1": {0, 1, 2}, "T2": {0, 1, 2}}, | ||
expected: BalanceStrategyPlan{ | ||
"M1": map[string][]int32{"T1": {0, 2}, "T2": {1}}, | ||
"M2": map[string][]int32{"T1": {1}, "T2": {0, 2}}, | ||
}, | ||
}, | ||
} | ||
|
||
strategy := BalanceStrategyRoundRobin | ||
if strategy.Name() != "roundrobin" { | ||
t.Errorf("Unexpected stategy name\nexpected: range\nactual: %v", strategy.Name()) | ||
} | ||
|
||
for _, test := range tests { | ||
members := make(map[string]ConsumerGroupMemberMetadata) | ||
for memberID, topics := range test.members { | ||
members[memberID] = ConsumerGroupMemberMetadata{Topics: topics} | ||
} | ||
|
||
actual, err := strategy.Plan(members, test.topics) | ||
if err != nil { | ||
t.Errorf("Unexpected error %v", err) | ||
} else if !reflect.DeepEqual(actual, test.expected) { | ||
t.Errorf("Plan does not match expectation\nexpected: %#v\nactual: %#v", test.expected, actual) | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here's an example of that. Topics can be distributed unevenly even under the same group ID.