/* * 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.endpoint.local; import akka.actor.ActorContext; import org.kaaproject.kaa.common.TransportType; import org.kaaproject.kaa.common.channels.protocols.kaatcp.messages.PingResponse; import org.kaaproject.kaa.common.dto.EndpointProfileDataDto; import org.kaaproject.kaa.common.dto.EndpointProfileDto; import org.kaaproject.kaa.common.dto.NotificationDto; import org.kaaproject.kaa.common.hash.EndpointObjectHash; import org.kaaproject.kaa.server.common.Base64Util; import org.kaaproject.kaa.server.common.log.shared.appender.LogEvent; import org.kaaproject.kaa.server.common.log.shared.appender.data.BaseLogEventPack; import org.kaaproject.kaa.server.common.thrift.gen.operations.ThriftEndpointConfigurationRefreshMessage; import org.kaaproject.kaa.server.common.thrift.gen.operations.ThriftEndpointDeregistrationMessage; import org.kaaproject.kaa.server.common.thrift.gen.operations.ThriftServerProfileUpdateMessage; import org.kaaproject.kaa.server.common.thrift.gen.operations.ThriftUnicastNotificationMessage; import org.kaaproject.kaa.server.operations.pojo.SyncContext; import org.kaaproject.kaa.server.operations.pojo.exceptions.GetDeltaException; import org.kaaproject.kaa.server.operations.service.akka.AkkaContext; import org.kaaproject.kaa.server.operations.service.akka.actors.core.endpoint.AbstractEndpointActorMessageProcessor; import org.kaaproject.kaa.server.operations.service.akka.actors.core.endpoint.local.ChannelMap.ChannelMetaData; import org.kaaproject.kaa.server.operations.service.akka.messages.core.endpoint.SyncRequestMessage; import org.kaaproject.kaa.server.operations.service.akka.messages.core.logs.LogDeliveryMessage; import org.kaaproject.kaa.server.operations.service.akka.messages.core.logs.LogEventPackMessage; import org.kaaproject.kaa.server.operations.service.akka.messages.core.route.ThriftEndpointActorMsg; import org.kaaproject.kaa.server.operations.service.akka.messages.core.session.ActorTimeoutMessage; import org.kaaproject.kaa.server.operations.service.akka.messages.core.session.ChannelTimeoutMessage; import org.kaaproject.kaa.server.operations.service.akka.messages.core.session.RequestTimeoutMessage; import org.kaaproject.kaa.server.operations.service.akka.messages.core.session.TimeoutMessage; 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.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.EndpointUserActionMessage; import org.kaaproject.kaa.server.operations.service.akka.messages.core.user.EndpointUserAttachMessage; import org.kaaproject.kaa.server.operations.service.akka.messages.core.user.EndpointUserConfigurationUpdateMessage; import org.kaaproject.kaa.server.operations.service.akka.messages.core.user.EndpointUserConnectMessage; import org.kaaproject.kaa.server.operations.service.akka.messages.core.user.EndpointUserDetachMessage; import org.kaaproject.kaa.server.operations.service.akka.messages.core.user.EndpointUserDisconnectMessage; import org.kaaproject.kaa.server.operations.service.akka.messages.core.user.verification.UserVerificationRequestMessage; import org.kaaproject.kaa.server.operations.service.akka.messages.core.user.verification.UserVerificationResponseMessage; import org.kaaproject.kaa.server.operations.service.akka.messages.io.response.NettySessionResponseMessage; import org.kaaproject.kaa.server.operations.service.akka.utils.EntityConvertUtils; import org.kaaproject.kaa.server.operations.service.event.EventClassFamilyVersion; import org.kaaproject.kaa.server.sync.ClientSync; import org.kaaproject.kaa.server.sync.EndpointAttachResponse; import org.kaaproject.kaa.server.sync.EndpointDetachRequest; import org.kaaproject.kaa.server.sync.EndpointDetachResponse; import org.kaaproject.kaa.server.sync.Event; import org.kaaproject.kaa.server.sync.EventClientSync; import org.kaaproject.kaa.server.sync.EventSequenceNumberResponse; import org.kaaproject.kaa.server.sync.EventServerSync; import org.kaaproject.kaa.server.sync.LogClientSync; import org.kaaproject.kaa.server.sync.LogEntry; import org.kaaproject.kaa.server.sync.ServerSync; import org.kaaproject.kaa.server.sync.SyncStatus; import org.kaaproject.kaa.server.sync.UserAttachNotification; import org.kaaproject.kaa.server.sync.UserAttachRequest; import org.kaaproject.kaa.server.sync.UserClientSync; import org.kaaproject.kaa.server.sync.UserDetachNotification; import org.kaaproject.kaa.server.sync.UserServerSync; import org.kaaproject.kaa.server.transport.EndpointRevocationException; import org.kaaproject.kaa.server.transport.channel.ChannelAware; import org.kaaproject.kaa.server.transport.channel.ChannelType; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import scala.concurrent.duration.Duration; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import java.util.UUID; import java.util.concurrent.TimeUnit; public class LocalEndpointActorMessageProcessor extends AbstractEndpointActorMessageProcessor<LocalEndpointActorState> { private static final Logger LOG = LoggerFactory.getLogger( LocalEndpointActorMessageProcessor.class); private final Map<Integer, LogDeliveryMessage> logUploadResponseMap; private final Map<UUID, UserVerificationResponseMessage> userAttachResponseMap; /** * All-args constructor. */ public LocalEndpointActorMessageProcessor(AkkaContext context, String appToken, EndpointObjectHash key, String actorKey) { super(new LocalEndpointActorState(Base64Util.encode(key.getData()), actorKey), context.getOperationsService(), appToken, key, actorKey, Base64Util.encode(key.getData()), context.getLocalEndpointTimeout()); this.logUploadResponseMap = new HashMap<>(); this.userAttachResponseMap = new LinkedHashMap<>(); } /** * Process an endpoint sync. * * @param context actor context * @param message sync request message */ public void processEndpointSync(ActorContext context, SyncRequestMessage message) { sync(context, message); } /** * Process an endpoint event receive message. * * @param context actor context * @param message endpoint event receive message */ public void processEndpointEventReceiveMessage(ActorContext context, EndpointEventReceiveMessage message) { EndpointEventDeliveryMessage response; if (state.isValidForEvents()) { Set<ChannelMetaData> eventChannels = state.getChannelsByType(TransportType.EVENT); if (!eventChannels.isEmpty()) { for (ChannelMetaData eventChannel : eventChannels) { addEventsAndReply(context, eventChannel, message); } response = new EndpointEventDeliveryMessage(message, EventDeliveryStatus.SUCCESS); } else { LOG.debug("[{}] Message ignored due to no channel contexts registered for events", actorKey, message); response = new EndpointEventDeliveryMessage(message, EventDeliveryStatus.FAILURE); state.setUserRegistrationPending(false); } } else { LOG.debug( "[{}][{}] Endpoint profile is not valid for receiving events. " + "Either no assigned user or no event families in sdk", endpointKey, actorKey); response = new EndpointEventDeliveryMessage(message, EventDeliveryStatus.FAILURE); } tellParent(context, response); } /** * Process a thrift notification. * * @param context actor context. */ public void processThriftNotification(ActorContext context) { Set<ChannelMetaData> channels = state.getChannelsByTypes( TransportType.CONFIGURATION, TransportType.NOTIFICATION); LOG.debug("[{}][{}] Processing thrift norification for {} channels", endpointKey, actorKey, channels.size()); syncChannels(context, channels, true, true); } /** * Process a user configuration update message. * * @param context actor context * @param message endpoint user configuration update message */ public void processUserConfigurationUpdate(ActorContext context, EndpointUserConfigurationUpdateMessage message) { if (message.getUserConfigurationUpdate() != null) { state.setUcfHash(message.getUserConfigurationUpdate().getHash()); refreshConfiguration(context); } } @Override protected void processThriftMsg(ActorContext context, ThriftEndpointActorMsg<?> msg) { Object thriftMsg = msg.getMsg(); if (thriftMsg instanceof ThriftServerProfileUpdateMessage) { processServerProfileUpdateMsg(context, (ThriftServerProfileUpdateMessage) thriftMsg); } else if (thriftMsg instanceof ThriftUnicastNotificationMessage) { processUnicastNotificationMsg(context, (ThriftUnicastNotificationMessage) thriftMsg); } else if (thriftMsg instanceof ThriftEndpointDeregistrationMessage) { processEndpointDeregistrationMessage(context, (ThriftEndpointDeregistrationMessage) thriftMsg); } else if (thriftMsg instanceof ThriftEndpointConfigurationRefreshMessage) { processEndpointSpecificConfigurationChanged(context); } } private void processEndpointSpecificConfigurationChanged(ActorContext context) { if (state.getProfile() == null) { state.setProfile(operationsService.refreshServerEndpointProfile(key)); } EndpointProfileDto profile = state.getProfile(); state.setEpsConfigurationHash(operationsService.fetchEndpointSpecificConfigurationHash(profile)); refreshConfiguration(context); } private void refreshConfiguration(ActorContext context) { syncChannels(context, state.getChannelsByTypes(TransportType.CONFIGURATION), true, false); } private void processServerProfileUpdateMsg(ActorContext context, ThriftServerProfileUpdateMessage thriftMsg) { EndpointProfileDto endpointProfile = state.getProfile(); if (endpointProfile != null) { state.setProfile(operationsService.refreshServerEndpointProfile(key)); Set<ChannelMetaData> channels = state.getChannelsByTypes( TransportType.CONFIGURATION, TransportType.NOTIFICATION); LOG.debug("[{}][{}] Processing profile update for {} channels", endpointKey, actorKey, channels.size()); syncChannels(context, channels, true, true); } else { LOG.warn("[{}][{}] Can't update server profile for an empty state", endpointKey, actorKey); } } private void processUnicastNotificationMsg(ActorContext context, ThriftUnicastNotificationMessage thriftMsg) { processNotification(context, NotificationMessage.fromUnicastId(thriftMsg.getNotificationId())); } private void processEndpointDeregistrationMessage( ActorContext context, ThriftEndpointDeregistrationMessage thriftMsg) { for (ChannelMetaData channel : state.getAllChannels()) { sendReply(context, channel.request, new EndpointRevocationException()); } } /** * Process a notification message. * * @param context actor context * @param message notification message */ public void processNotification(ActorContext context, NotificationMessage message) { LOG.debug("[{}][{}] Processing notification message {}", endpointKey, actorKey, message); Set<ChannelMetaData> channels = state.getChannelsByType(TransportType.NOTIFICATION); if (channels.isEmpty()) { LOG.debug("[{}][{}] No channels to process notification message", endpointKey, actorKey); return; } String unicastNotificationId = message.getUnicastNotificationId(); List<NotificationDto> validNfs = state.filter(message.getNotifications()); if (unicastNotificationId == null && validNfs.isEmpty()) { LOG.debug("[{}][{}] message is no longer valid for current endpoint", endpointKey, actorKey); return; } for (ChannelMetaData channel : channels) { LOG.debug("[{}][{}] processing channel {} and response {}", endpointKey, actorKey, channel, channel.getResponseHolder().getResponse()); ServerSync syncResponse = operationsService.updateSyncResponse( channel.getResponseHolder().getResponse(), validNfs, unicastNotificationId); if (syncResponse != null) { LOG.debug("[{}][{}] processed channel {} and response {}", endpointKey, actorKey, channel, syncResponse); sendReply(context, channel.getRequestMessage(), syncResponse); if (!channel.getType().isAsync()) { state.removeChannel(channel); } } } } /** * Process a request timeout message. * * @param context actor context * @param message request timeout message */ public void processRequestTimeoutMessage(ActorContext context, RequestTimeoutMessage message) { ChannelMetaData channel = state.getChannelByRequestId(message.getRequestId()); if (channel != null) { SyncContext response = channel.getResponseHolder(); sendReply(context, channel.getRequestMessage(), response.getResponse()); if (!channel.getType().isAsync()) { state.removeChannel(channel); } } else { LOG.debug("[{}][{}] Failed to find request by id [{}].", endpointKey, actorKey, message.getRequestId()); } } private void sync(ActorContext context, SyncRequestMessage requestMessage) { try { state.setLastActivityTime(System.currentTimeMillis()); long start = state.getLastActivityTime(); ChannelMetaData channel = initChannel(context, requestMessage); ClientSync request = mergeRequestForChannel(channel, requestMessage); ChannelType channelType = channel.getType(); LOG.debug("[{}][{}] Processing sync request {} from {} channel [{}]", endpointKey, actorKey, request, channelType, requestMessage.getChannelUuid()); SyncContext responseHolder = sync(request); state.setProfile(responseHolder.getEndpointProfile()); if (state.getProfile() != null) { processLogUpload(context, request, responseHolder); processUserAttachRequest(context, request, responseHolder); updateUserConnection(context); processEvents(context, request, responseHolder); notifyAffectedEndpoints(context, request, responseHolder); } else { LOG.warn("[{}][{}] Endpoint profile is not set after request processing!", endpointKey, actorKey); } LOG.debug("[{}][{}] SyncResponseHolder {}", endpointKey, actorKey, responseHolder); if (channelType.isAsync()) { LOG.debug("[{}][{}] Adding async request from channel [{}] to map ", endpointKey, actorKey, requestMessage.getChannelUuid()); channel.update(responseHolder); updateSubscriptionsToTopics(context, responseHolder); sendReply(context, requestMessage, responseHolder.getResponse()); } else { if (channelType.isLongPoll() && !responseHolder.requireImmediateReply()) { LOG.debug("[{}][{}] Adding long poll request from channel [{}] to map ", endpointKey, actorKey, requestMessage.getChannelUuid()); channel.update(responseHolder); updateSubscriptionsToTopics(context, responseHolder); scheduleTimeoutMessage( context, requestMessage.getChannelUuid(), getDelay(requestMessage, start)); } else { sendReply(context, requestMessage, responseHolder.getResponse()); state.removeChannel(channel); } } } catch (Exception ex) { LOG.error("[{}][{}] processEndpointRequest", endpointKey, actorKey, ex); sendReply(context, requestMessage, ex); } } private SyncContext sync(ClientSync request) throws GetDeltaException { if (!request.isValid()) { LOG.warn("[{}] Request is not valid. It does not contain profile information!", endpointKey); return SyncContext.failure(request.getRequestId()); } EndpointProfileDto profile = state.getProfile(); SyncContext context = new SyncContext(new ServerSync()); context.setNotificationVersion(state.getProfile()); context.setRequestId(request.getRequestId()); context.setStatus(SyncStatus.SUCCESS); context.setEndpointKey(endpointKey); context.setRequestHash(request.hashCode()); context.setMetaData(request.getClientSyncMetaData()); LOG.trace("[{}][{}] processing sync. Request: {}", endpointKey, context.getRequestHash(), request); context = operationsService.syncClientProfile(context, request.getProfileSync()); context = operationsService.syncUseConfigurationRawSchema( context, request.isUseConfigurationRawSchema()); if (context.getStatus() != SyncStatus.SUCCESS) { return context; } if (state.isUcfHashRequiresInitialization()) { byte[] hash = operationsService.fetchUcfHash(appToken, profile); LOG.debug("[{}][{}] Initialized endpoint user configuration hash {}", endpointKey, context.getRequestHash(), Arrays.toString(hash)); state.setUcfHash(hash); } if (state.isEpsConfigurationRequiresInitialization()) { state.setEpsConfigurationHash(operationsService.fetchEndpointSpecificConfigurationHash(profile)); } context = operationsService.processEndpointAttachDetachRequests( context, request.getUserSync()); context = operationsService.processEventListenerRequests(context, request.getEventSync()); if (state.isUserConfigurationUpdatePending() || state.isEpsConfigurationChanged()) { context = operationsService.syncConfigurationHashes(context, state.getUcfHash(), state.getEpsConfigurationHash()); } context = operationsService.syncConfiguration(context, request.getConfigurationSync()); context = operationsService.syncNotification(context, request.getNotificationSync()); LOG.trace("[{}][{}] processed sync. Response is {}", endpointKey, request.hashCode(), context.getResponse()); return context; } private void syncChannels(ActorContext context, Set<ChannelMetaData> channels, boolean cfUpdate, boolean nfUpdate) { for (ChannelMetaData channel : channels) { ClientSync originalRequest = channel.getRequestMessage().getRequest(); ClientSync newRequest = new ClientSync(); newRequest.setRequestId(originalRequest.getRequestId()); newRequest.setClientSyncMetaData(originalRequest.getClientSyncMetaData()); newRequest.setUseConfigurationRawSchema(originalRequest.isUseConfigurationRawSchema()); if (cfUpdate && originalRequest.getConfigurationSync() != null) { newRequest.setForceConfigurationSync(true); newRequest.setConfigurationSync(originalRequest.getConfigurationSync()); } if (nfUpdate && originalRequest.getNotificationSync() != null) { newRequest.setForceNotificationSync(true); newRequest.setNotificationSync(originalRequest.getNotificationSync()); } LOG.debug("[{}][{}] Processing request {}", endpointKey, actorKey, newRequest); sync(context, new SyncRequestMessage(channel.getRequestMessage().getSession(), newRequest, channel.getRequestMessage().getCommand(), channel.getRequestMessage().getOriginator())); } } private ClientSync mergeRequestForChannel(ChannelMetaData channel, SyncRequestMessage requestMessage) { ClientSync request; if (channel.getType().isAsync()) { if (channel.isFirstRequest()) { request = channel.getRequestMessage().getRequest(); } else { LOG.debug("[{}][{}] Updating request for async channel {}", endpointKey, actorKey, channel); request = channel.mergeRequest(requestMessage); LOG.trace("[{}][{}] Updated request for async channel {} : {}", endpointKey, actorKey, channel, request); } } else { request = channel.getRequestMessage().getRequest(); } return request; } private void processUserAttachRequest(ActorContext context, ClientSync syncRequest, SyncContext responseHolder) { UserClientSync request = syncRequest.getUserSync(); if (request != null && request.getUserAttachRequest() != null) { UserAttachRequest attachRequest = request.getUserAttachRequest(); context.parent().tell(new UserVerificationRequestMessage( context.self(), attachRequest.getUserVerifierId(), attachRequest.getUserExternalId(), attachRequest.getUserAccessToken()), context.self()); LOG.debug("[{}][{}] received and forwarded user attach request {}", endpointKey, actorKey, request.getUserAttachRequest()); if (userAttachResponseMap.size() > 0) { Entry<UUID, UserVerificationResponseMessage> entryToSend = userAttachResponseMap.entrySet() .iterator() .next(); updateResponseWithUserAttachResults(responseHolder.getResponse(), entryToSend.getValue()); userAttachResponseMap.remove(entryToSend.getKey()); } } } private void updateResponseWithUserAttachResults(ServerSync response, UserVerificationResponseMessage message) { if (response.getUserSync() == null) { response.setUserSync(new UserServerSync()); } response.getUserSync().setUserAttachResponse(EntityConvertUtils.convert(message)); } private void processEvents(ActorContext context, ClientSync request, SyncContext responseHolder) { if (request.getEventSync() != null) { EventClientSync eventRequest = request.getEventSync(); processSeqNumber(eventRequest, responseHolder); if (state.isValidForEvents()) { sendEventsIfPresent(context, eventRequest); } else { LOG.debug( "[{}][{}] Endpoint profile is not valid for send/receive events. " + "Either no assigned user or no event families in sdk", endpointKey, actorKey); } } } private void processSeqNumber(EventClientSync request, SyncContext responseHolder) { if (request.isSeqNumberRequest()) { EventServerSync response = responseHolder.getResponse().getEventSync(); if (response == null) { response = new EventServerSync(); responseHolder.getResponse().setEventSync(response); } response.setEventSequenceNumberResponse(new EventSequenceNumberResponse( Math.max(state.getEventSeqNumber(), 0))); } } private void updateUserConnection(ActorContext context) { if (!state.isValidForUser()) { return; } if (state.userIdMismatch()) { sendDisconnectFromOldUser(context, state.getProfile()); state.setUserRegistrationPending(false); } if (!state.isUserRegistrationPending()) { state.setUserId(state.getProfileUserId()); if (state.getUserId() != null) { sendConnectToNewUser(context, state.getProfile()); state.setUserRegistrationPending(true); } } else { LOG.trace("[{}][{}] User registration request is already sent.", endpointKey, actorKey); } } private void processLogUpload(ActorContext context, ClientSync syncRequest, SyncContext responseHolder) { LogClientSync request = syncRequest.getLogSync(); if (request != null) { if (request.getLogEntries() != null && request.getLogEntries().size() > 0) { LOG.debug("[{}][{}] Processing log upload request {}", endpointKey, actorKey, request.getLogEntries().size()); EndpointProfileDataDto profileDto = convert(responseHolder.getEndpointProfile()); List<LogEvent> logEvents = new ArrayList<>(request.getLogEntries().size()); for (LogEntry logEntry : request.getLogEntries()) { LogEvent logEvent = new LogEvent(); logEvent.setLogData(logEntry.getData().array()); logEvents.add(logEvent); } BaseLogEventPack logPack = new BaseLogEventPack(profileDto, System.currentTimeMillis(), responseHolder.getEndpointProfile().getLogSchemaVersion(), logEvents); logPack.setUserId(state.getUserId()); context.parent().tell(new LogEventPackMessage( request.getRequestId(), context.self(), logPack), context.self()); } if (logUploadResponseMap.size() > 0) { responseHolder.getResponse().setLogSync(EntityConvertUtils.convert(logUploadResponseMap)); logUploadResponseMap.clear(); } } } private EndpointProfileDataDto convert(EndpointProfileDto profileDto) { return new EndpointProfileDataDto( profileDto.getId(), endpointKey, profileDto.getClientProfileVersion(), profileDto.getClientProfileBody(), profileDto.getServerProfileVersion(), profileDto.getServerProfileBody()); } private void sendConnectToNewUser(ActorContext context, EndpointProfileDto endpointProfile) { List<EventClassFamilyVersion> ecfVersions = EntityConvertUtils.convertToEcfVersions( endpointProfile.getEcfVersionStates()); EndpointUserConnectMessage userRegistrationMessage = new EndpointUserConnectMessage( state.getUserId(), key, ecfVersions, endpointProfile.getConfigurationVersion(), endpointProfile.getUserConfigurationHash(), appToken, context.self()); LOG.debug("[{}][{}] Sending user registration request {}", endpointKey, actorKey, userRegistrationMessage); context.parent().tell(userRegistrationMessage, context.self()); } private void sendDisconnectFromOldUser(ActorContext context, EndpointProfileDto endpointProfile) { LOG.debug("[{}][{}] Detected user change from [{}] to [{}]", endpointKey, actorKey, state.getUserId(), endpointProfile.getEndpointUserId()); EndpointUserDisconnectMessage userDisconnectMessage = new EndpointUserDisconnectMessage( state.getUserId(), key, appToken, context.self()); context.parent().tell(userDisconnectMessage, context.self()); } private long getDelay(SyncRequestMessage requestMessage, long start) { return requestMessage.getRequest() .getClientSyncMetaData() .getTimeout() - (System.currentTimeMillis() - start); } private ChannelMetaData initChannel(ActorContext context, SyncRequestMessage requestMessage) { ChannelMetaData channel = state.getChannelById(requestMessage.getChannelUuid()); if (channel == null) { channel = new ChannelMetaData(requestMessage); if (!channel.getType().isAsync() && channel.getType().isLongPoll()) { LOG.debug("[{}][{}] Received request using long poll channel.", endpointKey, actorKey); // Probably old long poll channels lost connection. Sending // reply to them just in case Set<ChannelMetaData> channels = state.getChannelsByType(TransportType.EVENT); for (ChannelMetaData oldChannel : channels) { if (!oldChannel.getType().isAsync() && channel.getType().isLongPoll()) { LOG.debug("[{}][{}] Closing old long poll channel [{}]", endpointKey, actorKey, oldChannel.getId()); sendReply( context, oldChannel.getRequestMessage(), oldChannel.getResponseHolder().getResponse()); state.removeChannel(oldChannel); } } } long time = System.currentTimeMillis(); channel.setLastActivityTime(time); if (channel.getType().isAsync() && channel.getKeepAlive() > 0) { scheduleKeepAliveCheck(context, channel); } state.addChannel(channel); } return channel; } private void scheduleKeepAliveCheck(ActorContext context, ChannelMetaData channel) { TimeoutMessage message = new ChannelTimeoutMessage( channel.getId(), channel.getLastActivityTime()); LOG.debug("Scheduling channel timeout message: {} to timeout in {}", message, channel.getKeepAlive() * 1000); scheduleTimeoutMessage(context, message, channel.getKeepAlive() * 1000); } private void notifyAffectedEndpoints(ActorContext context, ClientSync request, SyncContext responseHolder) { if (responseHolder.getResponse().getUserSync() != null) { List<EndpointAttachResponse> attachResponses = responseHolder.getResponse() .getUserSync() .getEndpointAttachResponses(); if (attachResponses != null && !attachResponses.isEmpty()) { state.resetEventSeqNumber(); for (EndpointAttachResponse response : attachResponses) { if (response.getResult() != SyncStatus.SUCCESS) { LOG.debug("[{}][{}] Skipped unsuccessful attach response [{}]", endpointKey, actorKey, response.getRequestId()); continue; } EndpointUserAttachMessage attachMessage = new EndpointUserAttachMessage( EndpointObjectHash.fromBytes( Base64Util.decode(response.getEndpointKeyHash())), state.getUserId(), endpointKey); context.parent().tell(attachMessage, context.self()); LOG.debug("[{}][{}] Notification to attached endpoint [{}] sent", endpointKey, actorKey, response.getEndpointKeyHash()); } } List<EndpointDetachRequest> detachRequests = request.getUserSync() == null ? null : request.getUserSync().getEndpointDetachRequests(); if (detachRequests != null && !detachRequests.isEmpty()) { state.resetEventSeqNumber(); for (EndpointDetachRequest detachRequest : detachRequests) { List<EndpointDetachResponse> endpointDetachResponses = responseHolder.getResponse() .getUserSync() .getEndpointDetachResponses(); for (EndpointDetachResponse detachResponse : endpointDetachResponses) { if (detachRequest.getRequestId() == detachResponse.getRequestId()) { if (detachResponse.getResult() != SyncStatus.SUCCESS) { LOG.debug("[{}][{}] Skipped unsuccessful detach response [{}]", endpointKey, actorKey, detachResponse.getRequestId()); continue; } EndpointUserDetachMessage attachMessage = new EndpointUserDetachMessage( EndpointObjectHash.fromBytes( Base64Util.decode(detachRequest.getEndpointKeyHash())), state.getUserId(), endpointKey); context.parent().tell(attachMessage, context.self()); LOG.debug("[{}][{}] Notification to detached endpoint [{}] sent", endpointKey, actorKey, detachRequest.getEndpointKeyHash()); } } } } } } protected void scheduleActorTimeout(ActorContext context) { if (state.isNoChannels()) { scheduleTimeoutMessage( context, new ActorTimeoutMessage(state.getLastActivityTime()), getInactivityTimeout()); } } /** * Subscribe to topics. * * @param response the response */ private void updateSubscriptionsToTopics(ActorContext context, SyncContext response) { Map<String, Integer> newStates = response.getSubscriptionStates(); if (newStates == null) { return; } Map<String, Integer> currentStates = state.getSubscriptionStates(); // detect and remove unsubscribed topics; Iterator<String> currentSubscriptionsIterator = currentStates.keySet().iterator(); while (currentSubscriptionsIterator.hasNext()) { String subscribedTopic = currentSubscriptionsIterator.next(); if (!newStates.containsKey(subscribedTopic)) { currentSubscriptionsIterator.remove(); TopicUnsubscriptionMessage topicSubscriptionMessage = new TopicUnsubscriptionMessage( subscribedTopic, appToken, key, context.self()); context.parent().tell(topicSubscriptionMessage, context.self()); } } // subscribe to new topics; for (Entry<String, Integer> entry : newStates.entrySet()) { if (!currentStates.containsKey(entry.getKey())) { TopicSubscriptionMessage topicSubscriptionMessage = new TopicSubscriptionMessage( entry.getKey(), entry.getValue(), response.getSystemNfVersion(), response.getUserNfVersion(), appToken, key, context.self()); context.parent().tell(topicSubscriptionMessage, context.self()); } } state.setSubscriptionStates(newStates); } private void scheduleTimeoutMessage(ActorContext context, UUID requestId, long delay) { scheduleTimeoutMessage(context, new RequestTimeoutMessage(requestId), delay); } private void scheduleTimeoutMessage(ActorContext context, TimeoutMessage message, long delay) { context.system() .scheduler() .scheduleOnce( Duration.create(delay, TimeUnit.MILLISECONDS), context.self(), message, context.dispatcher(), context.self()); } private void addEventsAndReply(ActorContext context, ChannelMetaData channel, EndpointEventReceiveMessage message) { SyncRequestMessage pendingRequest = channel.getRequestMessage(); SyncContext pendingResponse = channel.getResponseHolder(); EventServerSync eventResponse = pendingResponse.getResponse().getEventSync(); if (eventResponse == null) { eventResponse = new EventServerSync(); pendingResponse.getResponse().setEventSync(eventResponse); } eventResponse.setEvents(message.getEvents()); sendReply(context, pendingRequest, pendingResponse.getResponse()); if (!channel.getType().isAsync()) { state.removeChannel(channel); } } private void sendReply(ActorContext context, SyncRequestMessage request, ServerSync syncResponse) { sendReply(context, request, null, syncResponse); } private void sendReply(ActorContext context, SyncRequestMessage request, Exception ex) { sendReply(context, request, ex, null); } private void sendReply(ActorContext context, SyncRequestMessage request, Exception ex, ServerSync syncResponse) { LOG.debug("[{}] response: {}", actorKey, syncResponse); ServerSync copy = ServerSync.deepCopy(syncResponse); ServerSync.cleanup(syncResponse); NettySessionResponseMessage response = new NettySessionResponseMessage( request.getSession(), copy, ex, request.getCommand().getMessageBuilder(), request.getCommand().getErrorBuilder()); tellActor(context, request.getOriginator(), response); scheduleActorTimeout(context); } protected void sendEventsIfPresent(ActorContext context, EventClientSync request) { List<Event> events = request.getEvents(); if (state.getUserId() != null && events != null && !events.isEmpty()) { LOG.debug("[{}][{}] Processing events {} with seq number > {}", endpointKey, actorKey, events, state.getEventSeqNumber()); List<Event> eventsToSend = new ArrayList<>(events.size()); int maxSentEventSeqNum = state.getEventSeqNumber(); for (Event event : events) { if (event.getSeqNum() > state.getEventSeqNumber()) { event.setSource(endpointKey); eventsToSend.add(event); maxSentEventSeqNum = Math.max(event.getSeqNum(), maxSentEventSeqNum); } else { LOG.debug("[{}][{}] Ignoring duplicate/old event {} due to seq number < {}", endpointKey, actorKey, events, state.getEventSeqNumber()); } } state.setEventSeqNumber(maxSentEventSeqNum); if (!eventsToSend.isEmpty()) { EndpointEventSendMessage message = new EndpointEventSendMessage( state.getUserId(), eventsToSend, key, appToken, context.self()); context.parent().tell(message, context.self()); } } } /** * Process an endpoint user action message. * * @param context actor context * @param message endpoint user action message */ public void processEndpointUserActionMessage(ActorContext context, EndpointUserActionMessage message) { Set<ChannelMetaData> eventChannels = state.getChannelsByTypes( TransportType.EVENT, TransportType.USER); LOG.debug("[{}][{}] Current Endpoint was attached/detached from user. " + "Need to close all current event channels {}", endpointKey, actorKey, eventChannels.size()); state.setUserRegistrationPending(false); state.setProfile(operationsService.refreshServerEndpointProfile(key)); if (message instanceof EndpointUserAttachMessage) { LOG.debug("[{}][{}] Updating endpoint user id to {} in profile", endpointKey, actorKey, message.getUserId()); } else if (message instanceof EndpointUserDetachMessage) { LOG.debug("[{}][{}] Clanup endpoint user id in profile", endpointKey, actorKey, message.getUserId()); } if (!eventChannels.isEmpty()) { updateUserConnection(context); for (ChannelMetaData channel : eventChannels) { SyncRequestMessage pendingRequest = channel.getRequestMessage(); ServerSync pendingResponse = channel.getResponseHolder().getResponse(); UserServerSync userSyncResponse = pendingResponse.getUserSync(); if (userSyncResponse == null && pendingRequest.isValid(TransportType.USER)) { userSyncResponse = new UserServerSync(); pendingResponse.setUserSync(userSyncResponse); } if (userSyncResponse != null) { if (message instanceof EndpointUserAttachMessage) { userSyncResponse .setUserAttachNotification( new UserAttachNotification(message.getUserId(), message.getOriginator())); LOG.debug("[{}][{}] Adding user attach notification", endpointKey, actorKey); } else if (message instanceof EndpointUserDetachMessage) { userSyncResponse.setUserDetachNotification( new UserDetachNotification(message.getOriginator())); LOG.debug("[{}][{}] Adding user detach notification", endpointKey, actorKey); } } LOG.debug("[{}][{}] sending reply to [{}] channel", endpointKey, actorKey, channel.getId()); sendReply(context, pendingRequest, pendingResponse); if (!channel.getType().isAsync()) { state.removeChannel(channel); } } } else { LOG.debug("[{}][{}] Message ignored due to no channel contexts registered for events", endpointKey, actorKey, message); } } /** * Process disconnect message. * * @param context actor context * @param message channel aware message * @return true if channel is disconnected otherwise false */ public boolean processDisconnectMessage(ActorContext context, ChannelAware message) { LOG.debug("[{}][{}] Received disconnect message for channel [{}]", endpointKey, actorKey, message.getChannelUuid()); ChannelMetaData channel = state.getChannelById(message.getChannelUuid()); if (channel != null) { state.removeChannel(channel); scheduleActorTimeout(context); return true; } else { LOG.debug("[{}][{}] Can't find channel by uuid [{}]", endpointKey, actorKey, message.getChannelUuid()); return false; } } /** * Process a ping message. * * @param context actor context * @param message channel aware message * @return true if channel is found otherwise false */ public boolean processPingMessage(ActorContext context, ChannelAware message) { LOG.debug("[{}][{}] Received ping message for channel [{}]", endpointKey, actorKey, message.getChannelUuid()); ChannelMetaData channel = state.getChannelById(message.getChannelUuid()); if (channel != null) { long lastActivityTime = System.currentTimeMillis(); LOG.debug("[{}][{}] Updating last activity time for channel [{}] to ", endpointKey, actorKey, message.getChannelUuid(), lastActivityTime); channel.setLastActivityTime(lastActivityTime); channel.getContext().writeAndFlush(new PingResponse()); return true; } else { LOG.debug("[{}][{}] Can't find channel by uuid [{}]", endpointKey, actorKey, message.getChannelUuid()); return false; } } /** * Process a timeout message. * * @param context actor context * @param message channel timeout message * @return true if channel is removed otherwise false */ public boolean processChannelTimeoutMessage(ActorContext context, ChannelTimeoutMessage message) { LOG.debug("[{}][{}] Received channel timeout message for channel [{}]", endpointKey, actorKey, message.getChannelUuid()); ChannelMetaData channel = state.getChannelById(message.getChannelUuid()); if (channel != null) { if (channel.getLastActivityTime() <= message.getLastActivityTime()) { LOG.debug("[{}][{}] Timeout message accepted for channel [{}]. " + "Last activity time {} and timeout is {} ", endpointKey, actorKey, message.getChannelUuid(), channel.getLastActivityTime(), message.getLastActivityTime()); state.removeChannel(channel); scheduleActorTimeout(context); return true; } else { LOG.debug("[{}][{}] Timeout message ignored for channel [{}]. " + "Last activity time {} and timeout is {} ", endpointKey, actorKey, message.getChannelUuid(), channel.getLastActivityTime(), message.getLastActivityTime()); scheduleKeepAliveCheck(context, channel); return false; } } else { LOG.debug("[{}][{}] Can't find channel by uuid [{}]", endpointKey, actorKey, message.getChannelUuid()); scheduleActorTimeout(context); return false; } } /** * Process a log delivery message. * * @param context actor context * @param message log delivery message */ public void processLogDeliveryMessage(ActorContext context, LogDeliveryMessage message) { LOG.debug("[{}][{}] Received log delivery message for request [{}] with status {}", endpointKey, actorKey, message.getRequestId(), message.isSuccess()); logUploadResponseMap.put(message.getRequestId(), message); Set<ChannelMetaData> channels = state.getChannelsByType(TransportType.LOGGING); for (ChannelMetaData channel : channels) { SyncRequestMessage pendingRequest = channel.getRequestMessage(); ServerSync pendingResponse = channel.getResponseHolder().getResponse(); pendingResponse.setLogSync(EntityConvertUtils.convert(logUploadResponseMap)); LOG.debug("[{}][{}] sending reply to [{}] channel", endpointKey, actorKey, channel.getId()); sendReply(context, pendingRequest, pendingResponse); if (!channel.getType().isAsync()) { state.removeChannel(channel); } } logUploadResponseMap.clear(); } /** * Process a user verification message. * * @param context actor context * @param message user verification response message */ public void processUserVerificationMessage(ActorContext context, UserVerificationResponseMessage message) { LOG.debug("[{}][{}] Received user verification message for request [{}] with status {}", endpointKey, actorKey, message.getRequestId(), message.isSuccess()); userAttachResponseMap.put(message.getRequestId(), message); Set<ChannelMetaData> channels = state.getChannelsByType(TransportType.USER); Entry<UUID, UserVerificationResponseMessage> entryToSend = userAttachResponseMap.entrySet() .iterator() .next(); for (ChannelMetaData channel : channels) { SyncRequestMessage pendingRequest = channel.getRequestMessage(); ServerSync pendingResponse = channel.getResponseHolder().getResponse(); updateResponseWithUserAttachResults(pendingResponse, entryToSend.getValue()); LOG.debug("[{}][{}] sending reply to [{}] channel", endpointKey, actorKey, channel.getId()); sendReply(context, pendingRequest, pendingResponse); if (!channel.getType().isAsync()) { state.removeChannel(channel); } } userAttachResponseMap.remove(entryToSend.getKey()); if (message.isSuccess()) { state.setProfile( operationsService.attachEndpointToUser( state.getProfile(), appToken, message.getUserId())); updateUserConnection(context); } } }