/**
* diqube: Distributed Query Base.
*
* Copyright (C) 2015 Bastian Gloeckle
*
* This file is part of diqube.
*
* diqube is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.diqube.consensus;
import java.io.IOException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import javax.annotation.PostConstruct;
import org.diqube.context.AutoInstatiate;
import org.diqube.util.Pair;
import org.diqube.util.Triple;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Sets;
import com.google.common.reflect.ClassPath;
import com.google.common.reflect.ClassPath.ClassInfo;
import io.atomix.copycat.Operation;
import io.atomix.copycat.server.Commit;
/**
* Manages all interfaces/classes that have a {@link ConsensusStateMachine} annotation.
*
* @author Bastian Gloeckle
*/
@AutoInstatiate
public class ConsensusStateMachineManager {
private static final Logger logger = LoggerFactory.getLogger(ConsensusStateMachineManager.class);
private static final String BASE_PKG = "org.diqube";
/**
* Triple: Interface class, Method Name, Parameter classes of target method.
*/
private Map<Class<? extends Operation<?>>, Triple<Class<?>, String, Class<?>[]>> dataClassToInterfaceAndMethodNameAndParameters;
private Map<Class<?>, Class<?>> interfaceToImpl;
private Map<Class<?>, Set<Class<? extends Operation<?>>>> interfaceToOperations;
/**
* A set of all values specified in {@link ConsensusMethod#additionalSerializationClasses()}.
*/
private Set<Class<?>> allAdditionalSerializationClasses;
/**
* @return All Copycat {@link Operation} classes that the central copycat state machine needs to support.
*/
public Set<Class<? extends Operation<?>>> getAllOperationClasses() {
return Collections.unmodifiableSet(dataClassToInterfaceAndMethodNameAndParameters.keySet());
}
/**
* Details about the implementation of a specific {@link Operation} - what should a server call when a object of the
* given class arrives?
*
* @return {@link Pair} of {@link Class} object denoting the implementing class and {@link Method} that should be
* called.
* @throws IllegalStateException
* If anything goes wrong.
*/
public Pair<Class<?>, Method> getImplementation(Class<? extends Operation<?>> operationClass)
throws IllegalStateException {
Triple<Class<?>, String, Class<?>[]> t = dataClassToInterfaceAndMethodNameAndParameters.get(operationClass);
Class<?> interfaceClass = t.getLeft();
String methodName = t.getMiddle();
Class<?>[] methodParameters = t.getRight();
Class<?> implClass = interfaceToImpl.get(interfaceClass);
Method m;
try {
m = implClass.getMethod(methodName, methodParameters);
} catch (NoSuchMethodException | SecurityException e) {
throw new IllegalStateException("Could not find implementation method", e);
}
return new Pair<>(implClass, m);
}
/**
* @return All the Operation classes defined by a specific interface class.
*/
public Map<String, Class<? extends Operation<?>>> getOperationClassesAndMethodNamesOfInterface(
Class<?> interfaceClass) {
Map<String, Class<? extends Operation<?>>> res = new HashMap<>();
for (Class<? extends Operation<?>> dataClass : interfaceToOperations.get(interfaceClass))
res.put(dataClassToInterfaceAndMethodNameAndParameters.get(dataClass).getMiddle(), dataClass);
return res;
}
/**
* @return All those classes which have been used in {@link ConsensusMethod#additionalSerializationClasses()} and
* which need to be supported by copycat serialization therefore.
*/
public Set<Class<?>> getAllAdditionalSerializationClasses() {
return allAdditionalSerializationClasses;
}
@PostConstruct
public void initialize() {
ImmutableSet<ClassInfo> classInfos;
try {
classInfos = ClassPath.from(this.getClass().getClassLoader()).getTopLevelClassesRecursive(BASE_PKG);
} catch (IOException e) {
throw new RuntimeException("Could not inspect classpath", e);
}
Set<Class<?>> stateMachineInterfaces = new HashSet<>();
Set<Class<?>> stateMachineImplementations = new HashSet<>();
for (ClassInfo classInfo : classInfos) {
Class<?> clazz = classInfo.load();
boolean isStateMachineInterface =
clazz.isInterface() && clazz.getDeclaredAnnotation(ConsensusStateMachine.class) != null;
boolean isStateMachineImplementation =
!clazz.isInterface() && clazz.getDeclaredAnnotation(ConsensusStateMachineImplementation.class) != null;
if (isStateMachineInterface)
stateMachineInterfaces.add(clazz);
else if (isStateMachineImplementation)
stateMachineImplementations.add(clazz);
}
interfaceToImpl = new HashMap<>();
for (Class<?> implClass : stateMachineImplementations)
interfaceToImpl.put(findStateMachineInterface(implClass, stateMachineInterfaces), implClass);
if (interfaceToImpl.keySet().size() != stateMachineInterfaces.size())
throw new RuntimeException("There are StateMachine interfaces that do not have any implementation: "
+ Sets.difference(stateMachineInterfaces, interfaceToImpl.keySet()));
dataClassToInterfaceAndMethodNameAndParameters = new HashMap<>();
interfaceToOperations = new HashMap<>();
allAdditionalSerializationClasses = new HashSet<>();
Class<?>[] expectedParamTypes = new Class<?>[] { Commit.class };
for (Class<?> interfaceClass : interfaceToImpl.keySet()) {
interfaceToOperations.put(interfaceClass, new HashSet<>());
for (Method m : interfaceClass.getMethods()) {
if (Modifier.isStatic(m.getModifiers()))
continue;
ConsensusMethod consensusMethod = m.getAnnotation(ConsensusMethod.class);
if (consensusMethod != null) {
if (!Arrays.equals(expectedParamTypes, m.getParameterTypes()))
throw new RuntimeException("Method '" + m.toString() + "' has wrong parameter types to be a "
+ ConsensusMethod.class.getSimpleName());
Class<? extends Operation<?>> dataClass = consensusMethod.dataClass();
dataClassToInterfaceAndMethodNameAndParameters.put(dataClass,
new Triple<Class<?>, String, Class<?>[]>(interfaceClass, m.getName(), m.getParameterTypes()));
interfaceToOperations.get(interfaceClass).add(dataClass);
allAdditionalSerializationClasses.addAll(Arrays.asList(consensusMethod.additionalSerializationClasses()));
}
}
}
allAdditionalSerializationClasses.remove(Object.class); // remove default value of annotation
logger.debug("Loaded {} consensus operations of {} interfaces",
dataClassToInterfaceAndMethodNameAndParameters.keySet().size(), interfaceToImpl.keySet().size());
}
private Class<?> findStateMachineInterface(Class<?> stateMachineImplementation,
Set<Class<?>> stateMachineInterfaces) {
Set<Class<?>> interfaces = new HashSet<>();
Class<?> curClass = stateMachineImplementation;
while (curClass != Object.class) {
interfaces.addAll(Arrays.asList(curClass.getInterfaces()));
curClass = curClass.getSuperclass();
}
Set<Class<?>> curStateMachineInterfaces = Sets.intersection(interfaces, stateMachineInterfaces);
if (curStateMachineInterfaces.isEmpty())
throw new RuntimeException(
"Class " + stateMachineImplementation.getName() + " seems to not implement any StateMachineInterfaces.");
if (curStateMachineInterfaces.size() > 1)
throw new RuntimeException("Class " + stateMachineImplementation.getName()
+ " seems to implement multiple StateMachineInterfaces: " + curStateMachineInterfaces.toString());
return curStateMachineInterfaces.iterator().next();
}
}