/*
* Copyright © 2014 Cask Data, 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 co.cask.cdap.common.resource;
import co.cask.cdap.common.discovery.ResolvingDiscoverable;
import co.cask.cdap.common.utils.Networks;
import co.cask.cdap.common.zookeeper.coordination.BalancedAssignmentStrategy;
import co.cask.cdap.common.zookeeper.coordination.PartitionReplica;
import co.cask.cdap.common.zookeeper.coordination.ResourceCoordinator;
import co.cask.cdap.common.zookeeper.coordination.ResourceCoordinatorClient;
import co.cask.cdap.common.zookeeper.coordination.ResourceHandler;
import co.cask.cdap.common.zookeeper.coordination.ResourceRequirement;
import com.google.common.base.Throwables;
import com.google.common.collect.Sets;
import com.google.common.util.concurrent.AbstractIdleService;
import com.google.common.util.concurrent.Service;
import com.google.common.util.concurrent.SettableFuture;
import org.apache.twill.api.ElectionHandler;
import org.apache.twill.common.Cancellable;
import org.apache.twill.discovery.Discoverable;
import org.apache.twill.discovery.DiscoveryService;
import org.apache.twill.discovery.DiscoveryServiceClient;
import org.apache.twill.internal.Services;
import org.apache.twill.internal.zookeeper.LeaderElection;
import org.apache.twill.zookeeper.ZKClient;
import org.apache.twill.zookeeper.ZKClientService;
import org.apache.twill.zookeeper.ZKClients;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.UnknownHostException;
import java.util.Collection;
import java.util.Set;
/**
* A services that automatically balances resource assignments between its instances.
*/
public abstract class ResourceBalancerService extends AbstractIdleService {
private static final Logger LOG = LoggerFactory.getLogger(ResourceBalancerService.class);
private final String serviceName;
private final int partitionCount;
private final LeaderElection election;
private final ResourceCoordinatorClient resourceClient;
private final DiscoveryService discoveryService;
private final SettableFuture<?> completion;
private Cancellable cancelDiscoverable;
private Cancellable cancelResourceHandler;
/**
* Creates instance of {@link ResourceBalancerService}.
* @param serviceName name of the service
* @param partitionCount number of partitions of the resource to balance
* @param zkClient ZooKeeper place to keep metadata for sync; will be further namespaced with service name
* @param discoveryService discovery service to register this service
* @param discoveryServiceClient discovery service client to discover other instances of this service
*/
protected ResourceBalancerService(String serviceName,
int partitionCount,
ZKClientService zkClient,
DiscoveryService discoveryService,
final DiscoveryServiceClient discoveryServiceClient) {
this.serviceName = serviceName;
this.partitionCount = partitionCount;
this.discoveryService = discoveryService;
final ZKClient zk = ZKClients.namespace(zkClient, "/" + serviceName);
this.election =
new LeaderElection(zk, serviceName, new ElectionHandler() {
private ResourceCoordinator coordinator;
@Override
public void leader() {
coordinator = new ResourceCoordinator(zk,
discoveryServiceClient,
new BalancedAssignmentStrategy());
coordinator.startAndWait();
}
@Override
public void follower() {
if (coordinator != null) {
coordinator.stopAndWait();
coordinator = null;
}
}
});
this.resourceClient = new ResourceCoordinatorClient(zk);
this.completion = SettableFuture.create();
}
/**
* Creates an instance of {@link Service} that gets assigned the given partitions.
* @param partitions partitions to process
* @return instance of {@link Service}
*/
protected abstract Service createService(Set<Integer> partitions);
@Override
protected void startUp() throws Exception {
LOG.info("Starting ResourceBalancer {} service...", serviceName);
// We first submit requirement before starting coordinator to make sure all needed paths in ZK are created
ResourceRequirement requirement =
ResourceRequirement.builder(serviceName).addPartitions("", partitionCount, 1).build();
resourceClient.submitRequirement(requirement).get();
Discoverable discoverable = createDiscoverable(serviceName);
cancelDiscoverable = discoveryService.register(ResolvingDiscoverable.of(discoverable));
election.start();
resourceClient.startAndWait();
cancelResourceHandler = resourceClient.subscribe(serviceName,
createResourceHandler(discoverable));
LOG.info("Started ResourceBalancer {} service...", serviceName);
}
@Override
protected void shutDown() throws Exception {
LOG.info("Stopping ResourceBalancer {} service...", serviceName);
Throwable throwable = null;
try {
Services.chainStop(election, resourceClient).get();
} catch (Throwable th) {
throwable = th;
LOG.error("Exception while shutting down {}.", serviceName, th);
}
try {
cancelResourceHandler.cancel();
} catch (Throwable th) {
throwable = th;
LOG.error("Exception while shutting down {}.", serviceName, th);
}
try {
cancelDiscoverable.cancel();
} catch (Throwable th) {
throwable = th;
LOG.error("Exception while shutting down{}.", serviceName, th);
}
if (throwable != null) {
throw Throwables.propagate(throwable);
}
LOG.info("Stopped ResourceBalancer {} service.", serviceName);
}
private ResourceHandler createResourceHandler(Discoverable discoverable) {
return new ResourceHandler(discoverable) {
private Service service;
@Override
public void onChange(Collection<PartitionReplica> partitionReplicas) {
Set<Integer> partitions = Sets.newHashSet();
for (PartitionReplica replica : partitionReplicas) {
partitions.add(Integer.valueOf(replica.getName()));
}
LOG.info("Partitions changed {}, service: {}", partitions, serviceName);
try {
if (service != null) {
service.stopAndWait();
}
if (partitions.isEmpty() || !election.isRunning()) {
service = null;
} else {
service = createService(partitions);
service.startAndWait();
}
} catch (Throwable t) {
LOG.error("Failed to change partitions, service: {}.", serviceName, t);
completion.setException(t);
}
}
@Override
public void finished(Throwable failureCause) {
if (service != null) {
service.stopAndWait();
service = null;
}
}
};
}
private Discoverable createDiscoverable(final String serviceName) {
InetSocketAddress address;
// NOTE: at this moment we are not using port anywhere
int port = Networks.getRandomPort();
try {
address = new InetSocketAddress(InetAddress.getLocalHost(), port);
} catch (UnknownHostException e) {
address = new InetSocketAddress(port);
}
final InetSocketAddress finalAddress = address;
return new Discoverable() {
@Override
public String getName() {
return serviceName;
}
@Override
public InetSocketAddress getSocketAddress() {
return finalAddress;
}
};
}
}