Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
2 changes: 1 addition & 1 deletion checkstyle/suppressions.xml
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@
<suppress checks="ClassFanOutComplexity"
files="Worker(|Test).java"/>
<suppress checks="MethodLength"
files="(KafkaConfigBackingStore|Values|IncrementalCooperativeAssignor).java"/>
files="(DistributedHerder|KafkaConfigBackingStore|Values|IncrementalCooperativeAssignor).java"/>
<suppress checks="ParameterNumber"
files="Worker(SinkTask|SourceTask|Coordinator).java"/>
<suppress checks="ParameterNumber"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@
import org.apache.kafka.connect.connector.policy.ConnectorClientConfigOverridePolicy;
import org.apache.kafka.connect.connector.policy.ConnectorClientConfigRequest;
import org.apache.kafka.connect.errors.NotFoundException;
import org.apache.kafka.connect.runtime.distributed.ClusterConfigState;
import org.apache.kafka.connect.runtime.isolation.PluginType;
import org.apache.kafka.connect.runtime.isolation.Plugins;
import org.apache.kafka.connect.runtime.rest.entities.ActiveTopicsInfo;
Expand All @@ -44,6 +43,7 @@
import org.apache.kafka.connect.runtime.rest.errors.BadRequestException;
import org.apache.kafka.connect.sink.SinkConnector;
import org.apache.kafka.connect.source.SourceConnector;
import org.apache.kafka.connect.storage.ClusterConfigState;
import org.apache.kafka.connect.storage.ConfigBackingStore;
import org.apache.kafka.connect.storage.Converter;
import org.apache.kafka.connect.storage.HeaderConverter;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
import org.apache.kafka.connect.json.JsonConverter;
import org.apache.kafka.connect.json.JsonConverterConfig;
import org.apache.kafka.connect.runtime.ConnectMetrics.MetricGroup;
import org.apache.kafka.connect.runtime.distributed.ClusterConfigState;
import org.apache.kafka.connect.storage.ClusterConfigState;
import org.apache.kafka.connect.runtime.errors.DeadLetterQueueReporter;
import org.apache.kafka.connect.runtime.errors.ErrorHandlingMetrics;
import org.apache.kafka.connect.runtime.errors.ErrorReporter;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@
import org.apache.kafka.connect.header.ConnectHeaders;
import org.apache.kafka.connect.header.Headers;
import org.apache.kafka.connect.runtime.ConnectMetrics.MetricGroup;
import org.apache.kafka.connect.runtime.distributed.ClusterConfigState;
import org.apache.kafka.connect.storage.ClusterConfigState;
import org.apache.kafka.connect.runtime.errors.RetryWithToleranceOperator;
import org.apache.kafka.connect.runtime.errors.Stage;
import org.apache.kafka.connect.runtime.errors.WorkerErrantRecordReporter;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
import org.apache.kafka.clients.consumer.KafkaConsumer;
import org.apache.kafka.common.TopicPartition;
import org.apache.kafka.connect.errors.IllegalWorkerStateException;
import org.apache.kafka.connect.runtime.distributed.ClusterConfigState;
import org.apache.kafka.connect.storage.ClusterConfigState;
import org.apache.kafka.connect.sink.ErrantRecordReporter;
import org.apache.kafka.connect.sink.SinkTaskContext;
import org.slf4j.Logger;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@
import org.apache.kafka.connect.header.Headers;
import org.apache.kafka.connect.runtime.ConnectMetrics.MetricGroup;
import org.apache.kafka.connect.runtime.SubmittedRecords.SubmittedRecord;
import org.apache.kafka.connect.runtime.distributed.ClusterConfigState;
import org.apache.kafka.connect.storage.ClusterConfigState;
import org.apache.kafka.connect.runtime.errors.RetryWithToleranceOperator;
import org.apache.kafka.connect.runtime.errors.Stage;
import org.apache.kafka.connect.runtime.errors.ToleranceType;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
*/
package org.apache.kafka.connect.runtime;

