/*
* Copyright 2014 Avanza Bank AB
*
* 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 com.avanza.astrix.remoting.client;
import java.lang.annotation.Annotation;
import java.lang.reflect.Array;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.*;
import java.util.function.Consumer;
import com.avanza.astrix.core.AstrixPartitionedRouting;
import com.avanza.astrix.core.AstrixRemoteResult;
import com.avanza.astrix.core.RemoteResultReducer;
import com.avanza.astrix.core.remoting.RoutingKey;
import com.avanza.astrix.core.util.ReflectionUtil;
import rx.Observable;
import rx.functions.Func1;
/**
*
* @author Elias Lindholm (elilin)
*
*/
public class PartitionedRemoteServiceMethod implements RemoteServiceMethod {
private final int partitionedArgumentIndex;
private final String methodSignature;
private final RemotingEngine remotingEngine;
private final Type targetReturnType;
private final Class<? extends RemoteResultReducer<?>> reducerType;
private final ContainerType partitionedArgumentContainerType;
private final PartitionedRouter router;
private final Method proxiedMethod;
public PartitionedRemoteServiceMethod(int partitionedArgumentIndex,
Method proxiedMethod,
String methodSignature,
RemotingEngine remotingEngine,
Type targetReturnType,
Method targetServiceMethod) {
this.partitionedArgumentIndex = partitionedArgumentIndex;
this.proxiedMethod = proxiedMethod;
this.methodSignature = methodSignature;
this.remotingEngine = remotingEngine;
this.targetReturnType = targetReturnType;
AstrixPartitionedRouting partitionedRouting = getPartitionedRoutingAnnotation(proxiedMethod, partitionedArgumentIndex);
this.reducerType = getReducer(partitionedRouting, targetServiceMethod);
this.partitionedArgumentContainerType = getPartitionedArgumentContainerType(proxiedMethod, partitionedRouting);
this.router = createRouter(partitionedRouting);
}
private PartitionedRouter createRouter(AstrixPartitionedRouting partitionedRouting) {
Class<?> elementType = this.partitionedArgumentContainerType.getElementType();
if (!partitionedRouting.routingMethod().isEmpty()) {
Method routingMethod;
try {
routingMethod = elementType.getMethod(partitionedRouting.routingMethod());
return PartitionedRouter.routingMethod(routingMethod);
} catch (NoSuchMethodException | SecurityException e) {
throw new IllegalArgumentException("Failed to find routing method for partitioned routing:\n"
+ "service: " + ReflectionUtil.fullMethodName(proxiedMethod) + "\n"
+ "@AstrixPartitionedRouting.routingMethod: " + partitionedRouting.routingMethod(), e);
}
}
return PartitionedRouter.identity();
}
private ContainerType getPartitionedArgumentContainerType(Method proxiedMethod, AstrixPartitionedRouting partitionBy) {
Class<?> partitionedArgumentType = proxiedMethod.getParameterTypes()[partitionedArgumentIndex];
if (partitionedArgumentType.isArray()) {
return new ArrayContainerType(partitionedArgumentType.getComponentType());
}
Class<? extends Collection<?>> collectionFactory = (Class<? extends Collection<?>>) partitionBy.collectionFactory();
if (!proxiedMethod.getParameterTypes()[partitionedArgumentIndex].isAssignableFrom(collectionFactory)) {
throw new IllegalArgumentException(String.format("Collection class supplied by @AstrixPartitionedRouting is not "
+ "compatible with argument type, argumentType=%s classType=%s",
proxiedMethod.getParameterTypes()[partitionedArgumentIndex].getName(),
collectionFactory));
}
Type rawType = proxiedMethod.getGenericParameterTypes()[partitionedArgumentIndex];
if (!(rawType instanceof ParameterizedType)) {
throw new IllegalArgumentException("Illegal service method: " + ReflectionUtil.fullMethodName(proxiedMethod) + ".\nWhen defining a routingMethod for @AstrixPartitionedRouting the target Collection type must not be a raw type. \nwas: " + rawType);
}
ParameterizedType partitionedArgumentTypeParameters = (ParameterizedType) rawType;
return new CollectionContainerType(collectionFactory, (Class<?>)partitionedArgumentTypeParameters.getActualTypeArguments()[0]);
}
private Class<? extends RemoteResultReducer<?>> getReducer(AstrixPartitionedRouting partitionBy, Method targetServiceMethod) {
Class<? extends RemoteResultReducer<?>> reducerType = (Class<? extends RemoteResultReducer<?>>) partitionBy.reducer();
RemotingProxyUtil.validateRemoteResultReducer(targetServiceMethod, reducerType);
return reducerType;
}
private static AstrixPartitionedRouting getPartitionedRoutingAnnotation(Method proxiedMethod, int partitionedByArgumentIndex) {
for (Annotation a : proxiedMethod.getParameterAnnotations()[partitionedByArgumentIndex]) {
if (a instanceof AstrixPartitionedRouting) {
return AstrixPartitionedRouting.class.cast(a);
}
}
throw new IllegalStateException("Programming error, proxied method does not hold AstrixPartitionedBy annotation: " + proxiedMethod);
}
@Override
public String getSignature() {
return methodSignature;
}
@Override
public Observable<?> invoke(AstrixServiceInvocationRequest invocationRequest, Object[] args) throws Exception {
/*
* 1. Partition Requests
* 2. Marshall arguments
* 3. Execute requests
*/
ServiceInvocationPartitioner serviceInvocationPartitioner = new ServiceInvocationPartitioner();
List<RoutedServiceInvocationRequest> partitionInvocationRequest = serviceInvocationPartitioner.partitionInvocationRequest(invocationRequest, args);
Observable<List<AstrixServiceInvocationResponse>> serviceInvocationResponses = remotingEngine.submitRoutedRequests(partitionInvocationRequest);
return reduce(serviceInvocationResponses);
}
private <T> Observable<T> reduce(Observable<List<AstrixServiceInvocationResponse>> responses) {
if (targetReturnType.equals(Void.TYPE) || targetReturnType.equals(Void.class)) {
return responses.map(responseList -> {
readResults(responseList);
return null;
});
}
final RemoteResultReducer<T> reducer = newRemoteResultReducer();
return responses.map(responseList -> {
List<AstrixRemoteResult<T>> unmarshalledResponses = new ArrayList<>(responseList.size());
for (AstrixServiceInvocationResponse response : responseList) {
AstrixRemoteResult<T> result = remotingEngine.toRemoteResult(response, targetReturnType);
unmarshalledResponses.add(result);
}
return reducer.reduce(unmarshalledResponses);
});
}
private void readResults(List<AstrixServiceInvocationResponse> responseList) {
responseList.forEach(res -> remotingEngine.toRemoteResult(res, targetReturnType).getResult());
}
@SuppressWarnings("unchecked")
private <T> RemoteResultReducer<T> newRemoteResultReducer() {
return (RemoteResultReducer<T>) ReflectionUtil.newInstance(this.reducerType);
}
private ContainerBuilder newCollectionInstance() {
return partitionedArgumentContainerType.newInstance();
}
private class RoutedServiceInvocationRequestBuilder {
private final ContainerBuilder routingKeys;
private final RoutingKey targetPartitionRoutingKey;
public RoutedServiceInvocationRequestBuilder(ContainerBuilder keys, int targetPartition) {
this.routingKeys = keys;
this.targetPartitionRoutingKey = RoutingKey.create(targetPartition);
}
public void addKey(Object requestedKey) {
this.routingKeys.add(requestedKey);
}
private RoutedServiceInvocationRequest createInvocationRequest(
AstrixServiceInvocationRequest invocationRequest,
Object[] unpartitionedArguments) {
AstrixServiceInvocationRequest partitionedRequest = new AstrixServiceInvocationRequest();
partitionedRequest.setAllHeaders(invocationRequest.getHeaders());
Object[] requestForPartition = Arrays.copyOf(unpartitionedArguments, unpartitionedArguments.length);
requestForPartition[partitionedArgumentIndex] = this.routingKeys.buildTarget();
partitionedRequest.setArguments(remotingEngine.marshall(requestForPartition));
return new RoutedServiceInvocationRequest(partitionedRequest, targetPartitionRoutingKey);
}
}
private class ServiceInvocationPartitioner {
private final RoutedServiceInvocationRequestBuilder[] requests;
public ServiceInvocationPartitioner() {
this.requests = new RoutedServiceInvocationRequestBuilder[remotingEngine.partitionCount()];
}
public List<RoutedServiceInvocationRequest> partitionInvocationRequest(AstrixServiceInvocationRequest invocationRequest, Object[] args) {
partitionedArgumentContainerType.iterateContainer(getContainerInstance(args), new Consumer<Object>() {
@Override
public void accept(Object element) {
addElement(element);
}
});
List<RoutedServiceInvocationRequest> result = new LinkedList<>();
for (RoutedServiceInvocationRequestBuilder routedInvocationReqeustBuilder : requests) {
if (routedInvocationReqeustBuilder != null) {
result.add(routedInvocationReqeustBuilder.createInvocationRequest(invocationRequest, args));
}
}
return result;
}
private Object getContainerInstance(Object[] args) {
return args[partitionedArgumentIndex];
}
public void addElement(Object element) {
RoutingKey routingKey = router.getRoutingKey(element);
int targetPartition = routingKey.hashCode() % requests.length;
RoutedServiceInvocationRequestBuilder invocationRequestBuilderForPartition = this.requests[targetPartition];
if (invocationRequestBuilderForPartition == null) {
invocationRequestBuilderForPartition = new RoutedServiceInvocationRequestBuilder(newCollectionInstance(), targetPartition);
this.requests[targetPartition] = invocationRequestBuilderForPartition;
}
invocationRequestBuilderForPartition.addKey(element);
}
}
private interface ContainerType {
ContainerBuilder newInstance();
void iterateContainer(Object container, Consumer<Object> consumer);
Class<?> getElementType();
}
private static class CollectionContainerType implements ContainerType {
private final Class<?> elementType;
private final Class<? extends Collection<?>> collectionFactory;
public CollectionContainerType(Class<? extends Collection<?>> collectionFactory, Class<?> elementType) {
this.collectionFactory = Objects.requireNonNull(collectionFactory);
this.elementType = Objects.requireNonNull(elementType);
}
@Override
public ContainerBuilder newInstance() {
return new CollectionContainerBuilder((Collection<? super Object>) ReflectionUtil.newInstance(this.collectionFactory));
}
@Override
public void iterateContainer(Object container, Consumer<Object> consumer) {
for (Object element : (Collection<? extends Object>) container) {
consumer.accept(element);
}
}
@Override
public Class<?> getElementType() {
return elementType;
}
}
private static class ArrayContainerType implements ContainerType {
private final Class<?> elementType;
public ArrayContainerType(Class<?> elementType) {
this.elementType = elementType;
}
@Override
public ContainerBuilder newInstance() {
return new ArrayContainerBuilder(elementType);
}
@Override
public void iterateContainer(Object container, Consumer<Object> consumer) {
for (int i = 0; i < Array.getLength(container); i++) {
consumer.accept(Array.get(container, i));
}
}
@Override
public Class<?> getElementType() {
return this.elementType;
}
}
private static class ArrayContainerBuilder extends ContainerBuilder {
private final Class<?> elementType;
private final List<Object> elements = new ArrayList<>();
public ArrayContainerBuilder(Class<?> elementType) {
this.elementType = elementType;
}
@Override
void add(Object element) {
elements.add(element);
}
@Override
Object buildTarget() {
Object array = Array.newInstance(elementType, elements.size());
int nextIndex = 0;
for (Object element : elements) {
Array.set(array, nextIndex, element);
nextIndex++;
}
return array;
}
}
private static abstract class ContainerBuilder {
abstract void add(Object element);
abstract Object buildTarget();
}
private static class CollectionContainerBuilder extends ContainerBuilder {
private Collection<? super Object> collection;
public CollectionContainerBuilder(Collection<? super Object> collection) {
this.collection = collection;
}
@Override
void add(Object element) {
this.collection.add(element);
}
@Override
Object buildTarget() {
return collection;
}
}
}