/**
* Copyright 2014 Confluent Inc.
*
* Licensed 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 io.confluent.kafka.schemaregistry.zookeeper;
import org.I0Itec.zkclient.IZkDataListener;
import org.I0Itec.zkclient.IZkStateListener;
import org.I0Itec.zkclient.ZkClient;
import org.I0Itec.zkclient.exception.ZkNoNodeException;
import org.I0Itec.zkclient.exception.ZkNodeExistsException;
import org.apache.zookeeper.Watcher;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import io.confluent.kafka.schemaregistry.exceptions.SchemaRegistryException;
import io.confluent.kafka.schemaregistry.exceptions.SchemaRegistryTimeoutException;
import io.confluent.kafka.schemaregistry.storage.KafkaSchemaRegistry;
import io.confluent.kafka.schemaregistry.exceptions.SchemaRegistryStoreException;
import kafka.utils.ZkUtils;
public class ZookeeperMasterElector {
private static final Logger log = LoggerFactory.getLogger(ZookeeperMasterElector.class);
private static final String MASTER_PATH = "/schema_registry_master";
private final ZkClient zkClient;
private final ZkUtils zkUtils;
private final SchemaRegistryIdentity myIdentity;
private final String myIdentityString;
private final KafkaSchemaRegistry schemaRegistry;
public ZookeeperMasterElector(ZkUtils zkUtils,
SchemaRegistryIdentity myIdentity,
KafkaSchemaRegistry schemaRegistry,
boolean isEligibleForMasterElection)
throws SchemaRegistryTimeoutException, SchemaRegistryStoreException {
this.zkClient = zkUtils.zkClient();
this.zkUtils = zkUtils;
this.myIdentity = myIdentity;
try {
this.myIdentityString = myIdentity.toJson();
} catch (IOException e) {
throw new SchemaRegistryStoreException(String.format(
"Error while serializing schema registry identity %s to json", myIdentity.toString()), e);
}
this.schemaRegistry = schemaRegistry;
zkClient.subscribeStateChanges(new SessionExpirationListener(isEligibleForMasterElection));
zkClient.subscribeDataChanges(MASTER_PATH,
new MasterChangeListener(isEligibleForMasterElection));
if (isEligibleForMasterElection) {
electMaster();
} else {
readCurrentMaster();
}
}
public void close() {
zkClient.unsubscribeAll();
}
public void electMaster() throws
SchemaRegistryStoreException, SchemaRegistryTimeoutException {
SchemaRegistryIdentity masterIdentity = null;
try {
zkUtils.createEphemeralPathExpectConflict(MASTER_PATH, myIdentityString,
zkUtils.defaultAcls(MASTER_PATH));
log.info("Successfully elected the new master: " + myIdentityString);
masterIdentity = myIdentity;
schemaRegistry.setMaster(masterIdentity);
} catch (ZkNodeExistsException znee) {
readCurrentMaster();
}
}
public void readCurrentMaster()
throws SchemaRegistryTimeoutException, SchemaRegistryStoreException {
SchemaRegistryIdentity masterIdentity = null;
// If someone else has written the path, read the new master back
try {
String masterIdentityString = zkUtils.readData(MASTER_PATH)._1();
try {
masterIdentity = SchemaRegistryIdentity.fromJson(masterIdentityString);
} catch (IOException ioe) {
log.error("Can't parse schema registry identity json string " + masterIdentityString);
}
} catch (ZkNoNodeException znne) {
// NOTE: masterIdentity is already initialized to null. The master will then be updated to
// null so register requests directed to this node can throw the right error code back
}
schemaRegistry.setMaster(masterIdentity);
}
private class MasterChangeListener implements IZkDataListener {
private final boolean isEligibleForMasterElection;
public MasterChangeListener(boolean isEligibleForMasterElection) {
this.isEligibleForMasterElection = isEligibleForMasterElection;
}
/**
* Called when the master information stored in ZooKeeper has changed (or, in some cases,
* deleted).
*
* <p>>** Note ** The ZkClient library has unexpected behavior - under certain conditions,
* handleDataChange may be called instead of handleDataDeleted when the ephemeral node holding
* MASTER_PATH is deleted. Therefore it is necessary to call electMaster() here to ensure
* every eligible node participates in election after a deletion event.
*
* @throws Exception On any error.
*/
@Override
public void handleDataChange(String dataPath, Object data) {
try {
if (isEligibleForMasterElection) {
electMaster();
} else {
readCurrentMaster();
}
} catch (SchemaRegistryException e) {
log.error("Error while reading the schema registry master", e);
}
}
/**
* Called when the master information stored in zookeeper has been deleted. Try to elect as the
* leader
*
* @throws Exception On any error.
*/
@Override
public void handleDataDeleted(String dataPath) throws Exception {
if (isEligibleForMasterElection) {
electMaster();
} else {
schemaRegistry.setMaster(null);
}
}
}
private class SessionExpirationListener implements IZkStateListener {
private final boolean isEligibleForMasterElection;
public SessionExpirationListener(boolean isEligibleForMasterElection) {
this.isEligibleForMasterElection = isEligibleForMasterElection;
}
@Override
public void handleStateChanged(Watcher.Event.KeeperState state) {
// do nothing, since zkclient will do reconnect for us.
}
/**
* Called after the zookeeper session has expired and a new session has been created. You would
* have to re-create any ephemeral nodes here.
*
* @throws Exception On any error.
*/
@Override
public void handleNewSession() throws Exception {
if (isEligibleForMasterElection) {
electMaster();
} else {
readCurrentMaster();
}
}
@Override
public void handleSessionEstablishmentError(Throwable t) throws Exception {
log.error("Failed to re-establish Zookeeper connection: ", t);
throw new SchemaRegistryStoreException("Couldn't establish Zookeeper connection", t);
}
}
}