/******************************************************************************* * Copyright 2015 Klaus Pfeiffer - klaus@allpiper.com * * 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 com.jfastnet.processors; import com.jfastnet.Config; import com.jfastnet.IServerHooks; import com.jfastnet.State; import com.jfastnet.idprovider.ReliableModeIdProvider; import com.jfastnet.messages.Message; import com.jfastnet.messages.StackAckMessage; import com.jfastnet.messages.StackedMessage; import lombok.Getter; import lombok.Setter; import lombok.experimental.Accessors; import lombok.extern.slf4j.Slf4j; import java.util.*; import java.util.concurrent.ConcurrentHashMap; /** @author Klaus Pfeiffer - klaus@allpiper.com */ @Slf4j public class StackedMessageProcessor extends AbstractMessageProcessor<StackedMessageProcessor.ProcessorConfig> implements IMessageReceiverPreProcessor, IMessageSenderPreProcessor, IServerHooks { @SuppressWarnings("unchecked") private static final Stack EMPTY_STACK = new Stack(0, Collections.EMPTY_LIST); /** Client id, Msg Id. */ @Getter private Map<Integer, Long> lastAckMessageIdMap = new ConcurrentHashMap<>(); /** Stacked messaged id that I acknowledged to the other side. */ @Setter private long myLastAckMessageId; private long myLastAckMessageTimestamp; private Map<Long, Message> unacknowledgedSentMessagesMap = new ConcurrentHashMap<>(); public StackedMessageProcessor(Config config, State state) { super(config, state); if (!config.idProviderClass.equals(ReliableModeIdProvider.class)) { log.warn("StackedMessageProcessor only works with the ReliableModeIdProvider."); } } @Override public void onUnregister(int clientId) { lastAckMessageIdMap.remove(clientId); } @Override public Message beforeReceive(Message message) { if (message instanceof StackAckMessage) { StackAckMessage stackAckMessage = (StackAckMessage) message; log.trace("Received acknowledge message for id {}", stackAckMessage.getLastReceivedId()); long lastId = lastAckMessageIdMap.getOrDefault(message.getSenderId(), 0L); if (lastId < stackAckMessage.getLastReceivedId()) { lastAckMessageIdMap.put(message.getSenderId(), stackAckMessage.getLastReceivedId()); } return null; } if (message instanceof StackedMessage) { StackedMessage stackedMessage = (StackedMessage) message; if (stackedMessage.getMessages().size() > 0) { receiveStackedMessage(stackedMessage); Message lastReceivedMessage = stackedMessage.getMessages().get(stackedMessage.getMessages().size() - 1); long lastReceivedId = lastReceivedMessage.getMsgId(); boolean receivedMessageCountExceedsAckThreshold = lastReceivedId - myLastAckMessageId >= processorConfig.stackedMessagesAckThreshold; boolean timeFrameExceedsAckThreshold = config.timeProvider.get() > myLastAckMessageTimestamp + processorConfig.stackedMessagesAckTimeThresholdMs; if (receivedMessageCountExceedsAckThreshold || timeFrameExceedsAckThreshold) { config.internalSender.send(new StackAckMessage(lastReceivedId)); myLastAckMessageId = lastReceivedId; myLastAckMessageTimestamp = config.timeProvider.get(); log.trace("Send acknowledge message for id: {}", lastReceivedId); } } return null; } return message; } private void receiveStackedMessage(StackedMessage stackedMessage) { for (Message message : stackedMessage.getMessages()) { message.copyAttributesFrom(stackedMessage); log.trace("Received stack message: {}", message); config.internalReceiver.receive(message); } } @Override public Message beforeSend(Message message) { // Check if message is "stackable" and don't stack messages that get resent // Check for ReliableMode.SEQUENCE_NUMBER, so we can be sure about correct ascending message ids if (state.isEnableStackedMessages() && message.stackable() && !message.isResendMessage() && message.getReliableMode() == Message.ReliableMode.SEQUENCE_NUMBER) { checkCorrectIdProvider(); cleanUpUnacknowledgedSentMessagesMap(); unacknowledgedSentMessagesMap.put(message.getMsgId(), message); if (isSentToAllFromServer(message.getReceiverId())) { state.getProcessorOf(MessageLogProcessor.class).afterSend(message); Set<Integer> clientIds = state.getClientStates().idSet(); clientIds.forEach(id -> { Stack stack = createStackForReceiver(message, id); stack.send(config, state); }); return null; // Discard message } else { Stack stack = createStackForReceiver(message, message.getReceiverId()); if (stack.stackSendingIsReasonable()) { state.getProcessorOf(MessageLogProcessor.class).afterSend(message); stack.send(config, state); return null; // Discard message } } } return message; } private void checkCorrectIdProvider() { if (!config.idProviderClass.equals(ReliableModeIdProvider.class)) { throw new UnsupportedOperationException("StackedMessageProcessor only works with the ReliableModeIdProvider!"); } } private void cleanUpUnacknowledgedSentMessagesMap() { if (state.isHost()) { Set<Integer> clientIds = state.getClientStates().idSet(); long maxAckId = 0L; for (Integer clientId : clientIds) { long clientLastAckId = lastAckMessageIdMap.getOrDefault(clientId, 0L); maxAckId = Math.min(maxAckId, clientLastAckId); } final long finalMaxAckId = maxAckId; unacknowledgedSentMessagesMap.keySet().removeIf(id -> id < finalMaxAckId); } else { long serverLastAckId = lastAckMessageIdMap.getOrDefault(0, 0L); unacknowledgedSentMessagesMap.keySet().removeIf(id -> id < serverLastAckId); } } /** Create individual stack for receiver id. */ private Stack createStackForReceiver(Message newMessage, int receiverId) { long startStackMsgId = lastAckMessageIdMap.getOrDefault(receiverId, -1L) + 1L; Stack stack = EMPTY_STACK; // At least the new message id must be present if (newMessage.getMsgId() >= startStackMsgId) { List<Message> messages = new ArrayList<>(); log.trace( " ++++ begin stack ++++"); for (long msgId = startStackMsgId; msgId <= newMessage.getMsgId() && messages.size() < processorConfig.maximumNumberOfMessagesPerStack; msgId++) { Message stackMsg = unacknowledgedSentMessagesMap.get(msgId); if (stackMsg != null) { // Be tolerant about missing ids. // Not every id has to be present, because some messages // may not be stackable. messages.add(stackMsg); log.trace(" ** added to stack: {}", stackMsg); } else { log.trace(" ** not added to stack: {}", msgId); } } stack = new Stack(receiverId, messages); log.trace( " ++++ end stack ++++"); } return stack; } private boolean isSentToAllFromServer(int receiverId) { return config.senderId == 0 && receiverId == 0; } @Override public Class<ProcessorConfig> getConfigClass() { return ProcessorConfig.class; } @Setter @Getter @Accessors(chain = true) public static class ProcessorConfig { /** After X received stacked messages we send an ack packet. */ public int stackedMessagesAckThreshold = 7; public int maximumNumberOfMessagesPerStack = stackedMessagesAckThreshold * 3; /** The next message after X milliseconds we send an ack packet. */ public int stackedMessagesAckTimeThresholdMs = 300; } private static class Stack { private final int receiverId; private final List<Message> messages; public Stack(int receiverId, List<Message> messages) { this.receiverId = receiverId; this.messages = messages; } boolean stackSendingIsReasonable() { return messages.size() > 1; } void send(Config config, State state) { if (messages.isEmpty()) { log.trace("Stack was empty on send."); return; } StackedMessage stackedMessage = new StackedMessage(messages); stackedMessage.setReceiverId(receiverId); if (!state.idProvider.resolveEveryClientMessage()) { // Clear receiver id, if every client receives the same id for a particular message messages.forEach(message -> message.setReceiverId(0)); } config.internalSender.send(stackedMessage); } } }