import org.apache.kafka.connect.runtime.distributed.ClusterConfigState;
import org.apache.kafka.connect.storage.ClusterConfigState;
import org.apache.kafka.connect.source.SourceTaskContext;
import org.apache.kafka.connect.storage.OffsetStorageReader;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
import org.apache.kafka.connect.runtime.TargetState;
import org.apache.kafka.connect.runtime.TaskStatus;
import org.apache.kafka.connect.runtime.Worker;
import org.apache.kafka.connect.storage.PrivilegedWriteException;
import org.apache.kafka.connect.runtime.rest.InternalRequestSignature;
import org.apache.kafka.connect.runtime.rest.RestClient;
import org.apache.kafka.connect.runtime.rest.entities.ConnectorInfo;
Expand All @@ -61,6 +62,7 @@
import org.apache.kafka.connect.source.ExactlyOnceSupport;
import org.apache.kafka.connect.source.SourceConnector;
import org.apache.kafka.connect.source.SourceTask;
import org.apache.kafka.connect.storage.ClusterConfigState;
import org.apache.kafka.connect.storage.ConfigBackingStore;
import org.apache.kafka.connect.storage.StatusBackingStore;
import org.apache.kafka.connect.util.Callback;
Expand Down Expand Up @@ -189,6 +191,7 @@ public class DistributedHerder extends AbstractHerder implements Runnable {
// herder's main thread.
private Set<String> connectorTargetStateChanges = new HashSet<>();
private boolean needsReconfigRebalance;
private volatile boolean fencedFromConfigTopic;
private volatile int generation;
private volatile long scheduledRebalance;
private volatile SecretKey sessionKey;
Expand Down Expand Up @@ -288,6 +291,7 @@ public DistributedHerder(DistributedConfig config,
configState = ClusterConfigState.EMPTY;
rebalanceResolved = true; // If we still need to follow up after a rebalance occurred, starting up tasks
needsReconfigRebalance = false;
fencedFromConfigTopic = false;
canReadConfigs = true; // We didn't try yet, but Configs are readable until proven otherwise
scheduledRebalance = Long.MAX_VALUE;
keyExpiration = Long.MAX_VALUE;
Expand Down Expand Up @@ -372,18 +376,36 @@ public void tick() {
return;
}

if (fencedFromConfigTopic) {
if (isLeader()) {
// We were accidentally fenced out, possibly by a zombie leader
try {
log.debug("Reclaiming write privileges for config topic after being fenced out");
configBackingStore.claimWritePrivileges();
fencedFromConfigTopic = false;
log.debug("Successfully reclaimed write privileges for config topic after being fenced out");
} catch (Exception e) {
log.warn("Unable to claim write privileges for config topic. Will backoff and possibly retry if still the leader", e);
backoff(CONFIG_TOPIC_WRITE_PRIVILEGES_BACKOFF_MS);
return;
}
} else {
log.trace("Relinquished write privileges for config topic after being fenced out, since worker is no longer the leader of the cluster");
// We were meant to be fenced out because we fell out of the group and a new leader was elected
fencedFromConfigTopic = false;
}
}

long now = time.milliseconds();

if (checkForKeyRotation(now)) {
log.debug("Distributing new session key");
keyExpiration = Long.MAX_VALUE;
try {
configBackingStore.putSessionKey(new SessionKey(
keyGenerator.generateKey(),
now
));
SessionKey newSessionKey = new SessionKey(keyGenerator.generateKey(), now);
writeToConfigTopicAsLeader(() -> configBackingStore.putSessionKey(newSessionKey));
} catch (Exception e) {
log.info("Failed to write new session key to config topic; forcing a read to the end of the config topic before possibly retrying");
log.info("Failed to write new session key to config topic; forcing a read to the end of the config topic before possibly retrying", e);
canReadConfigs = false;
return;
}
Expand Down Expand Up @@ -492,6 +514,12 @@ private boolean checkForKeyRotation(long now) {
SecretKey key;
long expiration;
synchronized (this) {
// This happens on startup; the snapshot contains the session key,
// but no callback in the config update listener has been fired for it yet.
if (sessionKey == null && configState.sessionKey() != null) {
sessionKey = configState.sessionKey().key();
keyExpiration = configState.sessionKey().creationTimestamp() + keyRotationIntervalMs;
}
key = sessionKey;
expiration = keyExpiration;
}
Expand All @@ -511,10 +539,6 @@ private boolean checkForKeyRotation(long now) {
+ "than required by current worker configuration. Distributing new key now.");
return true;
}
} else if (key == null && configState.sessionKey() != null) {
// This happens on startup for follower workers; the snapshot contains the session key,
// but no callback in the config update listener has been fired for it yet.
sessionKey = configState.sessionKey().key();
}
}
return false;
Expand Down Expand Up @@ -836,7 +860,7 @@ public void deleteConnectorConfig(final String connName, final Callback<Created<
callback.onCompletion(new NotFoundException("Connector " + connName + " not found"), null);
} else {
log.trace("Removing connector config {} {}", connName, configState.connectors());
configBackingStore.removeConnectorConfig(connName);
writeToConfigTopicAsLeader(() -> configBackingStore.removeConnectorConfig(connName));
callback.onCompletion(null, new Created<>(false, null));
}
return null;
Expand Down Expand Up @@ -1008,7 +1032,7 @@ public void putConnectorConfig(final String connName, final Map<String, String>
}

log.trace("Submitting connector config {} {} {}", connName, allowReplace, configState.connectors());
configBackingStore.putConnectorConfig(connName, config);
writeToConfigTopicAsLeader(() -> configBackingStore.putConnectorConfig(connName, config));

// Note that we use the updated connector config despite the fact that we don't have an updated
// snapshot yet. The existing task info should still be accurate.
Expand Down Expand Up @@ -1107,7 +1131,7 @@ public void putTaskConfigs(final String connName, final List<Map<String, String>
else if (!configState.contains(connName))
callback.onCompletion(new NotFoundException("Connector " + connName + " not found"), null);
else {
configBackingStore.putTaskConfigs(connName, configs);
writeToConfigTopicAsLeader(() -> configBackingStore.putTaskConfigs(connName, configs));
callback.onCompletion(null, null);
}
return null;
Expand Down Expand Up @@ -1328,6 +1352,25 @@ private String leaderUrl() {
return assignment.leaderUrl();
}

/**
* Perform an action that writes to the config topic, and if it fails because the leader has been fenced out, make note of that
* fact so that we can try to reclaim write ownership (if still the leader of the cluster) in a subsequent iteration of the tick loop.
* Note that it is not necessary to wrap every write to the config topic in this method, only the writes that should be performed
* exclusively by the leader. For example, {@link ConfigBackingStore#putTargetState(String, TargetState)} does not require this
* method, as it can be invoked by any worker in the cluster.
* @param write the action that writes to the config topic, such as {@link ConfigBackingStore#putSessionKey(SessionKey)} or
* {@link ConfigBackingStore#putConnectorConfig(String, Map)}.
*/
private void writeToConfigTopicAsLeader(Runnable write) {
try {
write.run();
} catch (PrivilegedWriteException e) {
log.warn("Failed to write to config topic as leader; will rejoin group if necessary and, if still leader, attempt to reclaim write privileges for the config topic", e);
fencedFromConfigTopic = true;
throw new ConnectException("Failed to write to config topic; this may be due to a transient error and the request can be safely retried", e);
}
}

/**
* Handle post-assignment operations, either trying to resolve issues that kept assignment from completing, getting
* this node into sync and its work started.
Expand Down Expand Up @@ -1700,7 +1743,7 @@ private void reconfigureConnector(final String connName, final Callback<Void> cb
if (changed) {
List<Map<String, String>> rawTaskProps = reverseTransform(connName, configState, taskProps);
if (isLeader()) {
configBackingStore.putTaskConfigs(connName, rawTaskProps);
writeToConfigTopicAsLeader(() -> configBackingStore.putTaskConfigs(connName, rawTaskProps));
cb.onCompletion(null, null);
} else {
// We cannot forward the request on the same thread because this reconfiguration can happen as a result of connector
Expand Down Expand Up @@ -2009,12 +2052,20 @@ public void onAssigned(ExtendedAssignment assignment, int generation) {
herderMetrics.rebalanceStarted(time.milliseconds());
}

// Delete the statuses of all connectors and tasks removed prior to the start of this rebalance. This
// has to be done after the rebalance completes to avoid race conditions as the previous generation
// attempts to change the state to UNASSIGNED after tasks have been stopped.
if (isLeader()) {
// Delete the statuses of all connectors and tasks removed prior to the start of this rebalance. This
// has to be done after the rebalance completes to avoid race conditions as the previous generation
// attempts to change the state to UNASSIGNED after tasks have been stopped.
updateDeletedConnectorStatus();
updateDeletedTaskStatus();
// As the leader, we're now allowed to write directly to the config topic for important things like
// connector configs, session keys, and task count records
try {
configBackingStore.claimWritePrivileges();
} catch (Exception e) {
fencedFromConfigTopic = true;
log.error("Unable to claim write privileges for config topic after being elected leader during rebalance", e);
}
}

// We *must* interrupt any poll() call since this could occur when the poll starts, and we might then
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

import org.apache.kafka.common.utils.CircularIterator;
import org.apache.kafka.common.utils.LogContext;
import org.apache.kafka.connect.storage.ClusterConfigState;
import org.apache.kafka.connect.util.ConnectorTaskId;
import org.slf4j.Logger;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import org.apache.kafka.connect.runtime.distributed.WorkerCoordinator.ConnectorsAndTasks;
import org.apache.kafka.connect.runtime.distributed.WorkerCoordinator.WorkerLoad;
import org.apache.kafka.connect.util.ConnectUtils;
import org.apache.kafka.connect.storage.ClusterConfigState;
import org.apache.kafka.connect.util.ConnectorTaskId;
import org.slf4j.Logger;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
import org.apache.kafka.common.utils.LogContext;
import org.apache.kafka.common.utils.Time;
import org.apache.kafka.common.utils.Timer;
import org.apache.kafka.connect.storage.ClusterConfigState;
import org.apache.kafka.connect.storage.ConfigBackingStore;
import org.apache.kafka.connect.util.ConnectorTaskId;
import org.slf4j.Logger;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
import org.apache.kafka.connect.runtime.SourceConnectorConfig;
import org.apache.kafka.connect.runtime.TargetState;
import org.apache.kafka.connect.runtime.Worker;
import org.apache.kafka.connect.runtime.distributed.ClusterConfigState;
import org.apache.kafka.connect.storage.ClusterConfigState;
import org.apache.kafka.connect.runtime.rest.InternalRequestSignature;
import org.apache.kafka.connect.runtime.rest.entities.ConfigInfos;
import org.apache.kafka.connect.runtime.rest.entities.ConnectorInfo;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.kafka.connect.runtime.distributed;
package org.apache.kafka.connect.storage;

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Moved to a different package to leverage package-private fields and methods while testing other classes in this package (such as the KafkaConfigBackingStore class).

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

org.apache.kafka.connect.storage is a public package, and AFAICS this change in public API wasn't mentioned in KIP-618.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

That package contains a mix of public and private API. The public parts are all documented in the Javadocs for the package and include the Converter interface and the SimpleHeaderConverter class; the private parts (which include everything in the connect/runtime module) contain the ClusterConfigState class.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

OK. I missed that this package move on its own won't make this class a public API.


import org.apache.kafka.common.config.provider.ConfigProvider;
import org.apache.kafka.connect.runtime.SessionKey;
Expand Down Expand Up @@ -46,12 +46,12 @@ public class ClusterConfigState {

private final long offset;
private final SessionKey sessionKey;
private final Map<String, Integer> connectorTaskCounts;
private final Map<String, Map<String, String>> connectorConfigs;
private final Map<String, TargetState> connectorTargetStates;
private final Map<ConnectorTaskId, Map<String, String>> taskConfigs;
private final Set<String> inconsistentConnectors;
private final WorkerConfigTransformer configTransformer;
final Map<String, Integer> connectorTaskCounts;
final Map<String, Map<String, String>> connectorConfigs;
final Map<String, TargetState> connectorTargetStates;
final Map<ConnectorTaskId, Map<String, String>> taskConfigs;
final Set<String> inconsistentConnectors;

public ClusterConfigState(long offset,
SessionKey sessionKey,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@
import org.apache.kafka.connect.runtime.RestartRequest;
import org.apache.kafka.connect.runtime.SessionKey;
import org.apache.kafka.connect.runtime.TargetState;
import org.apache.kafka.connect.runtime.distributed.ClusterConfigState;
import org.apache.kafka.connect.util.ConnectorTaskId;

import java.util.Collection;
Expand Down Expand Up @@ -90,6 +89,10 @@ public interface ConfigBackingStore {
*/
void putTargetState(String connector, TargetState state);

/**
* Store a new {@link SessionKey} that can be used to validate internal (i.e., non-user-triggered) inter-worker communication.
* @param sessionKey the session key to store
*/
void putSessionKey(SessionKey sessionKey);

/**
Expand All @@ -98,6 +101,15 @@ public interface ConfigBackingStore {
*/
void putRestartRequest(RestartRequest restartRequest);

/**
* Prepare to write to the backing config store. May be required by some implementations (such as those that only permit a single
* writer at a time across a cluster of workers) before performing mutating operations like writing configurations, target states, etc.
* The default implementation is a no-op; it is the responsibility of the implementing class to override this and document any expectations for
* when it must be invoked.
*/
default void claimWritePrivileges() {
}

/**
* Set an update listener to get notifications when there are config/target state
* changes.
Expand Down
Loading