/*
* Copyright 2014-2016 CyberVision, Inc.
*
* 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.kaaproject.kaa.server.operations.service.akka.actors.core.user;
import akka.actor.ActorContext;
import akka.actor.ActorRef;
import akka.actor.LocalActorRef;
import akka.actor.Terminated;
import org.kaaproject.kaa.common.hash.EndpointObjectHash;
import org.kaaproject.kaa.server.operations.service.akka.AkkaContext;
import org.kaaproject.kaa.server.operations.service.akka.messages.core.route.RouteOperation;
import org.kaaproject.kaa.server.operations.service.akka.messages.core.session.EndpointEventTimeoutMessage;
import org.kaaproject.kaa.server.operations.service.akka.messages.core.user.EndpointEventDeliveryMessage;
import org.kaaproject.kaa.server.operations.service.akka.messages.core.user.EndpointEventDeliveryMessage.EventDeliveryStatus;
import org.kaaproject.kaa.server.operations.service.akka.messages.core.user.EndpointEventReceiveMessage;
import org.kaaproject.kaa.server.operations.service.akka.messages.core.user.EndpointEventSendMessage;
import org.kaaproject.kaa.server.operations.service.akka.messages.core.user.EndpointRouteUpdateMessage;
import org.kaaproject.kaa.server.operations.service.akka.messages.core.user.EndpointUserConnectMessage;
import org.kaaproject.kaa.server.operations.service.akka.messages.core.user.EndpointUserDisconnectMessage;
import org.kaaproject.kaa.server.operations.service.akka.messages.core.user.RemoteEndpointEventMessage;
import org.kaaproject.kaa.server.operations.service.akka.messages.core.user.RouteInfoMessage;
import org.kaaproject.kaa.server.operations.service.akka.messages.core.user.UserRouteInfoMessage;
import org.kaaproject.kaa.server.operations.service.cache.CacheService;
import org.kaaproject.kaa.server.operations.service.cache.EventClassFqnKey;
import org.kaaproject.kaa.server.operations.service.event.EndpointEcfVersionMap;
import org.kaaproject.kaa.server.operations.service.event.EndpointEvent;
import org.kaaproject.kaa.server.operations.service.event.EventClassFamilyVersion;
import org.kaaproject.kaa.server.operations.service.event.EventClassFqnVersion;
import org.kaaproject.kaa.server.operations.service.event.EventDeliveryTable;
import org.kaaproject.kaa.server.operations.service.event.EventService;
import org.kaaproject.kaa.server.operations.service.event.EventStorage;
import org.kaaproject.kaa.server.operations.service.event.GlobalRouteInfo;
import org.kaaproject.kaa.server.operations.service.event.RemoteEndpointEvent;
import org.kaaproject.kaa.server.operations.service.event.RouteInfo;
import org.kaaproject.kaa.server.operations.service.event.RouteTable;
import org.kaaproject.kaa.server.operations.service.event.RouteTableAddress;
import org.kaaproject.kaa.server.operations.service.event.RouteTableKey;
import org.kaaproject.kaa.server.operations.service.event.UserRouteInfo;
import org.kaaproject.kaa.server.sync.Event;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import scala.concurrent.duration.Duration;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.concurrent.TimeUnit;
public class LocalUserActorMessageProcessor {
private static final Logger LOG = LoggerFactory.getLogger(LocalUserActorMessageProcessor.class);
private final CacheService cacheService;
private final EventService eventService;
private final String userId;
private final String tenantId;
private final RouteTable routeTable;
private final EndpointEcfVersionMap versionMap;
private final EventStorage eventStorage;
private final EventDeliveryTable eventDeliveryTable;
private final Map<String, EndpointObjectHash> endpoints;
private final long eventTimeout;
private final Map<RouteTableAddress, GlobalRouteInfo> localRoutes;
private boolean firstConnectRequestToActor = true;
private String mainUserNode;
LocalUserActorMessageProcessor(AkkaContext context, String userId, String tenantId) {
super();
this.cacheService = context.getCacheService();
this.eventService = context.getEventService();
this.eventTimeout = context.getEventTimeout();
this.userId = userId;
this.tenantId = tenantId;
this.endpoints = new HashMap<>();
this.routeTable = new RouteTable();
this.versionMap = new EndpointEcfVersionMap();
this.eventStorage = new EventStorage();
this.eventDeliveryTable = new EventDeliveryTable();
this.localRoutes = new HashMap<RouteTableAddress, GlobalRouteInfo>();
this.mainUserNode = eventService.getUserNode(userId);
}
void processEndpointConnectMessage(ActorContext context, EndpointUserConnectMessage message) {
RouteTableAddress address = new RouteTableAddress(message.getKey(), message.getAppToken());
// register endpoint for send/receive events
registerEndpointForEvents(context, message, address);
// report existence of this endpoint to global user actor
addGlobalRoute(context, message, address);
endpoints.put(getActorPathName(message.getOriginator()), address.getEndpointKey());
}
void processClusterUpdate(ActorContext context) {
String newNode = eventService.getUserNode(userId);
if (!mainUserNode.equals(newNode)) {
LOG.trace("User node changed from {} to {}", mainUserNode, newNode);
mainUserNode = newNode;
for (GlobalRouteInfo route : localRoutes.values()) {
sendGlobalRouteUpdate(context, route);
}
}
}
void processEndpointDisconnectMessage(ActorContext context,
EndpointUserDisconnectMessage message) {
List<String> actorsToRemove = new LinkedList<>();
for (Entry<String, EndpointObjectHash> entry : endpoints.entrySet()) {
if (entry.getValue().equals(message.getKey())) {
actorsToRemove.add(entry.getKey());
}
}
for (String actor : actorsToRemove) {
LOG.debug("[{}] removed endpoint actor [{}]", userId, actor);
endpoints.remove(actor);
}
removeEndpoint(context, message.getKey());
}
void processEndpointEventSendMessage(ActorContext context, EndpointEventSendMessage message) {
EndpointObjectHash sender = message.getKey();
List<Event> events = message.getEvents();
for (Event event : events) {
processEvent(context, new EndpointEvent(sender, event));
}
}
void processRemoteEndpointEventMessage(ActorContext context,
RemoteEndpointEventMessage message) {
LOG.debug("[{}] Processing remote event message: {}", userId, message);
EndpointEvent localEvent = message.getEvent().toLocalEvent();
processEvent(context, localEvent);
}
void processEndpointEventTimeoutMessage(ActorContext context,
EndpointEventTimeoutMessage message) {
LOG.debug("[{}] processing event timeout message for [{}]",
userId, message.getEvent().getId());
if (eventStorage.clear(message.getEvent())) {
LOG.debug("[{}] removed event [{}] from storage", userId, message.getEvent().getId());
}
if (eventDeliveryTable.clear(message.getEvent())) {
LOG.debug("[{}] removed event [{}] from delivery table",
userId, message.getEvent().getId());
}
}
void processEndpointEventDeliveryMessage(ActorContext context,
EndpointEventDeliveryMessage message) {
LOG.debug("[{}] processing event delivery message for [{}] with status {}",
userId, message.getMessage().getAddress(),
message.getStatus());
boolean success = message.getStatus() == EventDeliveryStatus.SUCCESS;
RouteTableAddress address = message.getMessage().getAddress();
for (EndpointEvent event : message.getMessage().getEndpointEvents()) {
if (success) {
LOG.debug("[{}] registering successful delivery of event [{}] to address {}",
userId, event.getId(), address);
eventDeliveryTable.registerDeliverySuccess(event, address);
} else {
LOG.debug("[{}] registering failure to delivery of event [{}] to address {}",
userId, event.getId(), address);
eventDeliveryTable.registerDeliveryFailure(event, address);
}
}
}
void processRouteInfoMessage(ActorContext context, RouteInfoMessage message) {
RouteInfo routeInfo = message.getRouteInfo();
if (RouteOperation.DELETE.equals(routeInfo.getRouteOperation())) {
LOG.debug("[{}] Removing all routes from route table by address {}",
userId, routeInfo.getAddress());
routeTable.removeByAddress(routeInfo.getAddress());
} else {
for (EventClassFamilyVersion ecfVersion : routeInfo.getEcfVersions()) {
RouteTableKey key = new RouteTableKey(
routeInfo.getAddress().getApplicationToken(), ecfVersion);
LOG.debug("[{}] Updating route table with key {} and address {}",
userId, key, routeInfo.getAddress());
updateRouteTable(context, key, routeInfo.getAddress());
}
}
reportAllLocalRoutes(routeInfo.getAddress().getServerId());
}
void processUserRouteInfoMessage(ActorContext context, UserRouteInfoMessage message) {
UserRouteInfo userRouteInfo = message.getRouteInfo();
LOG.debug("[{}] Cleanup all route table data related to serverId: {}",
userId, userRouteInfo.getServerId());
routeTable.clearRemoteServerData(userRouteInfo.getServerId());
if (!RouteOperation.DELETE.equals(userRouteInfo.getRouteOperation())) {
reportAllLocalRoutes(userRouteInfo.getServerId());
}
}
void processTerminationMessage(ActorContext context, Terminated message) {
ActorRef terminated = message.actor();
if (terminated instanceof LocalActorRef) {
LocalActorRef localActor = (LocalActorRef) terminated;
String name = getActorPathName(localActor);
EndpointObjectHash endpoint = endpoints.remove(name);
if (endpoint != null) {
boolean stilPresent = false;
for (EndpointObjectHash existingEndpoint : endpoints.values()) {
if (existingEndpoint.equals(endpoint)) {
stilPresent = true;
break;
}
}
if (stilPresent) {
LOG.debug("[{}] received termination message for endpoint actor [{}], "
+ "but other actor is still registered for this endpoint.",
userId, localActor);
} else {
removeEndpoint(context, endpoint);
LOG.debug("[{}] removed endpoint [{}]", userId, localActor);
}
}
} else {
LOG.warn("remove commands for remote actors are not supported yet!");
}
}
private void registerEndpointForEvents(ActorContext context,
EndpointUserConnectMessage message,
RouteTableAddress address) {
List<EventClassFamilyVersion> ecfVersions = message.getEcfVersions();
if (!ecfVersions.isEmpty()) {
for (EventClassFamilyVersion ecfVersion : ecfVersions) {
RouteTableKey key = new RouteTableKey(address.getApplicationToken(), ecfVersion);
updateRouteTable(context, key, address);
}
if (firstConnectRequestToActor) {
firstConnectRequestToActor = false;
// report existence of this actor to other operation servers
eventService.sendUserRouteInfo(new UserRouteInfo(tenantId, userId));
}
for (String serverId : routeTable.getRemoteServers()) {
if (routeTable.isDeliveryRequired(serverId, address)) {
LOG.debug("[{}] Sending route info about address {} to server {}",
userId, address, serverId);
eventService.sendRouteInfo(new RouteInfo(
tenantId, userId, address, ecfVersions), serverId);
}
}
versionMap.put(address.getEndpointKey(), message.getEcfVersions());
}
}
protected String getActorPathName(ActorRef actorRef) {
return actorRef.path().name();
}
private void addGlobalRoute(ActorContext context,
EndpointUserConnectMessage message,
RouteTableAddress address) {
GlobalRouteInfo route = GlobalRouteInfo.add(
tenantId, userId, address, message.getCfVersion(), message.getUcfHash());
localRoutes.put(address, route);
sendGlobalRouteUpdate(context, route);
}
private void sendGlobalRouteUpdate(ActorContext context, GlobalRouteInfo route) {
if (eventService.isMainUserNode(userId)) {
context.parent().tell(new EndpointRouteUpdateMessage(route), context.self());
} else {
LOG.debug("[{}] Sending connect message to global actor", userId);
eventService.sendEndpointRouteInfo(route);
}
}
private void updateRouteTable(ActorContext context,
RouteTableKey key,
RouteTableAddress address) {
LOG.debug("[{}] adding to route table key: {} address: {}",
userId, key, address);
routeTable.add(key, address);
sendPendingEvents(context, key, address);
}
private void sendPendingEvents(ActorContext context,
RouteTableKey key,
RouteTableAddress address) {
List<EndpointEvent> events = eventStorage.getEvents(key, address);
if (events.size() > 0) {
sendEventsToRecepient(context, address, events);
}
}
private void sendEventToRecepients(ActorContext context,
EndpointEvent event,
Collection<RouteTableAddress> recipients) {
for (RouteTableAddress recipient : recipients) {
sendEventsToRecepient(context, recipient, Collections.singletonList(event));
}
}
private void sendEventsToRecepient(ActorContext context,
RouteTableAddress recipient,
List<EndpointEvent> events) {
List<EndpointEvent> eventsToSend = new ArrayList<>(events.size());
for (EndpointEvent event : events) {
if (!eventDeliveryTable.isDeliveryStarted(event, recipient)) {
eventsToSend.add(event);
}
}
if (eventsToSend.size() > 0) {
if (recipient.isLocal()) {
if (LOG.isTraceEnabled()) {
for (EndpointEvent event : eventsToSend) {
LOG.trace("[{}] forwarding event {} to local recepient {}", userId, event, recipient);
}
}
EndpointEventReceiveMessage message = new EndpointEventReceiveMessage(
userId, eventsToSend, recipient, context.self());
sendEventToLocal(context, message);
} else {
for (EndpointEvent event : eventsToSend) {
LOG.trace("[{}] forwarding event {} to remote recepient {}",
userId, event, recipient);
RemoteEndpointEvent remoteEvent = new RemoteEndpointEvent(
tenantId, userId, event, recipient);
eventService.sendEvent(remoteEvent);
}
}
for (EndpointEvent event : eventsToSend) {
LOG.debug("[{}] registering delivery attempt of event {} to recepient {}",
userId, event, recipient);
eventDeliveryTable.registerDeliveryAttempt(event, recipient);
}
}
}
protected void sendEventToLocal(ActorContext context, EndpointEventReceiveMessage message) {
context.parent().tell(message, context.self());
}
private void processEvent(ActorContext context, EndpointEvent event) {
String fqn = event.getEventClassFqn();
LOG.debug("[{}] Processing event {} from {}", userId, event.getId(), event.getSender());
Integer version;
if (event.getVersion() == 0) {
version = lookupVersion(event, fqn);
} else {
version = event.getVersion();
}
if (version != null && version > 0) {
event.setVersion(version);
Set<RouteTableKey> recipientKeys = cacheService.getRouteKeys(
new EventClassFqnVersion(tenantId, fqn, version));
if (!recipientKeys.isEmpty()) {
LOG.debug("[{}] Put event {} with {} recipient keys to storage",
userId, event.getId(), recipientKeys.size());
eventStorage.put(event, recipientKeys);
Set<RouteTableAddress> recipients = routeTable.getRoutes(recipientKeys, event.getTarget());
recipients = filterOutRecipientsByKeyHash(event, recipients);
if (!recipients.isEmpty()) {
sendEventToRecepients(context, event, recipients);
} else {
LOG.debug("[{}] there is no recipients for event with class fqn {} and version {} yet",
userId, fqn, version);
}
scheduleTimeoutMessage(context, event);
} else {
LOG.debug("[{}] event {} is ignored due to it does not have any potential recepients",
userId, event.getId());
}
}
}
protected Integer lookupVersion(EndpointEvent event, String fqn) {
Integer version;
LOG.debug("[{}] Lookup event class family id using event class fqn {}", userId, fqn);
String ecfId = cacheService.getEventClassFamilyIdByEventClassFqn(
new EventClassFqnKey(tenantId, fqn));
LOG.debug("[{}] Lookup event {} version from user's version map using ecfId {} ",
userId, fqn, ecfId);
version = versionMap.get(event.getSender(), ecfId);
if (version == null) {
LOG.warn("[{}] Lookup event {} version from user's version map using ecfId {} FAILED!",
userId, fqn, ecfId);
}
return version;
}
protected Set<RouteTableAddress> filterOutRecipientsByKeyHash(
EndpointEvent event, Set<RouteTableAddress> recipients) {
Iterator<RouteTableAddress> recipientsIterator = recipients.iterator();
while (recipientsIterator.hasNext()) {
RouteTableAddress recipient = recipientsIterator.next();
if (recipient.getEndpointKey().equals(event.getSender())) {
recipientsIterator.remove();
}
}
return recipients;
}
protected void removeEndpoint(ActorContext context, EndpointObjectHash endpoint) {
LOG.debug("[{}] removing endpoint [{}] from route tables", userId, endpoint);
RouteTableAddress address = routeTable.removeLocal(endpoint);
versionMap.remove(endpoint);
for (String serverId : routeTable.getRemoteServers()) {
LOG.debug("[{}] removing endpoint [{}] from remote route table on server {}",
userId, endpoint, serverId);
eventService.sendRouteInfo(
RouteInfo.deleteRouteFromAddress(tenantId, userId, address), serverId);
}
// cleanup and notify global route actor
GlobalRouteInfo route = GlobalRouteInfo.delete(tenantId, userId, address);
if (eventService.isMainUserNode(userId)) {
context.parent().tell(new EndpointRouteUpdateMessage(route), context.self());
} else {
LOG.debug("[{}] Sending disconnect message to global actor", userId);
eventService.sendEndpointRouteInfo(route);
}
}
private void reportAllLocalRoutes(String serverId) {
LOG.debug("[{}] Reporting all local routes to serverId: {}", userId, serverId);
Set<RouteTableAddress> localAddresses = routeTable.getAllLocalRoutes();
List<RouteInfo> localRoutes = new ArrayList<>();
for (RouteTableAddress localAddress : localAddresses) {
if (routeTable.isDeliveryRequired(serverId, localAddress)) {
Set<RouteTableKey> routeKeys = routeTable.getLocalRouteTableKeys(localAddress);
Set<EventClassFamilyVersion> ecfVersions = new HashSet<>();
for (RouteTableKey routeKey : routeKeys) {
ecfVersions.add(routeKey.getEcfVersion());
}
localRoutes.add(
new RouteInfo(tenantId, userId, localAddress, new ArrayList<>(ecfVersions)));
} else {
LOG.debug("[{}] Address {} is already delivered to serverId {} and will not be sent again",
userId, localAddress, serverId);
}
}
LOG.debug("[{}] Reporting {}/{} local addresses/routes count",
userId, localAddresses.size(), localRoutes.size());
if (!localRoutes.isEmpty()) {
eventService.sendRouteInfo(localRoutes, serverId);
routeTable.registerRouteInfoReport(localAddresses, serverId);
}
}
void scheduleTimeoutMessage(ActorContext context, EndpointEvent event) {
context.system()
.scheduler()
.scheduleOnce(Duration.create(getTtl(event), TimeUnit.MILLISECONDS), context.self(),
new EndpointEventTimeoutMessage(event), context.dispatcher(), context.self());
}
private long getTtl(EndpointEvent event) {
return Math.max(eventTimeout - (System.currentTimeMillis() - event.getCreateTime()), 0L);
}
}