Browse Source
Part of KIP-714. Reviewers: Andrew Schofield <aschofield@confluent.io>, Philip Nee <pnee@confluent.io>, Kirk True <ktrue@confluent.io>, Walker Carlson <wcarlson@confluent.io>, Matthias J. Sax <matthias@confluent.io>pull/14583/head
Apoorv Mittal
1 year ago
committed by
GitHub
2 changed files with 277 additions and 0 deletions
@ -0,0 +1,166 @@ |
|||||||
|
/* |
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one or more |
||||||
|
* contributor license agreements. See the NOTICE file distributed with |
||||||
|
* this work for additional information regarding copyright ownership. |
||||||
|
* The ASF licenses this file to You under the Apache License, Version 2.0 |
||||||
|
* (the "License"); you may not use this file except in compliance with |
||||||
|
* the License. You may obtain a copy of the License at |
||||||
|
* |
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
* |
||||||
|
* Unless required by applicable law or agreed to in writing, software |
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||||
|
* See the License for the specific language governing permissions and |
||||||
|
* limitations under the License. |
||||||
|
*/ |
||||||
|
|
||||||
|
package org.apache.kafka.common.telemetry; |
||||||
|
|
||||||
|
import org.apache.kafka.common.utils.Utils; |
||||||
|
|
||||||
|
import java.util.Arrays; |
||||||
|
import java.util.Collections; |
||||||
|
import java.util.EnumMap; |
||||||
|
import java.util.List; |
||||||
|
import java.util.Map; |
||||||
|
|
||||||
|
/** |
||||||
|
* State that helps determine where client exists in the telemetry state i.e. subscribe->wait->push loop. |
||||||
|
*/ |
||||||
|
public enum ClientTelemetryState { |
||||||
|
|
||||||
|
/** |
||||||
|
* Client needs subscription from the broker. |
||||||
|
*/ |
||||||
|
SUBSCRIPTION_NEEDED, |
||||||
|
|
||||||
|
/** |
||||||
|
* Network I/O is in progress to retrieve subscription. |
||||||
|
*/ |
||||||
|
SUBSCRIPTION_IN_PROGRESS, |
||||||
|
|
||||||
|
/** |
||||||
|
* Awaiting telemetry interval for pushing metrics to broker. |
||||||
|
*/ |
||||||
|
PUSH_NEEDED, |
||||||
|
|
||||||
|
/** |
||||||
|
* Network I/O in progress for pushing metrics payload. |
||||||
|
*/ |
||||||
|
PUSH_IN_PROGRESS, |
||||||
|
|
||||||
|
/** |
||||||
|
* Need to push the terminal metrics payload. |
||||||
|
*/ |
||||||
|
TERMINATING_PUSH_NEEDED, |
||||||
|
|
||||||
|
/** |
||||||
|
* Network I/O in progress for pushing terminal metrics payload. |
||||||
|
*/ |
||||||
|
TERMINATING_PUSH_IN_PROGRESS, |
||||||
|
|
||||||
|
/** |
||||||
|
* No more work should be performed, telemetry client terminated. |
||||||
|
*/ |
||||||
|
TERMINATED; |
||||||
|
|
||||||
|
private final static Map<ClientTelemetryState, List<ClientTelemetryState>> VALID_NEXT_STATES = new EnumMap<>(ClientTelemetryState.class); |
||||||
|
|
||||||
|
static { |
||||||
|
/* |
||||||
|
If clients needs a subscription, then issue telemetry API to fetch subscription from broker. |
||||||
|
|
||||||
|
However, it's still possible that client doesn't get very far before terminating. |
||||||
|
*/ |
||||||
|
VALID_NEXT_STATES.put( |
||||||
|
SUBSCRIPTION_NEEDED, Arrays.asList(SUBSCRIPTION_IN_PROGRESS, TERMINATED)); |
||||||
|
|
||||||
|
/* |
||||||
|
If client is finished waiting for subscription, then client is ready to push the telemetry. |
||||||
|
But, it's possible that no telemetry metrics are requested, hence client should go back to |
||||||
|
subscription needed state i.e. requesting the next updated subscription. |
||||||
|
|
||||||
|
However, it's still possible that client doesn't get very far before terminating. |
||||||
|
*/ |
||||||
|
VALID_NEXT_STATES.put(SUBSCRIPTION_IN_PROGRESS, Arrays.asList(PUSH_NEEDED, |
||||||
|
SUBSCRIPTION_NEEDED, TERMINATING_PUSH_NEEDED, TERMINATED)); |
||||||
|
|
||||||
|
/* |
||||||
|
If client transitions out of this state, then client should proceed to push the metrics. |
||||||
|
But, if the push fails (network issues, the subscription changed, etc.) then client should |
||||||
|
go back to subscription needed state and request the next subscription. |
||||||
|
|
||||||
|
However, it's still possible that client doesn't get very far before terminating. |
||||||
|
*/ |
||||||
|
VALID_NEXT_STATES.put(PUSH_NEEDED, Arrays.asList(PUSH_IN_PROGRESS, SUBSCRIPTION_NEEDED, |
||||||
|
TERMINATING_PUSH_NEEDED, TERMINATED)); |
||||||
|
|
||||||
|
/* |
||||||
|
A successful push should transition client to push needed which sends the next telemetry |
||||||
|
metrics after the elapsed wait interval. But, if the push fails (network issues, the |
||||||
|
subscription changed, etc.) then client should go back to subscription needed state and |
||||||
|
request the next subscription. |
||||||
|
|
||||||
|
However, it's still possible that client doesn't get very far before terminating. |
||||||
|
*/ |
||||||
|
VALID_NEXT_STATES.put( |
||||||
|
PUSH_IN_PROGRESS, Arrays.asList(PUSH_NEEDED, SUBSCRIPTION_NEEDED, TERMINATING_PUSH_NEEDED, |
||||||
|
TERMINATED)); |
||||||
|
|
||||||
|
/* |
||||||
|
If client is moving out of this state, then try to send last metrics push. |
||||||
|
|
||||||
|
However, it's still possible that client doesn't get very far before terminating. |
||||||
|
*/ |
||||||
|
VALID_NEXT_STATES.put( |
||||||
|
TERMINATING_PUSH_NEEDED, Arrays.asList(TERMINATING_PUSH_IN_PROGRESS, TERMINATED)); |
||||||
|
|
||||||
|
/* |
||||||
|
Client should only be transited to terminated state. |
||||||
|
*/ |
||||||
|
VALID_NEXT_STATES.put(TERMINATING_PUSH_IN_PROGRESS, Collections.singletonList(TERMINATED)); |
||||||
|
|
||||||
|
/* |
||||||
|
Client should never be able to transition out of terminated state. |
||||||
|
*/ |
||||||
|
VALID_NEXT_STATES.put(TERMINATED, Collections.emptyList()); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Validates that the <code>newState</code> is one of the valid transition from the current |
||||||
|
* {@code TelemetryState}. |
||||||
|
* |
||||||
|
* @param newState State into which the telemetry client requesting to transition; must be |
||||||
|
* non-<code>null</code> |
||||||
|
* @return {@code TelemetryState} <code>newState</code> if validation succeeds. Returning |
||||||
|
* <code>newState</code> helps state assignment chaining. |
||||||
|
* @throws IllegalStateException if the state transition validation fails. |
||||||
|
*/ |
||||||
|
|
||||||
|
public ClientTelemetryState validateTransition(ClientTelemetryState newState) { |
||||||
|
List<ClientTelemetryState> allowableStates = VALID_NEXT_STATES.get(this); |
||||||
|
|
||||||
|
if (allowableStates != null && allowableStates.contains(newState)) { |
||||||
|
return newState; |
||||||
|
} |
||||||
|
|
||||||
|
// State transition validation failed, construct error message and throw exception.
|
||||||
|
String validStatesClause; |
||||||
|
if (allowableStates != null && !allowableStates.isEmpty()) { |
||||||
|
validStatesClause = String.format("the valid telemetry state transitions from %s are: %s", |
||||||
|
this, |
||||||
|
Utils.join(allowableStates, ", ")); |
||||||
|
} else { |
||||||
|
validStatesClause = String.format("there are no valid telemetry state transitions from %s", this); |
||||||
|
} |
||||||
|
|
||||||
|
String message = String.format("Invalid telemetry state transition from %s to %s; %s", |
||||||
|
this, |
||||||
|
newState, |
||||||
|
validStatesClause); |
||||||
|
|
||||||
|
throw new IllegalStateException(message); |
||||||
|
} |
||||||
|
|
||||||
|
} |
@ -0,0 +1,111 @@ |
|||||||
|
/* |
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one or more |
||||||
|
* contributor license agreements. See the NOTICE file distributed with |
||||||
|
* this work for additional information regarding copyright ownership. |
||||||
|
* The ASF licenses this file to You under the Apache License, Version 2.0 |
||||||
|
* (the "License"); you may not use this file except in compliance with |
||||||
|
* the License. You may obtain a copy of the License at |
||||||
|
* |
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
* |
||||||
|
* Unless required by applicable law or agreed to in writing, software |
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||||
|
* See the License for the specific language governing permissions and |
||||||
|
* limitations under the License. |
||||||
|
*/ |
||||||
|
package org.apache.kafka.common.telemetry; |
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test; |
||||||
|
import org.junit.jupiter.api.function.Executable; |
||||||
|
|
||||||
|
import java.util.ArrayList; |
||||||
|
import java.util.Arrays; |
||||||
|
import java.util.Collections; |
||||||
|
import java.util.List; |
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertThrows; |
||||||
|
|
||||||
|
public class ClientTelemetryStateTest { |
||||||
|
|
||||||
|
@Test |
||||||
|
public void testValidateTransitionForSubscriptionNeeded() { |
||||||
|
List<ClientTelemetryState> validStates = new ArrayList<>(); |
||||||
|
validStates.add(ClientTelemetryState.SUBSCRIPTION_IN_PROGRESS); |
||||||
|
validStates.add(ClientTelemetryState.TERMINATED); |
||||||
|
|
||||||
|
testValidateTransition(ClientTelemetryState.SUBSCRIPTION_NEEDED, validStates); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void testValidateTransitionForSubscriptionInProgress() { |
||||||
|
List<ClientTelemetryState> validStates = new ArrayList<>(); |
||||||
|
validStates.add(ClientTelemetryState.PUSH_NEEDED); |
||||||
|
validStates.add(ClientTelemetryState.SUBSCRIPTION_NEEDED); |
||||||
|
validStates.add(ClientTelemetryState.TERMINATING_PUSH_NEEDED); |
||||||
|
validStates.add(ClientTelemetryState.TERMINATED); |
||||||
|
|
||||||
|
testValidateTransition(ClientTelemetryState.SUBSCRIPTION_IN_PROGRESS, validStates); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void testValidateTransitionForPushNeeded() { |
||||||
|
List<ClientTelemetryState> validStates = new ArrayList<>(); |
||||||
|
validStates.add(ClientTelemetryState.PUSH_IN_PROGRESS); |
||||||
|
validStates.add(ClientTelemetryState.SUBSCRIPTION_NEEDED); |
||||||
|
validStates.add(ClientTelemetryState.TERMINATING_PUSH_NEEDED); |
||||||
|
validStates.add(ClientTelemetryState.TERMINATED); |
||||||
|
|
||||||
|
testValidateTransition(ClientTelemetryState.PUSH_NEEDED, validStates); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void testValidateTransitionForPushInProgress() { |
||||||
|
List<ClientTelemetryState> validStates = new ArrayList<>(); |
||||||
|
validStates.add(ClientTelemetryState.PUSH_NEEDED); |
||||||
|
validStates.add(ClientTelemetryState.SUBSCRIPTION_NEEDED); |
||||||
|
validStates.add(ClientTelemetryState.TERMINATING_PUSH_NEEDED); |
||||||
|
validStates.add(ClientTelemetryState.TERMINATED); |
||||||
|
|
||||||
|
testValidateTransition(ClientTelemetryState.PUSH_IN_PROGRESS, validStates); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void testValidateTransitionForTerminating() { |
||||||
|
List<ClientTelemetryState> validStates = new ArrayList<>(); |
||||||
|
validStates.add(ClientTelemetryState.TERMINATING_PUSH_IN_PROGRESS); |
||||||
|
validStates.add(ClientTelemetryState.TERMINATED); |
||||||
|
|
||||||
|
testValidateTransition(ClientTelemetryState.TERMINATING_PUSH_NEEDED, validStates); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void testValidateTransitionForTerminatingPushInProgress() { |
||||||
|
testValidateTransition(ClientTelemetryState.TERMINATING_PUSH_IN_PROGRESS, |
||||||
|
Collections.singletonList(ClientTelemetryState.TERMINATED)); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void testValidateTransitionForTerminated() { |
||||||
|
// There's no transitioning out of the terminated state
|
||||||
|
testValidateTransition(ClientTelemetryState.TERMINATED, Collections.emptyList()); |
||||||
|
} |
||||||
|
|
||||||
|
private void testValidateTransition(ClientTelemetryState oldState, List<ClientTelemetryState> validStates) { |
||||||
|
for (ClientTelemetryState newState : validStates) { |
||||||
|
oldState.validateTransition(newState); |
||||||
|
} |
||||||
|
|
||||||
|
// Copy value to a new list for modification.
|
||||||
|
List<ClientTelemetryState> invalidStates = new ArrayList<>(Arrays.asList(ClientTelemetryState.values())); |
||||||
|
// Remove the valid states from the list of all states, leaving only the invalid.
|
||||||
|
invalidStates.removeAll(validStates); |
||||||
|
|
||||||
|
for (ClientTelemetryState newState : invalidStates) { |
||||||
|
Executable e = () -> oldState.validateTransition(newState); |
||||||
|
String unexpectedSuccessMessage = "Should have thrown an IllegalTelemetryStateException for transitioning from " + oldState + " to " + newState; |
||||||
|
assertThrows(IllegalStateException.class, e, unexpectedSuccessMessage); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
} |
Loading…
Reference in new issue