/**
* Copyright © 2016-2017 The Thingsboard 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.thingsboard.server.service.cluster.routing;
import com.google.common.hash.HashCode;
import com.google.common.hash.HashFunction;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.util.Assert;
import org.thingsboard.server.common.data.id.UUIDBased;
import org.thingsboard.server.common.msg.cluster.ServerAddress;
import org.thingsboard.server.service.cluster.discovery.DiscoveryService;
import org.thingsboard.server.service.cluster.discovery.DiscoveryServiceListener;
import org.thingsboard.server.service.cluster.discovery.ServerInstance;
import org.thingsboard.server.utils.MiscUtils;
import javax.annotation.PostConstruct;
import java.util.Optional;
import java.util.concurrent.ConcurrentNavigableMap;
import java.util.concurrent.ConcurrentSkipListMap;
/**
* Cluster service implementation based on consistent hash ring
*/
@Service
@Slf4j
public class ConsistentClusterRoutingService implements ClusterRoutingService, DiscoveryServiceListener {
@Autowired
private DiscoveryService discoveryService;
@Value("${cluster.hash_function_name}")
private String hashFunctionName;
@Value("${cluster.vitrual_nodes_size}")
private Integer virtualNodesSize;
private ServerInstance currentServer;
private HashFunction hashFunction;
private final ConcurrentNavigableMap<Long, ServerInstance> circle =
new ConcurrentSkipListMap<>();
@PostConstruct
public void init() {
log.info("Initializing Cluster routing service!");
hashFunction = MiscUtils.forName(hashFunctionName);
discoveryService.addListener(this);
this.currentServer = discoveryService.getCurrentServer();
addNode(discoveryService.getCurrentServer());
for (ServerInstance instance : discoveryService.getOtherServers()) {
addNode(instance);
}
logCircle();
log.info("Cluster routing service initialized!");
}
@Override
public ServerAddress getCurrentServer() {
return discoveryService.getCurrentServer().getServerAddress();
}
@Override
public Optional<ServerAddress> resolve(UUIDBased entityId) {
Assert.notNull(entityId);
if (circle.isEmpty()) {
return Optional.empty();
}
Long hash = hashFunction.newHasher().putLong(entityId.getId().getMostSignificantBits())
.putLong(entityId.getId().getLeastSignificantBits()).hash().asLong();
if (!circle.containsKey(hash)) {
ConcurrentNavigableMap<Long, ServerInstance> tailMap =
circle.tailMap(hash);
hash = tailMap.isEmpty() ?
circle.firstKey() : tailMap.firstKey();
}
ServerInstance result = circle.get(hash);
if (!currentServer.equals(result)) {
return Optional.of(result.getServerAddress());
} else {
return Optional.empty();
}
}
@Override
public void onServerAdded(ServerInstance server) {
log.debug("On server added event: {}", server);
addNode(server);
logCircle();
}
@Override
public void onServerUpdated(ServerInstance server) {
log.debug("Ignoring server onUpdate event: {}", server);
}
@Override
public void onServerRemoved(ServerInstance server) {
log.debug("On server removed event: {}", server);
removeNode(server);
logCircle();
}
private void addNode(ServerInstance instance) {
for (int i = 0; i < virtualNodesSize; i++) {
circle.put(hash(instance, i).asLong(), instance);
}
}
private void removeNode(ServerInstance instance) {
for (int i = 0; i < virtualNodesSize; i++) {
circle.remove(hash(instance, i).asLong());
}
}
private HashCode hash(ServerInstance instance, int i) {
return hashFunction.newHasher().putString(instance.getHost(), MiscUtils.UTF8).putInt(instance.getPort()).putInt(i).hash();
}
private void logCircle() {
log.trace("Consistent Hash Circle Start");
circle.entrySet().forEach((e) -> log.debug("{} -> {}", e.getKey(), e.getValue().getServerAddress()));
log.trace("Consistent Hash Circle End");
}
}