package de.tum.in.www1.jReto.connectivity; import java.nio.ByteBuffer; import java.util.Arrays; import java.util.HashSet; import java.util.Set; import java.util.UUID; import java.util.concurrent.Executor; import de.tum.in.www1.jReto.connectivity.packet.CloseAcknowledge; import de.tum.in.www1.jReto.connectivity.packet.CloseAnnounce; import de.tum.in.www1.jReto.connectivity.packet.CloseRequest; import de.tum.in.www1.jReto.packet.PacketType; import de.tum.in.www1.jReto.util.RepeatedExecutor; /** * The ReliablityManager is responsible for cleanly closing connections and attempting to reconnect failed connections. * * TODO: This class currently assumes that if connecting takes longer than a specific amount of time (1 second), the connection attempt failed and tries to reconnect. * This causes issues if establishing the connection takes longer than 1 second, especially with routed/multicast connections, causing the connection establishment process to fail. * To fix this, the next attempt should only be started after a generous timeout or if the connection establishment process failed, and not if it is still in progress. */ public class ReliablitiyManager implements PacketConnection.Handler { /** * The ReliablityManager's delegate protocol. */ public static interface ReliabilityManagerHandler { /** Called when the connection connected successfully. */ void onConnectionConnected(); /** Called when the connection closes expectedly (i.e. when close() was called on the connection) */ void onExpectedConnectionClose(); /** Called when the connection failed and could not be reestablished. */ void onUnexpectedFinalConnectionClose(); } /** * A ConnectionManager manages PacketConnections. */ public static interface PacketConnectionManager { /** * Called when a connection closed. */ void notifyConnectionClose(PacketConnection connection); /** * Called when a new underlying connection needs to be established for an packet connection. */ void establishUnderlyingConnection(PacketConnection connection); } /** The delegate */ private ReliabilityManagerHandler handler; /** The PacketConnection thats reliability is managed. */ private final PacketConnection packetConnection; /** The connection manager */ private final PacketConnectionManager packetConnectionManager; /** The local peer's identifier */ private final UUID localIdentifier; /** Set to true when the underlying connection is expected to close. */ private boolean isExpectingConnectionToClose; /** Set to true if this ReliabilityManager is expected to attempt to reconnect when a connection fails. */ private final boolean isExpectedToReconnect; /** The executor used to repeat reconnect attempts */ private final RepeatedExecutor reconnectRepeater; /** The number of reconnect attempts that have been performed */ private int reconnectAttempts = 0; /** All of the managed connection's destination's identifiers */ private final Set<UUID> connectionDestinations; /** The number of close acknowledge packets received. Necessary since acknowledgements need to be received from all destinations before a connection can be closed safely. */ private final Set<UUID> receivedCloseAcknowledgements = new HashSet<>(); /** * Constructs a new ReliablityManager. * @param packetConnection The packet connection managed * @param packetConnectionManager The connection manager responsible for the packet connection. * @param localIdentifier The local peer's identifier. * @param connectionDestinations The destinations of the connection * @param isExpectedToReconnect If set to true, this ReliabilityManager will attempt to reconnect the packet connection if its underlying connection fails. * @param executor The Executor on which all delegate method calls are dispatched. */ public ReliablitiyManager(PacketConnection packetConnection, PacketConnectionManager packetConnectionManager, UUID localIdentifier, Set<UUID> connectionDestinations, boolean isExpectedToReconnect, Executor executor) { this.packetConnection = packetConnection; this.packetConnectionManager = packetConnectionManager; this.localIdentifier = localIdentifier; this.connectionDestinations = connectionDestinations; this.isExpectingConnectionToClose = false; this.isExpectedToReconnect = isExpectedToReconnect; this.reconnectRepeater = new RepeatedExecutor(new Runnable() { @Override public void run() { ReliablitiyManager.this.attemptConnect(); } }, 2, 1, executor); this.packetConnection.addDelegate(this); } /** Closes the packet connection cleanly. */ public void closeConnection() { if (this.isExpectedToReconnect) { this.packetConnection.writePacket(new CloseAnnounce()); } else { this.packetConnection.writePacket(new CloseRequest()); } } private void handleCloseRequest() { this.isExpectingConnectionToClose = true; this.packetConnection.writePacket(new CloseAnnounce()); } private void handleCloseAnnounce() { this.isExpectingConnectionToClose = true; this.packetConnection.writePacket(new CloseAcknowledge(this.localIdentifier)); } private void handleCloseAcknowledge(CloseAcknowledge acknowledgePacket) { this.isExpectingConnectionToClose = true; this.receivedCloseAcknowledgements.add(acknowledgePacket.source); if (this.receivedCloseAcknowledgements.equals(this.connectionDestinations)) { this.receivedCloseAcknowledgements.clear(); this.packetConnection.disconnectUnderlyingConnection(); } } /** Attempts to connect a PacketConnection with no or a failed underlying connection. */ public void attemptConnect() { if (this.packetConnection.getIsConnected()) return; this.reconnectAttempts++; if (this.reconnectAttempts > 8) { this.reconnectRepeater.stop(); this.handler.onUnexpectedFinalConnectionClose(); } else { if (this.reconnectAttempts > 1) System.out.println("Attempting to connect (try "+this.reconnectAttempts+")..."); this.reconnectRepeater.start(); this.packetConnectionManager.establishUnderlyingConnection(this.packetConnection); } } public void setHandler(ReliabilityManagerHandler handler) { this.handler = handler; } public ReliabilityManagerHandler getHandler() { return this.handler; } @Override public Set<PacketType> getHandledPacketTypes() { return new HashSet<>(Arrays.asList(PacketType.CLOSE_REQUEST, PacketType.CLOSE_ANNOUNCE, PacketType.CLOSE_ACKNOWLEDGE)); } @Override public void handlePacket(ByteBuffer packet, PacketType type) { switch (type) { case CLOSE_REQUEST: handleCloseRequest(); break; case CLOSE_ANNOUNCE: handleCloseAnnounce(); break; case CLOSE_ACKNOWLEDGE: handleCloseAcknowledge(CloseAcknowledge.deserialize(packet)); break; default: throw new IllegalArgumentException("Invalid type: "+type); } } @Override public void onUnderlyingConnectionClose(PacketConnection connection) { this.reconnectAttempts = 0; if (this.isExpectingConnectionToClose) { this.reconnectRepeater.stop(); this.packetConnectionManager.notifyConnectionClose(packetConnection); this.handler.onExpectedConnectionClose(); } else if (this.isExpectedToReconnect) { this.reconnectRepeater.start(); } } @Override public void onWillSwapUnderlyingConnection(PacketConnection connection) {} @Override public void onUnderlyingConnectionConnected(PacketConnection connection) { this.reconnectRepeater.stop(); this.reconnectAttempts = 0; this.handler.onConnectionConnected(); } @Override public void onNoPacketsLeft(PacketConnection connection) {} }