Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -574,7 +574,11 @@ private class JoinGroupResponseHandler extends CoordinatorResponseHandler<JoinGr
@Override
public void handle(JoinGroupResponse joinResponse, RequestFuture<ByteBuffer> future) {
Errors error = joinResponse.error();
if (error == Errors.NONE) {
if (isProtocolTypeInconsistent(joinResponse.data().protocolType())) {
log.debug("JoinGroup failed: Received inconsistent ProtocolType ({})",
joinResponse.data().protocolType());
future.raise(Errors.INCONSISTENT_GROUP_PROTOCOL);
} else if (error == Errors.NONE) {
log.debug("Received successful JoinGroup response: {}", joinResponse);
sensors.joinSensor.record(response.requestLatencyMs());

Expand Down Expand Up @@ -654,6 +658,8 @@ private RequestFuture<ByteBuffer> onJoinFollower() {
new SyncGroupRequestData()
.setGroupId(rebalanceConfig.groupId)
.setMemberId(generation.memberId)
.setProtocolType(protocolType())
.setProtocolName(generation.protocol)
.setGroupInstanceId(this.rebalanceConfig.groupInstanceId.orElse(null))
.setGenerationId(generation.generationId)
.setAssignments(Collections.emptyList())
Expand Down Expand Up @@ -681,6 +687,8 @@ private RequestFuture<ByteBuffer> onJoinLeader(JoinGroupResponse joinResponse) {
new SyncGroupRequestData()
.setGroupId(rebalanceConfig.groupId)
.setMemberId(generation.memberId)
.setProtocolType(protocolType())
.setProtocolName(generation.protocol)
.setGroupInstanceId(this.rebalanceConfig.groupInstanceId.orElse(null))
.setGenerationId(generation.generationId)
.setAssignments(groupAssignmentList)
Expand All @@ -704,7 +712,12 @@ private class SyncGroupResponseHandler extends CoordinatorResponseHandler<SyncGr
public void handle(SyncGroupResponse syncResponse,
RequestFuture<ByteBuffer> future) {
Errors error = syncResponse.error();
if (error == Errors.NONE) {
if (isProtocolTypeInconsistent(syncResponse.data.protocolType())

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the error code should take precedence over this validation. Perhaps we do this only when the error is NONE? Similarly for the JoinGroup response.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right. Let me do this.

|| isProtocolNameInconsistent(syncResponse.data.protocolName())) {
log.debug("SyngGroup failed: Received inconsistent ProtocolType ({}) and/or ProtocolName ({})",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: I'd suggest breaking this into two separate cases so that the user doesn't have to guess what was inconsistent. Also, can we include the expected value?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That makes sense. I will also include the expected value.

syncResponse.data.protocolType(), syncResponse.data.protocolName());
future.raise(Errors.INCONSISTENT_GROUP_PROTOCOL);
} else if (error == Errors.NONE) {
sensors.syncSensor.record(response.requestLatencyMs());
future.complete(ByteBuffer.wrap(syncResponse.data.assignment()));
} else {
Expand Down Expand Up @@ -887,6 +900,14 @@ protected synchronized void requestRejoin() {
this.rejoinNeeded = true;
}

private boolean isProtocolTypeInconsistent(String protocolType) {
return protocolType != null && !protocolType.equals(protocolType());
}

private boolean isProtocolNameInconsistent(String protocolName) {
return protocolName != null && !protocolName.equals(generation().protocol);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we change Generation.protocol to protocolName as well?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Definitely. Good catch!

}

/**
* Close the coordinator, waiting if needed to send LeaveGroup.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ public String toString() {
public static final String UNKNOWN_MEMBER_ID = "";
public static final int UNKNOWN_GENERATION_ID = -1;
public static final String UNKNOWN_PROTOCOL = "";
public static final String UNKNOWN_PROTOCOL_TYPE = null;

private static final int MAX_GROUP_INSTANCE_ID_LENGTH = 249;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,10 @@ public Map<String, ByteBuffer> groupAssignments() {
return groupAssignments;
}

public boolean areProtocolTypeAndNamePresent() {
Comment thread
dajac marked this conversation as resolved.
Outdated
return version() >= 5 && (data.protocolType() == null || data.protocolName() == null);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The version check is probably not necessary. Also, shouldn't we be checking for non-null entries?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that we need the version check. ProtocolType and ProtocolName are null by default thus they will be null here for all the versions oldest than version 5 and we must accept them, right? However, for newer version, we want to ensure that both fields are provided.

Regarding the non-null entries, the verification (== provided values match the group's ones) is done in the GroupCoordinator when the fields are provided. Does it answer your question?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe I'm missing something, but the function name suggests that it should return true if both the protocol type and name are non-null. The current logic returns true if either field is null. Are we missing a !?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh... The name is indeed misleading. I have reworked this part a bit. Now, the function is called areMandatoryProtocolTypeAndNamePresent. It returns true for any version above 5, and verifies that both fields are non-null for versions from 5 and above. The call is the Api layer has been updated accordingly. Apparently, my unit tests were broken as well as I was not calling the right function. Good catch!

}

public static SyncGroupRequest parse(ByteBuffer buffer, short version) {
return new SyncGroupRequest(ApiKeys.SYNC_GROUP.parseRequest(version, buffer), version);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ protected Struct toStruct(short version) {
}

public static SyncGroupResponse parse(ByteBuffer buffer, short version) {
return new SyncGroupResponse(ApiKeys.SYNC_GROUP.parseResponse(version, buffer));
return new SyncGroupResponse(ApiKeys.SYNC_GROUP.parseResponse(version, buffer), version);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,9 @@
// with assigned id.
//
// Version 6 is the first flexible version.
"validVersions": "0-6",
//
// Version 7 is the same as version 6.
"validVersions": "0-7",
"flexibleVersions": "6+",
"fields": [
{ "name": "GroupId", "type": "string", "versions": "0+", "entityType": "groupId",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,9 @@
// Version 5 is bumped to apply group.instance.id to identify member across restarts.
//
// Version 6 is the first flexible version.
"validVersions": "0-6",
//
// Starting from version 7, the broker sends back the Protocol Type to the client (KIP-599).

@twmb twmb Feb 2, 2020

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

KIP-559 (and elsewhere)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch! Thanks!

"validVersions": "0-7",
"flexibleVersions": "6+",
"fields": [
{ "name": "ThrottleTimeMs", "type": "int32", "versions": "2+", "ignorable": true,
Expand All @@ -38,6 +40,9 @@
"about": "The error code, or 0 if there was no error." },
{ "name": "GenerationId", "type": "int32", "versions": "0+", "default": "-1",
"about": "The generation ID of the group." },
{ "name": "ProtocolType", "type": "string", "versions": "7+",
"nullableVersions": "7+", "default": "null", "ignorable": true,
"about": "The group protocol name." },
{ "name": "ProtocolName", "type": "string", "versions": "0+",
"about": "The group protocol selected by the coordinator." },
{ "name": "Leader", "type": "string", "versions": "0+",
Expand Down
12 changes: 11 additions & 1 deletion clients/src/main/resources/common/message/SyncGroupRequest.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,11 @@
// Starting from version 3, we add a new field called groupInstanceId to indicate member identity across restarts.
//
// Version 4 is the first flexible version.
"validVersions": "0-4",
//
// Starting from version 5, the client sends the Protocol Type and the Protocol Name
// to the broker (KIP-599). THe broker will reject the request if they are inconsistent
Comment thread
dajac marked this conversation as resolved.
Outdated
// with the Type and Name known by the broker.
"validVersions": "0-5",
"flexibleVersions": "4+",
"fields": [
{ "name": "GroupId", "type": "string", "versions": "0+", "entityType": "groupId",
Expand All @@ -34,6 +38,12 @@
{ "name": "GroupInstanceId", "type": "string", "versions": "3+",
"nullableVersions": "3+", "default": "null",
"about": "The unique identifier of the consumer instance provided by end user." },
{ "name": "ProtocolType", "type": "string", "versions": "5+",
"nullableVersions": "5+", "default": "null", "ignorable": true,
"about": "The group protocol type." },
{ "name": "ProtocolName", "type": "string", "versions": "5+",
"nullableVersions": "5+", "default": "null", "ignorable": true,
"about": "The group protocol name." },
{ "name": "Assignments", "type": "[]SyncGroupRequestAssignment", "versions": "0+",
"about": "Each assignment.", "fields": [
{ "name": "MemberId", "type": "string", "versions": "0+",
Expand Down
11 changes: 10 additions & 1 deletion clients/src/main/resources/common/message/SyncGroupResponse.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,22 @@
// Starting from version 3, syncGroupRequest supports a new field called groupInstanceId to indicate member identity across restarts.
//
// Version 4 is the first flexible version.
"validVersions": "0-4",
//
// Starting from version 5, the broker sends back the Protocol Type and the Protocol Name
// to the client (KIP-599).
"validVersions": "0-5",
"flexibleVersions": "4+",
"fields": [
{ "name": "ThrottleTimeMs", "type": "int32", "versions": "1+", "ignorable": true,
"about": "The duration in milliseconds for which the request was throttled due to a quota violation, or zero if the request did not violate any quota." },
{ "name": "ErrorCode", "type": "int16", "versions": "0+",
"about": "The error code, or 0 if there was no error." },
{ "name": "ProtocolType", "type": "string", "versions": "5+",
"nullableVersions": "5+", "default": "null", "ignorable": true,
"about": "The group protocol type." },
{ "name": "ProtocolName", "type": "string", "versions": "5+",
"nullableVersions": "5+", "default": "null", "ignorable": true,
"about": "The group protocol name." },
{ "name": "Assignment", "type": "bytes", "versions": "0+",
"about": "The member assignment." }
]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import org.apache.kafka.common.Node;
import org.apache.kafka.common.errors.AuthenticationException;
import org.apache.kafka.common.errors.FencedInstanceIdException;
import org.apache.kafka.common.errors.InconsistentGroupProtocolException;
import org.apache.kafka.common.errors.UnknownMemberIdException;
import org.apache.kafka.common.errors.WakeupException;
import org.apache.kafka.common.internals.ClusterResourceListeners;
Expand Down Expand Up @@ -85,6 +86,8 @@ public class AbstractCoordinatorTest {
private static final int REQUEST_TIMEOUT_MS = 40000;
private static final String GROUP_ID = "dummy-group";
private static final String METRIC_GROUP_PREFIX = "consumer";
private static final String PROTOCOL_TYPE = "dummy";
private static final String PROTOCOL_NAME = "dummy-subprotocol";

private Node node;
private Metrics metrics;
Expand Down Expand Up @@ -360,6 +363,63 @@ public void testJoinGroupRequestWithFencedInstanceIdException() {
assertFalse(future.isRetriable());
}

@Test
public void testJoinGroupProtocolTypeAndName() {
String wrongProtocolType = "wrong-type";
String wrongProtocolName = "wrong-name";

// No Protocol Type in both JoinGroup and SyncGroup responses
assertTrue(joinGroupWithProtocolTypeAndName(null, null, null));

// Protocol Type in both JoinGroup and SyncGroup responses
assertTrue(joinGroupWithProtocolTypeAndName(PROTOCOL_TYPE, PROTOCOL_TYPE, PROTOCOL_NAME));

// Wrong protocol type in the JoinGroupResponse
assertThrows(InconsistentGroupProtocolException.class,
() -> joinGroupWithProtocolTypeAndName("wrong", null, null));

// Correct protocol type in the JoinGroupResponse
// Wrong protocol type in the SyncGroupResponse
// Correct protocol name in the SyncGroupResponse
assertThrows(InconsistentGroupProtocolException.class,
() -> joinGroupWithProtocolTypeAndName(PROTOCOL_TYPE, wrongProtocolType, PROTOCOL_NAME));

// Correct protocol type in the JoinGroupResponse
// Correct protocol type in the SyncGroupResponse
// Wrong protocol name in the SyncGroupResponse
assertThrows(InconsistentGroupProtocolException.class,
() -> joinGroupWithProtocolTypeAndName(PROTOCOL_TYPE, PROTOCOL_TYPE, wrongProtocolName));
}

private boolean joinGroupWithProtocolTypeAndName(String joinGroupResponseProtocolType,
String syncGroupResponseProtocolType,
String syncGroupResponseProtocolName) {
setupCoordinator();
mockClient.reset();
mockClient.prepareResponse(groupCoordinatorResponse(node, Errors.NONE));
coordinator.ensureCoordinatorReady(mockTime.timer(0));

mockClient.prepareResponse(body -> {
if (!(body instanceof JoinGroupRequest)) {
return false;
}
JoinGroupRequest joinGroupRequest = (JoinGroupRequest) body;
return joinGroupRequest.data().protocolType().equals(PROTOCOL_TYPE);
}, joinGroupFollowerResponse(defaultGeneration, memberId,
"memberid", Errors.NONE, joinGroupResponseProtocolType));

mockClient.prepareResponse(body -> {
if (!(body instanceof SyncGroupRequest)) {
return false;
}
SyncGroupRequest syncGroupRequest = (SyncGroupRequest) body;
return syncGroupRequest.data.protocolType().equals(PROTOCOL_TYPE)
&& syncGroupRequest.data.protocolName().equals(PROTOCOL_NAME);
}, syncGroupResponse(Errors.NONE, syncGroupResponseProtocolType, syncGroupResponseProtocolName));

return coordinator.joinGroupIfNeeded(mockTime.timer(5000L));
}

@Test
public void testSyncGroupRequestWithFencedInstanceIdException() {
setupCoordinator();
Expand Down Expand Up @@ -946,12 +1006,24 @@ private HeartbeatResponse heartbeatResponse(Errors error) {
return new HeartbeatResponse(new HeartbeatResponseData().setErrorCode(error.code()));
}

private JoinGroupResponse joinGroupFollowerResponse(int generationId, String memberId, String leaderId, Errors error) {
private JoinGroupResponse joinGroupFollowerResponse(int generationId,
String memberId,
String leaderId,
Errors error) {
return joinGroupFollowerResponse(generationId, memberId, leaderId, error, null);
}

private JoinGroupResponse joinGroupFollowerResponse(int generationId,
String memberId,
String leaderId,
Errors error,
String protocolType) {
return new JoinGroupResponse(
new JoinGroupResponseData()
.setErrorCode(error.code())
.setGenerationId(generationId)
.setProtocolName("dummy-subprotocol")
.setProtocolType(protocolType)
.setProtocolName(PROTOCOL_NAME)
.setMemberId(memberId)
.setLeader(leaderId)
.setMembers(Collections.emptyList())
Expand All @@ -964,9 +1036,17 @@ private JoinGroupResponse joinGroupResponse(Errors error) {
}

private SyncGroupResponse syncGroupResponse(Errors error) {
return syncGroupResponse(error, null, null);
}

private SyncGroupResponse syncGroupResponse(Errors error,
String protocolType,
String protocolName) {
return new SyncGroupResponse(
new SyncGroupResponseData()
.setErrorCode(error.code())
.setProtocolType(protocolType)
.setProtocolName(protocolName)
.setAssignment(new byte[0])
);
}
Expand All @@ -992,14 +1072,14 @@ public DummyCoordinator(GroupRebalanceConfig rebalanceConfig,

@Override
protected String protocolType() {
return "dummy";
return PROTOCOL_TYPE;
}

@Override
protected JoinGroupRequestData.JoinGroupRequestProtocolCollection metadata() {
return new JoinGroupRequestData.JoinGroupRequestProtocolCollection(
Collections.singleton(new JoinGroupRequestData.JoinGroupRequestProtocol()
.setName("dummy-subprotocol")
.setName(PROTOCOL_NAME)
.setMetadata(EMPTY_DATA.array())).iterator()
);
}
Expand Down
Loading