/*
* Copyright 2014 the original author or authors.
*
* 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 org.springframework.xd.dirt.container.store;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.concurrent.atomic.AtomicReference;
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.recipes.cache.ChildData;
import org.apache.curator.framework.recipes.cache.PathChildrenCache;
import org.apache.curator.framework.recipes.cache.PathChildrenCacheEvent;
import org.apache.curator.framework.recipes.cache.PathChildrenCacheListener;
import org.apache.curator.utils.ThreadUtils;
import org.apache.zookeeper.CreateMode;
import org.apache.zookeeper.data.Stat;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationEvent;
import org.springframework.context.ApplicationListener;
import org.springframework.context.event.ContextClosedEvent;
import org.springframework.context.event.ContextStoppedEvent;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.util.Assert;
import org.springframework.xd.dirt.cluster.Container;
import org.springframework.xd.dirt.cluster.DetailedContainer;
import org.springframework.xd.dirt.cluster.NoSuchContainerException;
import org.springframework.xd.dirt.module.store.ModuleMetadata;
import org.springframework.xd.dirt.module.store.ZooKeeperModuleMetadataRepository;
import org.springframework.xd.dirt.util.PagingUtility;
import org.springframework.xd.dirt.zookeeper.Paths;
import org.springframework.xd.dirt.zookeeper.ZooKeeperConnection;
import org.springframework.xd.dirt.zookeeper.ZooKeeperUtils;
/**
* ZooKeeper backed repository for runtime info about Containers. This
* implementation uses {@link PathChildrenCache} to cache container
* info from ZooKeeper. The cache is used for all <em>reads</em>, whereas
* all <em>writes</em> are written directly to ZooKeeper. This means
* that a read that immediately follows a write <em>may</em> return
* {@code null} if the cache has not been updated yet.
*
* @author Mark Fisher
* @author David Turanski
* @author Ilayaperumal Gopinathan
* @author Patrick Peralta
*/
public class ZooKeeperContainerRepository implements ContainerRepository, ApplicationListener<ApplicationEvent> {
/**
* ZooKeeper connection.
*/
private final ZooKeeperConnection zkConnection;
private final ZooKeeperModuleMetadataRepository zkModuleMetadataRepository;
/**
* Paging support for this repository.
*/
private final PagingUtility<Container> pagingUtility = new PagingUtility<Container>();
/**
* Paging support for detailed containers.
*/
private final PagingUtility<DetailedContainer> detailedContainersUtil = new PagingUtility<DetailedContainer>();
/**
* Atomic reference to the {@link PathChildrenCache} for containers
* under the {@link Paths#CONTAINERS} node. This reference should
* <em>not</em> be used directly; instead use {@link #ensureCache}
* to ensure the cache is initialized.
*
* @see #ensureCache
*/
private final AtomicReference<PathChildrenCache> cacheRef = new AtomicReference<PathChildrenCache>();
/**
* Construct a {@code ZooKeeperContainerRepository}.
*
* @param zkConnection the ZooKeeper connection
* @param moduleMetadataRepository the module metadata repository
*/
@Autowired
public ZooKeeperContainerRepository(ZooKeeperConnection zkConnection,
ZooKeeperModuleMetadataRepository moduleMetadataRepository) {
this.zkConnection = zkConnection;
this.zkModuleMetadataRepository = moduleMetadataRepository;
}
/**
* {@inheritDoc}
*/
@Override
public void onApplicationEvent(ApplicationEvent event) {
if (event instanceof ContextStoppedEvent || event instanceof ContextClosedEvent) {
closeCache();
}
}
/**
* Close the {@link PathChildrenCache container cache} and null out
* the {@link #cacheRef atmoic reference}.
*/
private void closeCache() {
PathChildrenCache cache = cacheRef.get();
if (cache != null) {
try {
cache.close();
}
catch (Exception e) {
// ignore exception on close
}
finally {
cacheRef.compareAndSet(cache, null);
}
}
}
/**
* Return a {@link PathChildrenCache} for containers, creating and
* initializing a new instance if necessary.
*
* @return a {@code PathChildrenCache} for containers
* @throws java.lang.IllegalStateException if the cache could not be initialized
* (likely due to a ZooKeeper connection error)
*/
private PathChildrenCache ensureCache() {
if (cacheRef.get() == null) {
synchronized (cacheRef) {
if (cacheRef.get() == null) {
CuratorFramework client = zkConnection.getClient();
PathChildrenCache cache = new PathChildrenCache(client, Paths.CONTAINERS,
true, ThreadUtils.newThreadFactory("ContainerCache"));
cache.getListenable().addListener(new PathChildrenCacheListener() {
@Override
public void childEvent(CuratorFramework client, PathChildrenCacheEvent event) {
// shut down the cache if ZooKeeper connection goes away
if (event.getType() == PathChildrenCacheEvent.Type.CONNECTION_SUSPENDED ||
event.getType() == PathChildrenCacheEvent.Type.CONNECTION_LOST) {
closeCache();
}
}
});
try {
Paths.ensurePath(client, Paths.CONTAINERS);
cacheRef.set(cache);
cache.start(PathChildrenCache.StartMode.BUILD_INITIAL_CACHE);
}
catch (Exception e) {
try {
cache.close();
}
catch (Exception ce) {
// ignore exception on close
}
finally {
cacheRef.compareAndSet(cache, null);
}
throw ZooKeeperUtils.wrapThrowable(e);
}
}
}
}
PathChildrenCache cache = cacheRef.get();
Assert.state(cache != null, "Container cache not initialized " +
"(likely as a result of a ZooKeeper connection error)");
return cache;
}
/**
* {@inheritDoc}
*/
@Override
public Iterable<Container> findAll(Sort sort) {
// todo: add support for sort
return findAll();
}
/**
* {@inheritDoc}
*/
@Override
public Page<Container> findAll(Pageable pageable) {
return pagingUtility.getPagedData(pageable, findAll());
}
/**
* {@inheritDoc}
*/
@Override
public <S extends Container> S save(S entity) {
CuratorFramework client = zkConnection.getClient();
String path = Paths.build(Paths.CONTAINERS, entity.getName());
try {
client.create().creatingParentsIfNeeded().withMode(CreateMode.EPHEMERAL)
.forPath(path, ZooKeeperUtils.mapToBytes(entity.getAttributes()));
return entity;
}
catch (Exception e) {
throw ZooKeeperUtils.wrapThrowable(e);
}
}
/**
* {@inheritDoc}
*/
@Override
public void update(Container entity) {
CuratorFramework client = zkConnection.getClient();
String path = Paths.build(Paths.CONTAINERS, entity.getName());
try {
Stat stat = client.checkExists().forPath(path);
if (stat == null) {
throw new NoSuchContainerException("Could not find container with id " + entity.getName());
}
client.setData().forPath(path, ZooKeeperUtils.mapToBytes(entity.getAttributes()));
}
catch (Exception e) {
throw ZooKeeperUtils.wrapThrowable(e, e.getMessage());
}
}
/**
* {@inheritDoc}
*/
@Override
public <S extends Container> Iterable<S> save(Iterable<S> entities) {
List<S> results = new ArrayList<S>();
for (S entity : entities) {
results.add(save(entity));
}
return results;
}
/**
* {@inheritDoc}
*/
@Override
public Container findOne(String id) {
Container container = null;
String containerPath = path(id);
ChildData childData = ensureCache().getCurrentData(containerPath);
byte[] data = null;
if (childData != null) {
data = childData.getData();
}
if (data != null) {
container = new Container(id, ZooKeeperUtils.bytesToMap(data));
}
return container;
}
/**
* {@inheritDoc}
*/
@Override
public boolean exists(String id) {
return ensureCache().getCurrentData(path(id)) != null;
}
/**
* {@inheritDoc}
*/
@Override
public List<Container> findAll() {
List<Container> results = new ArrayList<Container>();
List<ChildData> children = new ArrayList<ChildData>(ensureCache().getCurrentData());
// sort containers by node creation date (cluster join time)
Collections.sort(children, new Comparator<ChildData>() {
@Override
public int compare(ChildData c1, ChildData c2) {
long t1 = c1.getStat().getCtime();
long t2 = c2.getStat().getCtime();
return (t1 < t2) ? -1 : ((t1 == t2) ? 0 : 1);
}
});
for (ChildData childData : children) {
results.add(findOne(Paths.stripPath(childData.getPath())));
}
return results;
}
/**
* {@inheritDoc}
*/
@Override
public Iterable<Container> findAll(Iterable<String> ids) {
List<Container> results = new ArrayList<Container>();
for (String id : ids) {
Container entity = findOne(id);
if (entity != null) {
results.add(entity);
}
}
return results;
}
/**
* Find all the {@link DetailedContainer}s in the XD cluster.
*
* @param pageable the pagination info
* @return the paged list of {@link DetailedContainer}s
*/
@Override
public Page<DetailedContainer> findAllRuntimeContainers(Pageable pageable) {
List<DetailedContainer> results = new ArrayList<DetailedContainer>();
List<Container> containers = this.findAll();
for (Container container : containers) {
DetailedContainer runtimeContainer = new DetailedContainer(container);
List<ModuleMetadata> deployedModules = zkModuleMetadataRepository.findAllByContainerId(container.getName());
runtimeContainer.setDeployedModules(deployedModules);
runtimeContainer.setDeploymentSize(deployedModules.size());
results.add(runtimeContainer);
}
return detailedContainersUtil.getPagedData(pageable, results);
}
/**
* {@inheritDoc}
*/
@Override
public long count() {
return ensureCache().getCurrentData().size();
}
/**
* {@inheritDoc}
*/
@Override
public void delete(String id) {
// Container metadata is "deleted" when a Container departs
}
/**
* {@inheritDoc}
*/
@Override
public void delete(Container entity) {
// Container metadata is "deleted" when a Container departs
}
/**
* {@inheritDoc}
*/
@Override
public void delete(Iterable<? extends Container> entities) {
// Container metadata is "deleted" when a Container departs
}
/**
* {@inheritDoc}
*/
@Override
public void deleteAll() {
// Container metadata is "deleted" when a Container departs
}
/**
* {@inheritDoc}
*/
@Override
public Iterable<Container> findAllInRange(String from, boolean fromInclusive, String to,
boolean toInclusive) {
throw new UnsupportedOperationException("Auto-generated method stub");
}
/**
* Return the path for a container.
*
* @param id container id
* @return path for the container
* @see Paths#build
*/
private String path(String id) {
return Paths.build(Paths.CONTAINERS, id);
}
}