/* * 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; import akka.actor.ActorRef; import akka.actor.LocalActorRef; import akka.actor.Terminated; import akka.actor.UntypedActor; import akka.japi.Creator; import org.kaaproject.kaa.common.dto.NotificationDto; import org.kaaproject.kaa.common.dto.NotificationTypeDto; import org.kaaproject.kaa.server.operations.service.akka.messages.core.endpoint.EndpointAwareMessage; import org.kaaproject.kaa.server.operations.service.akka.messages.core.notification.ThriftNotificationMessage; import org.kaaproject.kaa.server.operations.service.akka.messages.core.topic.NotificationMessage; import org.kaaproject.kaa.server.operations.service.akka.messages.core.topic.TopicSubscriptionMessage; import org.kaaproject.kaa.server.operations.service.akka.messages.core.topic.TopicUnsubscriptionMessage; import org.kaaproject.kaa.server.operations.service.notification.NotificationDeltaService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.ArrayList; import java.util.Calendar; import java.util.Collections; import java.util.Date; import java.util.GregorianCalendar; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.SortedMap; import java.util.TimeZone; import java.util.TreeMap; /** * The Class TopicActor. */ public class TopicActor extends UntypedActor { /** * The Constant LOG. */ private static final Logger LOG = LoggerFactory.getLogger(TopicActor.class); /** * The notification service. */ private final NotificationDeltaService notificationService; /** * The endpoint sessions. */ private final Map<String, ActorInfo> endpointSessions; /** * The notification cache. */ private final TreeMap<Integer, NotificationDto> notificationCache; // NOSONAR /** * Instantiates a new topic actor. * * @param notificationService the notification service */ public TopicActor(NotificationDeltaService notificationService) { this.notificationService = notificationService; this.endpointSessions = new HashMap<>(); this.notificationCache = new TreeMap<>(); } /** * Filter map. * * @param pendingNotificationMap the pending notification map * @param systemNfSchemaVersion the system nf schema version * @param userNfSchemaVersion the user nf schema version * @param calendar the calendar * @return the list */ public static List<NotificationDto> filterMap( SortedMap<Integer, NotificationDto> pendingNotificationMap, int systemNfSchemaVersion, int userNfSchemaVersion, Calendar calendar) { List<NotificationDto> pendingNotifications = new ArrayList<>(pendingNotificationMap.size()); long now = calendar.getTimeInMillis(); List<NotificationDto> expiredNotifications = null; for (NotificationDto dto : pendingNotificationMap.values()) { LOG.trace("Filtering notification {} using system schema version " + "{} and user schema version {}", dto, systemNfSchemaVersion, userNfSchemaVersion); Date date = dto.getExpiredAt(); if (date != null && date.getTime() > now) { if (isSchemaVersionMatch(dto, systemNfSchemaVersion, userNfSchemaVersion)) { pendingNotifications.add(dto); } } else { if (expiredNotifications == null) { expiredNotifications = new ArrayList<>(); } expiredNotifications.add(dto); LOG.trace("Detected expired notification: {}, nfTime: {}, curTime: {}", dto, date == null ? date : date.getTime(), now); } } if (expiredNotifications != null) { LOG.trace("Removing {} notifications from pendingNotificationMap", expiredNotifications.size()); pendingNotificationMap.values().removeAll(expiredNotifications); } return pendingNotifications; } /** * Checks if is schema version match. * * @param notificationDto the notification dto * @param systemNfVersion the system nf version * @param userNfVersion the user nf version * @return true, if is schema version match */ public static boolean isSchemaVersionMatch(NotificationDto notificationDto, int systemNfVersion, int userNfVersion) { if (notificationDto.getType() == NotificationTypeDto.SYSTEM) { return notificationDto.getNfVersion() == systemNfVersion; } else if (notificationDto.getType() == NotificationTypeDto.USER) { return notificationDto.getNfVersion() == userNfVersion; } else { return false; } } /* * (non-Javadoc) * * @see akka.actor.UntypedActor#onReceive(java.lang.Object) */ @Override public void onReceive(Object message) throws Exception { LOG.debug("Received: {}", message); if (message instanceof EndpointAwareMessage) { if (message instanceof TopicSubscriptionMessage) { processEndpointRegistration((TopicSubscriptionMessage) message); } else if (message instanceof TopicUnsubscriptionMessage) { processEndpointDeregistration((TopicUnsubscriptionMessage) message); } } else if (message instanceof Terminated) { processTermination((Terminated) message); } else if (message instanceof ThriftNotificationMessage) { broadcastToAllEndpoints((ThriftNotificationMessage) message); } } /** * Process endpoint registration. * * @param message the message */ private void processEndpointRegistration(TopicSubscriptionMessage message) { ActorRef endpointActor = message.getOriginator(); Integer seqNum = message.getSeqNumber(); SortedMap<Integer, NotificationDto> pendingNotificationMap = notificationCache.tailMap( seqNum, false); Calendar calendar = new GregorianCalendar(TimeZone.getTimeZone("UTC")); List<NotificationDto> pendingNotifications = filterMap( pendingNotificationMap, message.getSystemNfSchemaVersion(), message.getUserNfSchemaVersion(), calendar); if (!pendingNotifications.isEmpty()) { LOG.debug("Detected new messages during endpoint subscription!"); NotificationMessage notificationMessage = NotificationMessage.fromNotifications( pendingNotifications); endpointActor.tell(notificationMessage, self()); } else { LOG.debug("No new messages detected. Subscribing endpoint actor to topic actor"); String endpointKey = message.getOriginator().path().name(); ActorInfo actorInfo = new ActorInfo( endpointActor, message.getSystemNfSchemaVersion(), message.getUserNfSchemaVersion()); if (endpointSessions.put(endpointKey, actorInfo) != null) { LOG.warn("Detected duplication of registration message: {}", message); } context().watch(endpointActor); } } private void processEndpointDeregistration(TopicUnsubscriptionMessage message) { String endpointKey = message.getOriginator().path().name(); if (endpointSessions.remove(endpointKey) != null) { LOG.debug("Removed subsctioption for endpoint {}", endpointKey); } else { LOG.warn("Failed to remove subscription for endpoint {} from topic", endpointKey); } } /** * Broadcast to all endpoints. * * @param message the message */ private void broadcastToAllEndpoints(ThriftNotificationMessage message) { String notificationId = message.getNotification().getNotificationId(); NotificationDto notificationDto = notificationService.findNotificationById(notificationId); if (notificationDto == null) { LOG.warn("Can't find notification by id {}. Probably it has already expired!"); } else { notificationCache.put(notificationDto.getSecNum(), notificationDto); LOG.debug("[{}] Put notification to topic actor cache {}", notificationDto.getTopicId(), notificationDto); NotificationMessage notificationMessage = NotificationMessage.fromNotifications( Collections.singletonList(notificationDto)); for (ActorInfo endpoint : endpointSessions.values()) { if (isSchemaVersionMatch( notificationDto, endpoint.getSystemNfVersion(), endpoint.getUserNfVersion())) { endpoint.getActorRef().tell(notificationMessage, self()); } } } } /** * Process termination. * * @param message the message */ private void processTermination(Terminated message) { ActorRef terminated = message.actor(); if (terminated instanceof LocalActorRef) { LocalActorRef localActor = (LocalActorRef) terminated; String name = localActor.path().name(); if (endpointSessions.remove(name) != null) { LOG.debug("removed: {}", localActor); } } else { LOG.warn("remove commands for remote actors are not supported yet!"); } } /* * (non-Javadoc) * * @see akka.actor.UntypedActor#preStart() */ @Override public void preStart() { LOG.info("Starting " + this); } /* * (non-Javadoc) * * @see akka.actor.UntypedActor#postStop() */ @Override public void postStop() { LOG.info("Stoped " + this); } /** * The Class ActorCreator. */ public static class ActorCreator implements Creator<TopicActor> { /** * The Constant serialVersionUID. */ private static final long serialVersionUID = 1L; /** * The notification service. */ private final NotificationDeltaService notificationService; /** * Instantiates a new actor creator. * * @param notificationService the notification service */ public ActorCreator(NotificationDeltaService notificationService) { super(); this.notificationService = notificationService; } /* * (non-Javadoc) * * @see akka.japi.Creator#create() */ @Override public TopicActor create() throws Exception { return new TopicActor(notificationService); } } /** * The Class ActorInfo. */ public static final class ActorInfo { /** * The actor ref. */ private final ActorRef actorRef; /** * The system nf version. */ private final int systemNfVersion; /** * The user nf version. */ private final int userNfVersion; /** * Instantiates a new actor info. * * @param actorRef the actor ref * @param systemNfVersion the system nf version * @param userNfVersion the user nf version */ public ActorInfo(ActorRef actorRef, int systemNfVersion, int userNfVersion) { super(); this.actorRef = actorRef; this.systemNfVersion = systemNfVersion; this.userNfVersion = userNfVersion; } /** * Gets the actor ref. * * @return the actor ref */ public ActorRef getActorRef() { return actorRef; } /** * Gets the system nf version. * * @return the system nf version */ public int getSystemNfVersion() { return systemNfVersion; } /** * Gets the user nf version. * * @return the user nf version */ public int getUserNfVersion() { return userNfVersion; } } }