/*
* Copyright (c) 2010-2017. Axon Framework
* 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.axonframework.springcloud.commandhandling;
import org.axonframework.commandhandling.CommandMessage;
import org.axonframework.commandhandling.distributed.*;
import org.axonframework.serialization.SerializedObject;
import org.axonframework.serialization.Serializer;
import org.axonframework.serialization.SimpleSerializedObject;
import org.axonframework.serialization.xml.XStreamSerializer;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.cloud.client.discovery.event.HeartbeatEvent;
import org.springframework.context.event.EventListener;
import java.net.URI;
import java.util.*;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Predicate;
import java.util.stream.Collectors;
/**
* A {@link org.axonframework.commandhandling.distributed.CommandRouter} implementation which uses Spring Clouds
* {@link org.springframework.cloud.client.discovery.DiscoveryClient}s to discover and notify other nodes for routing
* Commands.
*/
public class SpringCloudCommandRouter implements CommandRouter {
private static final String LOAD_FACTOR = "loadFactor";
private static final String SERIALIZED_COMMAND_FILTER = "serializedCommandFilter";
private static final String SERIALIZED_COMMAND_FILTER_CLASS_NAME = "serializedCommandFilterClassName";
private static final boolean OVERWRITE_MEMBERS = true;
private static final boolean DO_NOT_OVERWRITE_MEMBERS = false;
private final DiscoveryClient discoveryClient;
private final RoutingStrategy routingStrategy;
private final XStreamSerializer serializer = new XStreamSerializer();
private final AtomicReference<ConsistentHash> atomicConsistentHash = new AtomicReference<>(new ConsistentHash());
/**
* Initialize a {@link org.axonframework.commandhandling.distributed.CommandRouter} with the given
* {@link org.springframework.cloud.client.discovery.DiscoveryClient} to update it's own membership as a
* {@code CommandRouter} and to create it's own awareness of available nodes to send commands to in a
* {@link org.axonframework.commandhandling.distributed.ConsistentHash}. The {@code routingStrategy} is used to
* define the key based on which Command Messages are routed to their respective handler nodes.
* The {@code serializer} is used to serialize this node it's set of Commands it can handle to be added as meta data
* to this {@link org.springframework.cloud.client.ServiceInstance}
*
* @param discoveryClient The {@code DiscoveryClient} used to discovery and notify other nodes
* @param routingStrategy The strategy for routing Commands to a Node
* @param serializer The serializer used to serialize this node it's set of Commands it can handle
* @deprecated {@code serializer} is no longer customizable
*/
@Deprecated
public SpringCloudCommandRouter(DiscoveryClient discoveryClient, RoutingStrategy routingStrategy,
Serializer serializer) {
this(discoveryClient, routingStrategy);
}
/**
* Initialize a {@link org.axonframework.commandhandling.distributed.CommandRouter} with the given
* {@link org.springframework.cloud.client.discovery.DiscoveryClient} to update it's own membership as a
* {@code CommandRouter} and to create it's own awareness of available nodes to send commands to in a
* {@link org.axonframework.commandhandling.distributed.ConsistentHash}. The {@code routingStrategy} is used to
* define the key based on which Command Messages are routed to their respective handler nodes.
*
* @param discoveryClient The {@code DiscoveryClient} used to discovery and notify other nodes
* @param routingStrategy The strategy for routing Commands to a Node
*/
public SpringCloudCommandRouter(DiscoveryClient discoveryClient, RoutingStrategy routingStrategy) {
this.discoveryClient = discoveryClient;
this.routingStrategy = routingStrategy;
}
@Override
public Optional<Member> findDestination(CommandMessage<?> commandMessage) {
return atomicConsistentHash.get().getMember(routingStrategy.getRoutingKey(commandMessage), commandMessage);
}
@Override
public void updateMembership(int loadFactor, Predicate<? super CommandMessage<?>> commandFilter) {
ServiceInstance localServiceInstance = this.discoveryClient.getLocalServiceInstance();
Map<String, String> localServiceInstanceMetadata = localServiceInstance.getMetadata();
localServiceInstanceMetadata.put(LOAD_FACTOR, Integer.toString(loadFactor));
SerializedObject<String> serializedCommandFilter = serializer.serialize(commandFilter, String.class);
localServiceInstanceMetadata.put(SERIALIZED_COMMAND_FILTER, serializedCommandFilter.getData());
localServiceInstanceMetadata.put(SERIALIZED_COMMAND_FILTER_CLASS_NAME, serializedCommandFilter.getType().getName());
updateMemberships(Collections.singleton(localServiceInstance), DO_NOT_OVERWRITE_MEMBERS);
}
@EventListener
@SuppressWarnings("UnusedParameters")
public void updateMemberships(HeartbeatEvent event) {
Set<ServiceInstance> allServiceInstances = discoveryClient.getServices().stream()
.map(discoveryClient::getInstances)
.flatMap(Collection::stream)
.filter(serviceInstance -> serviceInstance.getMetadata().containsKey(LOAD_FACTOR) &&
serviceInstance.getMetadata().containsKey(SERIALIZED_COMMAND_FILTER) &&
serviceInstance.getMetadata().containsKey(SERIALIZED_COMMAND_FILTER_CLASS_NAME))
.collect(Collectors.toSet());
updateMemberships(allServiceInstances, OVERWRITE_MEMBERS);
}
/**
* Update the router memberships.
*
* @param serviceInstances Services instances to add
* @param overwrite True to evict members absent from serviceInstances
*/
private void updateMemberships(Set<ServiceInstance> serviceInstances, boolean overwrite) {
AtomicReference<ConsistentHash> updatedConsistentHash;
if (overwrite) {
updatedConsistentHash = new AtomicReference<>(new ConsistentHash());
} else {
updatedConsistentHash = atomicConsistentHash;
}
ServiceInstance localServiceInstance = discoveryClient.getLocalServiceInstance();
URI localServiceUri = localServiceInstance.getUri();
serviceInstances.forEach(serviceInstance -> {
URI serviceUri = serviceInstance.getUri();
String serviceId = serviceInstance.getServiceId();
boolean local = localServiceUri.equals(serviceUri);
SimpleMember<URI> simpleMember = new SimpleMember<>(
serviceId.toUpperCase() + "[" + serviceUri + "]",
serviceUri,
local,
member -> atomicConsistentHash.updateAndGet(consistentHash -> consistentHash.without(member))
);
Map<String, String> serviceInstanceMetadata = serviceInstance.getMetadata();
int loadFactor = Integer.parseInt(serviceInstanceMetadata.get(LOAD_FACTOR));
SimpleSerializedObject<String> serializedObject = new SimpleSerializedObject<>(
serviceInstanceMetadata.get(SERIALIZED_COMMAND_FILTER), String.class,
serviceInstanceMetadata.get(SERIALIZED_COMMAND_FILTER_CLASS_NAME), null);
Predicate<? super CommandMessage<?>> commandNameFilter = serializer.deserialize(serializedObject);
updatedConsistentHash.updateAndGet(
consistentHash -> consistentHash.with(simpleMember, loadFactor, commandNameFilter)
);
});
atomicConsistentHash.set(updatedConsistentHash.get());
}
}