/interpreter/tags/at2-build190607/src/edu/vub/at/actors/net/comm/CommunicationBus.java

http://ambienttalk.googlecode.com/ · Java · 519 lines · 209 code · 47 blank · 263 comment · 24 complexity · 5e7d09bd8b68b3bdaea075837a1a19b8 MD5 · raw file

  1. /**
  2. * AmbientTalk/2 Project
  3. * CommunicationBus.java created on 2-apr-2007 at 19:30:12
  4. * (c) Programming Technology Lab, 2006 - 2007
  5. * Authors: Tom Van Cutsem & Stijn Mostinckx
  6. *
  7. * Permission is hereby granted, free of charge, to any person
  8. * obtaining a copy of this software and associated documentation
  9. * files (the "Software"), to deal in the Software without
  10. * restriction, including without limitation the rights to use,
  11. * copy, modify, merge, publish, distribute, sublicense, and/or
  12. * sell copies of the Software, and to permit persons to whom the
  13. * Software is furnished to do so, subject to the following
  14. * conditions:
  15. *
  16. * The above copyright notice and this permission notice shall be
  17. * included in all copies or substantial portions of the Software.
  18. *
  19. * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
  20. * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
  21. * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
  22. * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
  23. * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
  24. * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
  25. * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
  26. * OTHER DEALINGS IN THE SOFTWARE.
  27. */
  28. package edu.vub.at.actors.net.comm;
  29. import edu.vub.at.actors.natives.ELVirtualMachine;
  30. import edu.vub.at.actors.net.cmd.VMCommand;
  31. import edu.vub.at.util.logging.Logging;
  32. import java.io.BufferedInputStream;
  33. import java.io.BufferedOutputStream;
  34. import java.io.IOException;
  35. import java.io.ObjectInputStream;
  36. import java.io.ObjectOutputStream;
  37. import java.net.Socket;
  38. import java.util.Collection;
  39. import java.util.HashMap;
  40. import java.util.Iterator;
  41. import java.util.Map;
  42. import java.util.Timer;
  43. import java.util.TimerTask;
  44. /**
  45. * For each AmbientTalk virtual machine, there exists one communication bus instance.
  46. * The communication bus can be in two conceptual states. It can be:
  47. * <ul>
  48. * <li>Connected: when connected, the communication bus's {@link CommunicationBus#networkAddress_} field
  49. * is set.
  50. * <li>Disconnected: when disconnected, the {@link CommunicationBus#networkAddress_} field is
  51. * set to null.
  52. * </ul>
  53. *
  54. * A communication bus is always started in a disconnected state. Toggling between both
  55. * states is done using the {@link CommunicationBus#connect()} and
  56. * {@link CommunicationBus#disconnect()} methods.
  57. * <p>
  58. * The communication bus is the central coordinator of the network communication layer.
  59. * It encapsulates four important threads:
  60. * <ul>
  61. * <li>The {@link MulticastServerThread} is responsible for sending 'heartbeat' messages
  62. * in a multicast fashion across the network, to notify other VMs in the network that
  63. * this VM is still 'alive'.
  64. * <li>The {@link MulticastListenerThread} is responsible for listening for the
  65. * 'heartbeat' messages of other VMs, and for adding new connections in the
  66. * connection table of the communication bus.
  67. * <li>The {@link MasterConnectionThread} is responsible for opening a server
  68. * socket that accepts incoming connections from so-called <i>slave</i> VMs.
  69. * <li>A timer thread which runs a task at a fixed rate to checks the connection
  70. * table for timed out VMs.
  71. * </ul>
  72. *
  73. * The most important bookkeeping datastructure of the communication bus is the
  74. * {@link CommunicationBus#addressToConnection_} table, also referred to as the
  75. * <b>connection table</b>. This table stores all of the currently connected
  76. * VMs that the host VM knows about. Whenever a heartbeat is received, a VM's
  77. * connection registration in this table is updated. When a heartbeat is first
  78. * received, the VM is added. When a heartbeat has not been heard for longer
  79. * than the timeout interval, the VM is removed from the connection table.
  80. * <p>
  81. * Schematically, we have the following situation after establishing a connection
  82. * between a master and a slave VM (see the description of {@link MulticastListenerThread}):
  83. *
  84. * <pre>
  85. * Slave Master
  86. * cs = new Socket(masterAddress) ms = socket.accept()
  87. * cs.in <-------------------------- ms.out
  88. * cs.out -------------------------> ms.in
  89. * </pre>
  90. *
  91. * It does not matter which socket is registered in the connection table. Hence,
  92. * once both VMs have set up a connection, the concept of 'master' and 'slave' is
  93. * no longer useful and both VMs become peers.
  94. *
  95. * It is always the output stream of either socket that is used for sending messages to the other VM.
  96. * It is always the input stream of either socket that is used for receiving messages from the other VM.
  97. *
  98. * @author tvcutsem
  99. */
  100. public class CommunicationBus {
  101. /** the AmbientTalk VM that owns this communication bus */
  102. public final ELVirtualMachine host_;
  103. /** the name of the overlay network in which to discover AmbientTalk VMs */
  104. private final String groupName_;
  105. /** if non-null, the communication bus is connected to the network */
  106. private volatile Address networkAddress_;
  107. private MulticastListenerThread mcListener_;
  108. private MulticastServerThread mcServer_;
  109. private MasterConnectionThread masterConnectionThread_;
  110. /**
  111. * Maps the address of a currently connected VM to connection information
  112. * such as the last time it was seen, and the socket connection.
  113. * Also known as the "connection table".
  114. *
  115. * This datastructure is modified by different threads:
  116. * <ul>
  117. * <li>Addition of master VMs by {@link MulticastListenerThread}
  118. * <li>Addition of slave VMs by {@link MasterConnectionThread}
  119. * <li>Removal of disconnected VMs by {@link CommandProcessor}
  120. * <li>Checking for timed out VMs by {@link CommunicationBus#timeoutDetector_}
  121. * <li>Lookup of connections by AmbientTalk event loops for message transmission
  122. * </ul>
  123. * Hence, access to this datastructure must by <b>synchronized</b>!
  124. */
  125. private final HashMap addressToConnection_;
  126. /**
  127. * The timer used for checking 'stale' (i.e. timed out) connections
  128. * in the connection table.
  129. */
  130. private final Timer timeoutDetectorTimer_;
  131. /**
  132. * The timer task used for removing timed out connections
  133. * from the connections table.
  134. */
  135. private TimeoutDetectorTask timeoutDetector_;
  136. /**
  137. * Bookkeeping datastructure to store connection-related information
  138. * of a connected VM. A connection entry is uniquely identified by
  139. * means of the {@link Address} of its VM.
  140. */
  141. private static class Connection {
  142. public final Socket socket_;
  143. /** this value is updated as new heartbeats are received */
  144. public long lastSeenAtTime_;
  145. public final ObjectOutputStream outgoing_;
  146. public final ObjectInputStream incoming_;
  147. public Connection(Socket s, long lastSeenAt) throws IOException {
  148. socket_ = s;
  149. lastSeenAtTime_ = lastSeenAt;
  150. // NOTE: apparently it is highly important that the ObjectOutputStream on the socket.getOutputStream() is
  151. // created BEFORE trying to create an ObjectInputStream on the socket.getInputStream()
  152. // switching the below two statements causes the master and the slave to deadlock!
  153. outgoing_ = new ObjectOutputStream(new BufferedOutputStream(s.getOutputStream()));
  154. // The buffered output stream must be EXPLICITLY flushed, otherwise the buffered input stream
  155. // that is created below (but by the other VM) will block indefinitely
  156. outgoing_.flush();
  157. incoming_ = new ObjectInputStream(new BufferedInputStream(s.getInputStream()));
  158. }
  159. /**
  160. * Closes the underlying socket. This will eventually trigger the command processor
  161. * tied to this connection, which will cause this connection to be removed
  162. * from the table.
  163. */
  164. public void close() {
  165. try {
  166. socket_.close();
  167. } catch (IOException ioe) { }
  168. }
  169. }
  170. /**
  171. * The timer task used for removing timed out connections
  172. * from the connection table.
  173. */
  174. private class TimeoutDetectorTask extends TimerTask {
  175. /**
  176. * the maximum amount of time that a remote VM gets to send a new
  177. * heartbeat before it is considered as being 'offline'
  178. */
  179. private static final int MAX_RESPONSE_DELAY = 10000; // in milliseconds
  180. /**
  181. * The rate at which to schedule this timer task
  182. */
  183. public static final int DETECTION_RATE = 4000; // in milliseconds
  184. public void run() {
  185. closeConnectionOfMembersNotSeenIn(MAX_RESPONSE_DELAY);
  186. }
  187. }
  188. public CommunicationBus(ELVirtualMachine host, String ambientTalkNetworkName) {
  189. host_ = host;
  190. groupName_ = ambientTalkNetworkName;
  191. addressToConnection_ = new HashMap();
  192. timeoutDetectorTimer_ = new Timer(true); // create a daemon timer
  193. }
  194. /**
  195. * Tries to connect the communication bus to the underlying network.
  196. * @throws IOException if no server socket could be created to listen
  197. * for incoming connections. If this exception is raised, it is
  198. * guaranteed that the communication bus is left disconnected (i.e.
  199. * it is not partially connected)
  200. */
  201. public Address connect() throws NetworkException {
  202. if (networkAddress_ != null) {
  203. return networkAddress_; // if the bus is already connected, there is no need to connect it again
  204. }
  205. masterConnectionThread_ = new MasterConnectionThread(this);
  206. try {
  207. networkAddress_ = masterConnectionThread_.startServing(groupName_);
  208. } catch (IOException e) {
  209. masterConnectionThread_ = null;
  210. throw new NetworkException("Could not connect to network:", e);
  211. }
  212. mcListener_ = new MulticastListenerThread(this, networkAddress_);
  213. mcServer_ = new MulticastServerThread(networkAddress_);
  214. mcListener_.start();
  215. mcServer_.start();
  216. // start detecting timed out VMs
  217. timeoutDetector_ = new TimeoutDetectorTask();
  218. timeoutDetectorTimer_.scheduleAtFixedRate(timeoutDetector_, 0, TimeoutDetectorTask.DETECTION_RATE);
  219. return networkAddress_;
  220. }
  221. /**
  222. * Called by the VM when it has disconnected from the underlying channel.
  223. * It gracefully shuts down all network threads, sets the network address
  224. * to null and removes all current connections from the connection table.
  225. */
  226. public void disconnect() {
  227. if (networkAddress_ == null) {
  228. return; // if the bus is already disconnected, there is no need to take it offline again
  229. }
  230. // once the bus is disconnected, the network address is set to null.
  231. // this ensures that no further incoming connections are allowed to be
  232. // registered in the addConnection method!
  233. networkAddress_ = null;
  234. masterConnectionThread_.stopServing();
  235. mcListener_.stopListening();
  236. mcServer_.stopBroadcasting();
  237. masterConnectionThread_ = null;
  238. mcListener_ = null;
  239. mcServer_ = null;
  240. // stop detecting timed out VMs
  241. timeoutDetector_.cancel();
  242. timeoutDetector_ = null;
  243. closeConnectionOfAllMembers();
  244. }
  245. /**
  246. * Updates the time that the given virtual machine was last seen online.
  247. * This method is invoked frequently by the {@link MulticastListenerThread}.
  248. *
  249. * @param member the address of the detected virtual machine
  250. * @return true if the VM was properly registered, false if the VM is not yet registered
  251. * (i.e. it is not yet considered as 'online')
  252. */
  253. protected boolean updateTimeLastSeen(Address member) {
  254. synchronized (addressToConnection_) {
  255. Connection conn = (Connection) addressToConnection_.get(member);
  256. if (conn != null) {
  257. conn.lastSeenAtTime_ = System.currentTimeMillis();
  258. return true;
  259. } else {
  260. return false;
  261. }
  262. }
  263. }
  264. /**
  265. * Registers a new virtual machine connection for the given address.
  266. * If this VM was a slave in the discovery process, this method is
  267. * invoked by the {@link MulticastListenerThread}. If this VM was
  268. * a master in the discovery process, this method is invoked by the
  269. * {@link MasterConnectionThread}.
  270. *
  271. * The socket's output stream will be stored in the connection table and is used
  272. * for transmitting VM Commands to this VM. The socket's input stream will be
  273. * coupled to a dedicated {@link CommandProcessor} which is responsible for
  274. * processing incoming VM command objects.
  275. *
  276. * Calling this method implicitly also triggers a memberJoined event on this VM
  277. *
  278. * @throws IOException if no ObjectOutputStream can be created for the socket's output stream,
  279. * or if no ObjectInputStream can be created for the socket's input stream.
  280. * If this exception is raised, it is guaranteed the member is not registered in the connection table.
  281. */
  282. protected void addConnection(Address newMember, Socket conn) throws IOException {
  283. if (networkAddress_ == null) {
  284. Logging.Network_LOG.debug("ignored connection to " + newMember + ": bus disconnected");
  285. return; // the bus has been disconnected, do not accept any new connections
  286. }
  287. // create a new connection object that can be used to send command objects to this member
  288. Connection registeredConnection = new Connection(conn, System.currentTimeMillis());
  289. // spawn a new command processor dedicated for handling the command objects received from this member
  290. CommandProcessor processor = new CommandProcessor(newMember, conn, registeredConnection.incoming_, this);
  291. synchronized (addressToConnection_) {
  292. // first check whether a connection for this member already exists
  293. // (may happen when an old connection for this member has not yet been deleted)
  294. // This case IS possible when a master receives a new connection from a slave,
  295. // but that master has not yet detected the disconnection of the slave's old connection
  296. Connection oldConnection = (Connection) addressToConnection_.get(newMember);
  297. if (oldConnection != null) {
  298. // explicitly close the connection we're about to override
  299. // Note: in this case, we should *not* signal to the VM that a new member joined,
  300. // because in the corresponding removeConnection, we will also not signal a member left
  301. // (i.e. the connection was restored quickly enough such that we can abstract over the disconnection)
  302. oldConnection.close();
  303. } else {
  304. // notify the VM of a new connection
  305. host_.event_memberJoined(newMember);
  306. }
  307. // register the new connection in the table (this may override an old connection)
  308. addressToConnection_.put(newMember, registeredConnection);
  309. }
  310. // only start the processor if the member is properly registered
  311. processor.start();
  312. Logging.Network_LOG.debug("successfully registered connection to " + newMember);
  313. }
  314. /**
  315. * It is the responsibility of the {@link CommandProcessor} tied to the given VM
  316. * to invoke this method when its connection has failed.
  317. *
  318. * This is the <b>only</b> method responsible for removing entries from the
  319. * connection table.
  320. *
  321. * Calling this method implicitly also triggers a memberLeft event on this VM
  322. */
  323. protected void removeConnection(Address oldMember, Socket connSocket) {
  324. synchronized (addressToConnection_) {
  325. Connection conn = (Connection) addressToConnection_.get(oldMember);
  326. if (conn != null) {
  327. // ONLY delete the connection if that connection was registered for the given
  328. // socket. It might be that the connection in the connection table is already
  329. // a NEWER connection that has OVERWRITTEN the old one.
  330. if (conn.socket_ == connSocket) {
  331. conn.close();
  332. addressToConnection_.remove(oldMember);
  333. host_.event_memberLeft(oldMember); // notify VM that the member has left
  334. }
  335. // Note: if the sockets do not match, then the connection to be
  336. // removed by this call has already been replaced by a more recent connection to the same address
  337. // See the code for addConnection for more details
  338. // Because the connection to be removed was already replaced, it is not necessary
  339. // to notify the VM that a member has left
  340. }
  341. }
  342. }
  343. /**
  344. * The {@link MulticastListenerThread} invokes this method regularly to
  345. * remove connections to VMs which have not responded for longer than the
  346. * given timeout period.
  347. *
  348. * Removal is done implicitly by closing timed out connections. The
  349. * {@link CommandProcessor} tied to each connection is responsible for
  350. * actually removing it.
  351. *
  352. * Calling this method might also implicitly trigger one or more memberLeft
  353. * events on this VM.
  354. */
  355. protected void closeConnectionOfMembersNotSeenIn(long period) {
  356. synchronized (addressToConnection_) {
  357. long now = System.currentTimeMillis();
  358. Collection connections = addressToConnection_.values();
  359. for (Iterator iter = connections.iterator(); iter.hasNext();) {
  360. Connection c = (Connection) iter.next();
  361. if (now - c.lastSeenAtTime_ > period) {
  362. c.close(); // the entry will be deleted by the Command Processor
  363. Logging.Network_LOG.debug("Detected timed out VM");
  364. }
  365. }
  366. }
  367. }
  368. /**
  369. * When the communication bus is explicitly disconnected from the
  370. * network, the VM should be notified that all connected VMs
  371. * have disconnected.
  372. *
  373. * Removal is done implicitly by closing all connections. The
  374. * {@link CommandProcessor} tied to each connection is responsible for
  375. * actually removing them.
  376. */
  377. private void closeConnectionOfAllMembers() {
  378. synchronized (addressToConnection_) {
  379. Collection connections = addressToConnection_.values();
  380. for (Iterator iter = connections.iterator(); iter.hasNext();) {
  381. Connection c = (Connection) iter.next();
  382. c.close(); // ensures the command processor will remove the entry eventually
  383. }
  384. }
  385. }
  386. /**
  387. * Sends a VM Command object asynchronously to the recipient VM.
  388. * There are no delivery guarantees for this message.
  389. * If the recipient is offline, or the message times out, it is simply discarded
  390. */
  391. public void sendAsyncUnicast(VMCommand msg, Address recipientVM) {
  392. Logging.Network_LOG.info("sending async unicast cmd " + msg + " to " + recipientVM);
  393. Connection conn;
  394. synchronized (addressToConnection_) {
  395. conn = (Connection) addressToConnection_.get(recipientVM);
  396. }
  397. if (conn != null) {
  398. sendOneAsyncMessage(conn, msg, recipientVM);
  399. }
  400. }
  401. /**
  402. * Sends a VM Command object asynchronously to all connected VMs.
  403. * There are no delivery guarantees for this message, nor is it guaranteed
  404. * that all currently connected VMs will receive the message
  405. */
  406. public void sendAsyncMulticast(VMCommand msg) {
  407. Logging.Network_LOG.info("sending async multicast cmd: " + msg);
  408. // first clone the connection table such that we do not need to acquire the
  409. // lock for the entire duration of the multicast
  410. HashMap clonedConnections;
  411. synchronized (addressToConnection_) {
  412. clonedConnections = (HashMap) addressToConnection_.clone();
  413. }
  414. for (Iterator iter = clonedConnections.entrySet().iterator(); iter.hasNext();) {
  415. Map.Entry entry = (Map.Entry) iter.next();
  416. Address recipient = (Address) entry.getKey();
  417. Connection conn = (Connection) entry.getValue();
  418. sendOneAsyncMessage(conn, msg, recipient);
  419. }
  420. }
  421. /**
  422. * Sends the given {@link VMCommand} to the given member. With 'synchronous' transmission,
  423. * it is meant that if this method returns gracefully, the caller can rest assured that
  424. * the remote VM has correctly received the command object. It does not given any guarantees
  425. * that the command object will have been executed remotely.
  426. *
  427. * @throws NetworkException if either the given address is no longer connected, or
  428. * an I/O error occurs during the transmission of the command object. If this exception is
  429. * raised, the caller does not know whether the message was correctly received or not.
  430. */
  431. public void sendSynchronousUnicast(VMCommand msg, Address recipientVM) throws NetworkException {
  432. Logging.Network_LOG.info("sending sync unicast cmd: " + msg + " to " + recipientVM);
  433. Connection conn;
  434. synchronized (addressToConnection_) {
  435. conn = (Connection) addressToConnection_.get(recipientVM);
  436. }
  437. if (conn == null) {
  438. throw new NetworkException("Recipient " + recipientVM + " is offline");
  439. }
  440. try {
  441. conn.outgoing_.writeObject(msg);
  442. conn.outgoing_.flush();
  443. } catch (IOException e) {
  444. // it is the sender's responsibility to close the connection's socket if something goes wrong
  445. // the corresponding CommandProcessor registered on this socket will remove the member from
  446. // the connection table
  447. conn.close();
  448. Logging.Network_LOG.error("Error while trying to send command " + msg + " to " + recipientVM, e);
  449. throw new NetworkException("Error while trying to transmit message " + msg, e);
  450. }
  451. }
  452. /**
  453. * Note that an I/O exception during the transmission of a command object is <b>fatal</b>
  454. * for the connection. The connection is immediately terminated by closing the socket.
  455. * It is the responsibility of the {@link CommandProcessor} registered on the socket
  456. * connection's input stream to remove the recipient VM from the connection table.
  457. */
  458. private void sendOneAsyncMessage(Connection conn, VMCommand msg, Address recipientVM) {
  459. try {
  460. conn.outgoing_.writeObject(msg);
  461. conn.outgoing_.flush();
  462. } catch (IOException e) {
  463. // it is the sender's responsibility to close the connection's socket if something goes wrong
  464. // the CommandProcessor will remove the member from the connection table
  465. conn.close();
  466. Logging.Network_LOG.error("Could not send command " + msg + " to " + recipientVM + ", dropping.", e);
  467. }
  468. }
  469. public String toString() {
  470. return "communication bus";
  471. }
  472. }