/* * Copyright 2007-2010 Sun Microsystems, Inc. * * This file is part of Project Darkstar Server. * * Project Darkstar Server is free software: you can redistribute it * and/or modify it under the terms of the GNU General Public License * version 2 as published by the Free Software Foundation and * distributed hereunder to you. * * Project Darkstar Server is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. * * -- */ package com.sun.sgs.impl.service.watchdog; import com.sun.sgs.app.ManagedObject; import com.sun.sgs.app.NameNotBoundException; import com.sun.sgs.app.ObjectNotFoundException; import com.sun.sgs.app.TransactionException; import com.sun.sgs.impl.util.BoundNamesUtil; import com.sun.sgs.management.NodeInfo; import com.sun.sgs.service.DataService; import com.sun.sgs.service.Node; import java.io.Serializable; import java.util.ArrayList; import java.util.Collection; import java.util.HashSet; import java.util.Iterator; import java.util.Set; /** * Implements the {@link Node} interface. The state for a given * {@code nodeId} is bound in the datastore with the following service * bound name: * * <p><code>com.sun.sgs.impl.service.watchdog.NodeImpl.<i>nodeId</i></code> */ class NodeImpl implements Node, ManagedObject, Serializable, Comparable<NodeImpl> { /** The serialVersionUID of this class. */ private static final long serialVersionUID = 1L; /** The ID for an unknown node. */ public static final long INVALID_ID = -1L; /** The name of this class. */ private static final String PKG_NAME = "com.sun.sgs.impl.service.watchdog"; /** The prefix for NodeImpl state. */ private static final String NODE_PREFIX = PKG_NAME + ".node"; /** The node id. */ private final long id; /** The host name, or {@code null}. */ private final String host; /** The node's health. */ private Health health; /** The port JMX can listen on, or {@code -1}. */ private final int jmxPort; /** The watchdog client, or {@code null}. */ private final WatchdogClient client; /** The ID of the backup for this node. */ private long backupId = INVALID_ID; /** The set of primaries for which this node is a backup. */ private final Set<Long> primaryIds = new HashSet<Long>(); /** * The expiration time for this node. A value of {@code 0} means * that either the value has not been initialized or the value is * not meaningful because the node has failed. */ private transient long expiration; /** * Constructs an instance of this class with the given {@code * nodeId}, {@code hostName}, and {@code client}. * This instance's alive status is set to {@code true}. The expiration * time for this instance should be set as soon as it is known. * * @param nodeId a node ID * @param hostName a host name * @param jmxPort the port JMX is listening on for the node, * or {@code -1} * @param client a watchdog client */ NodeImpl(long nodeId, String hostName, int jmxPort, WatchdogClient client) { this (nodeId, hostName, jmxPort, client, Health.GREEN, INVALID_ID); } /** * Constructs an instance of this class with the given {@code * nodeId}, {@code hostName}, and {@code health}. This * instance's watchdog client is set to {@code null} and its * backup is unassigned (backup ID is -1). * * @param nodeId a node ID * @param hostName a host name, or {@code null} * @param health the node's health */ NodeImpl(long nodeId, String hostName, Health health) { this(nodeId, hostName, -1, null, health, INVALID_ID); } /** * Constructs an instance of this class with the given {@code * nodeId}, {@code hostName}, {@code isAlive} status, and * {@code backupId}. This instance's watchdog client is set to * {@code null}. * * @param nodeId a node ID * @param hostName a host name, or {@code null} * @param health the node's health * @param backupId the ID of the node's backup (-1 if no backup * is assigned) */ NodeImpl(long nodeId, String hostName, Health health, long backupId) { this(nodeId, hostName, -1, null, health, backupId); } /** * Constructs an instance of this class with the given {@code * nodeId}, {@code hostName}, {@code jmxPort}, {@code client}, * {@code health}, and {@code backupId}. * * @param nodeId a node ID * @param hostName a host name, or {@code null} * @param jmxPort the port JMX is listening on, or {@code -1} * @param client a watchdog client * @param health the node's health * @param backupId the ID of the node's backup (-1 if no backup * is assigned) */ private NodeImpl(long nodeId, String hostName, int jmxPort, WatchdogClient client, Health health, long backupId) { this.id = nodeId; this.host = hostName; this.client = client; this.health = health; this.backupId = backupId; this.jmxPort = jmxPort; } /* -- Implement Node -- */ /** {@inheritDoc} */ public long getId() { return id; } /** {@inheritDoc} */ public String getHostName() { return host; } /** {@inheritDoc} */ public boolean isAlive() { return getHealth().isAlive(); } /** {@inheritDoc} */ public synchronized Health getHealth() { return health; } /* -- Implement Comparable -- */ /** {@inheritDoc} */ public int compareTo(NodeImpl o) { long difference = getExpiration() - o.getExpiration(); if (difference == 0) { difference = id - o.id; if (difference == 0) { difference = compareStrings(host, o.host); } } return difference < 0 ? -1 : (difference > 0 ? 1 : 0); } /* -- Implement Object -- */ /** {@inheritDoc} */ public boolean equals(Object obj) { if (obj == null) { return false; } else if (this == obj) { return true; } else if (obj.getClass() == this.getClass()) { NodeImpl node = (NodeImpl) obj; if (id == node.id) { if (compareStrings(host, node.host) != 0) { throw new RuntimeException("two node objects with ID " + id + " have different host names: " + host + " and " + node.host); } return true; } } return false; } /** {@inheritDoc} */ public int hashCode() { return ((int) (id >>> 32)) ^ ((int) id); } /** {@inheritDoc} */ public synchronized String toString() { return getClass().getName() + "[" + id + ",health:" + health.toString() + ",backup:" + (backupId == INVALID_ID ? "(none)" : backupId) + "]@" + host; } /* -- package access methods -- */ /** * Returns the watchdog client, or {@code null}. */ WatchdogClient getWatchdogClient() { return client; } /** * Returns the expiration time. A value of {@code 0} means that * either the value has not been initialized or the value is not * meaningful because the node has failed. If {@link #isAlive} * returns {@code false} the value returned from this method is * not meaningful. */ synchronized long getExpiration() { return expiration; } /** * Sets the expiration time for this node instance. * * @param newExpiration the new expiration value */ synchronized void setExpiration(long newExpiration) { expiration = newExpiration; } /** * Returns {@code true} if the node is expired, and {@code false} * otherwise. * * @return {@code true} if the node is expired, and {@code false} * otherwise */ synchronized boolean isExpired() { return expiration <= System.currentTimeMillis(); } /** * Sets the health of this node instance to {@code RED}, * sets this node's backup to the specified {@code backup}, * empties the set of primaries for which this node is recovering, * and updates the node's state in the specified {@code * dataService}. Subsequent calls to {@link #isAlive isAlive} * will return {@code false}. * * @param dataService a data service * @param backup a chosen backup * @throws ObjectNotFoundException if this node has been removed * @throws TransactionException if there is a problem with the * current transaction */ synchronized void setFailed(DataService dataService, NodeImpl backup) { NodeImpl nodeImpl = getForUpdate(dataService); this.health = Health.RED; nodeImpl.health = Health.RED; this.backupId = (backup != null) ? backup.getId() : INVALID_ID; nodeImpl.backupId = this.backupId; this.primaryIds.clear(); nodeImpl.primaryIds.clear(); } /** * Sets the health of this node instance to a non-RED value. If the node * health is to be set to RED use {@code setFailed}. * * @param dataService a data service * @param newHealth the new health of this node * @throws ObjectNotFoundException if this node has been removed * @throws TransactionException if there is a problem with the * current transaction */ synchronized void setHealth(DataService dataService, Health newHealth) { if (!newHealth.isAlive()) { throw new AssertionError("Call to setHealth with RED health"); } NodeImpl nodeImpl = getForUpdate(dataService); this.health = newHealth; nodeImpl.health = newHealth; } /** * Adds the specified {@code primaryId} to the list of primaries * for which this node is a backup, and updates the node's state * in the specified {@code dataService}. * * @param dataService a data service * @param primaryId the ID of a primary for which this node is a * backup * @throws ObjectNotFoundException if this node has been removed * @throws TransactionException if there is a problem with the * current transaction */ synchronized void addPrimary(DataService dataService, long primaryId) { NodeImpl nodeImpl = getForUpdate(dataService); primaryIds.add(primaryId); nodeImpl.primaryIds.add(primaryId); } /** Returns the set of primary nodes for which this node is a backup. */ synchronized Set<Long> getPrimaries() { return primaryIds; } /** Returns {@code true} if this node has a backup. */ synchronized boolean hasBackup() { return backupId != INVALID_ID; } /** * Returns the backup for this node, or {@value INVALID_ID} if there * is no backup. */ synchronized long getBackupId() { return backupId; } /** * Stores this instance in the specified {@code dataService}. * This method should only be called within a transaction. * * @param dataService a data service * @throws TransactionException if there is a problem with the * current transaction */ synchronized void putNode(DataService dataService) { dataService.setServiceBinding(getNodeKey(id), this); } /** * Fetches this node's state from the specified {@code * dataService}, marked for update. * * @param dataService a data service * @throws ObjectNotFoundException if this node has been removed * @throws TransactionException if there is a problem with the * current transaction */ private NodeImpl getForUpdate(DataService dataService) { NodeImpl nodeImpl = getNodeForUpdate(dataService, id); if (nodeImpl == null) { throw new ObjectNotFoundException("node is removed"); } return nodeImpl; } /** * Returns the port used for remote JMX monitoring, or {@code -1} * if only local monitoring is allowed. * * @return the port used for remote JMX monitoring of this node */ private int getJmxPort() { return jmxPort; } /** * Returns the management information for this node. * * @return the management information for this node */ NodeInfo getNodeInfo() { return new NodeInfo(getHostName(), getId(), getHealth(), getBackupId(), getJmxPort()); } /** * Removes the node with the specified {@code nodeId} and its * binding from the specified {@code dataService}. If the binding * has already been removed from the {@code dataService} this * method takes no action. This method should only be called * within a transaction. * * @param dataService a data service * @param nodeId a node ID * @throws TransactionException if there is a problem with the * current transaction */ static void removeNode(DataService dataService, long nodeId) { String key = getNodeKey(nodeId); NodeImpl node; try { node = (NodeImpl) dataService.getServiceBinding(key); dataService.removeServiceBinding(key); dataService.removeObject(node); } catch (NameNotBoundException e) { } } /** * Returns the {@code Node} instance for the given {@code nodeId}, * retrieved from the specified {@code dataService}, or {@code * null} if the node isn't bound in the data service . This * method should only be called within a transaction. * * @param dataService a data service * @param nodeId a node ID * @return the node for the given {@code nodeId}, or {@code null} * @throws TransactionException if there is a problem with the * current transaction */ static NodeImpl getNode(DataService dataService, long nodeId) { String key = getNodeKey(nodeId); NodeImpl node = null; try { node = (NodeImpl) dataService.getServiceBinding(key); } catch (NameNotBoundException e) { } return node; } /** * Returns the {@code Node} instance for the given {@code nodeId}, * retrieved from the specified {@code dataService} for update. * This method returns {@code null} if the node isn't bound in the data * service . This method must only be called within a transaction. * * @param dataService a data service * @param nodeId a node ID * @return the node for the given {@code nodeId}, or {@code null} * @throws TransactionException if there is a problem with the * current transaction */ static NodeImpl getNodeForUpdate(DataService dataService, long nodeId) { String key = getNodeKey(nodeId); NodeImpl node = null; try { node = (NodeImpl) dataService.getServiceBinding(key); dataService.markForUpdate(node); } catch (NameNotBoundException e) { } return node; } /** * Marks all nodes currently bound in the specified {@code * dataService} as failed, and returns a collection of those * nodes. This method should only be called within a transaction. * * @param dataService a data service * @return a collection of currently bound nodes, each marked as failed * @throws TransactionException if there is a problem with the * current transaction */ static Collection<NodeImpl> markAllNodesFailed(DataService dataService) { Collection<NodeImpl> nodes = new ArrayList<NodeImpl>(); for (String key : BoundNamesUtil.getServiceBoundNamesIterable( dataService, NODE_PREFIX)) { NodeImpl node = (NodeImpl) dataService.getServiceBinding(key); node.setFailed(dataService, null); nodes.add(node); } return nodes; } /** * Returns an iterator for {@code Node} instances to be retrieved * from the specified {@code dataService}. The returned iterator * does not support the {@code remove} operation. This method * should only be called within a transaction, and the returned * iterator should only be used within that transaction. * * @param dataService a data service * @return an iterator for nodes * @throws TransactionException if there is a problem with the * current transaction */ static Iterator<Node> getNodes(DataService dataService) { return new NodeIterator(dataService); } /* -- private methods and classes -- */ /** * Compares the specified strings and returns -1, 0, or 1 * according to whether the first string is less than, equal to, * or greater than the second string in a lexicographic ordering. * In this ordering, a string with a value of {@code null} is less * than any non-{@code null} string. * * @param s1 a string, or {@code null} * @param s2 a string, or {@code null} * @return -1, 0, or 1 according to whether {@code s1} is less than, * equal to, or greater than {@code s2} */ private static int compareStrings(String s1, String s2) { if (s1 == null) { return (s2 == null) ? 0 : -1; } else if (s2 == null) { return 1; } else { return s1.compareTo(s2); } } /** * Returns the key to access from the data service the {@code * Node} instance with the specified {@code nodeId}. * * @param a node ID * @return a key for accessing the {@code Node} instance */ private static String getNodeKey(long nodeId) { return NODE_PREFIX + "." + nodeId; } /** * An iterator for node state. */ private static class NodeIterator implements Iterator<Node> { /** The data service. */ private final DataService dataService; /** The underlying iterator for service bound names. */ private Iterator<String> iterator; /** * Constructs an instance of this class with the specified * {@code dataService}. */ NodeIterator(DataService dataService) { this.dataService = dataService; this.iterator = BoundNamesUtil.getServiceBoundNamesIterator( dataService, NODE_PREFIX); } /** {@inheritDoc} */ public boolean hasNext() { return iterator.hasNext(); } /** {@inheritDoc} */ public Node next() { String key = iterator.next(); return (NodeImpl) dataService.getServiceBinding(key); } /** {@inheritDoc} */ public void remove() { throw new UnsupportedOperationException("remove is not supported"); } } }