blob: 4535299573def5ec4b59f4b3f3d34a84943ff932 [file] [log] [blame]
/*
* Copyright (c) 2010, 2018 Oracle and/or its affiliates. All rights reserved.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0, which is available at
* http://www.eclipse.org/legal/epl-2.0.
*
* This Source Code may also be made available under the following Secondary
* Licenses when the conditions for such availability set forth in the
* Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
* version 2 with the GNU Classpath Exception, which is available at
* https://www.gnu.org/software/classpath/license.html.
*
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
*/
package org.glassfish.gms.bootstrap;
import com.sun.enterprise.config.serverbeans.Cluster;
import com.sun.enterprise.config.serverbeans.Server;
import com.sun.enterprise.config.serverbeans.ServerRef;
import com.sun.enterprise.ee.cms.core.*;
import com.sun.enterprise.util.i18n.StringManager;
import com.sun.logging.LogDomains;
import org.glassfish.logging.annotation.LogMessageInfo;
import org.glassfish.logging.annotation.LogMessagesResourceBundle;
import org.glassfish.logging.annotation.LoggerInfo;
import org.jvnet.hk2.config.ConfigListener;
import org.jvnet.hk2.config.UnprocessedChangeEvents;
import java.beans.PropertyChangeEvent;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.logging.Logger;
import org.glassfish.api.logging.LogLevel;
/**
* Used to hold cluster history. This information is backed by
* a ConcurrentMap, so iterating over the instances is
* "weakly consistent" as the state could change at any time
* (especially during cluster startup).
*/
public final class HealthHistory implements ConfigListener {
//private final static Logger logger = LogDomains.getLogger(
// HealthHistory.class, LogDomains.CORE_LOGGER);
private static final StringManager strings =
StringManager.getManager(HealthHistory.class);
@LoggerInfo(subsystem = "CLSTR", description="Group Management Service Logger", publish=true)
private static final String GMSBS_LOGGER_NAME = "jakarta.enterprise.cluster.gms.bootstrap";
@LogMessagesResourceBundle
private static final String LOG_MESSAGES_RB = "org.glassfish.cluster.gms.bootstrap.LogMessages";
static final Logger GMSBS_LOGGER = Logger.getLogger(GMSBS_LOGGER_NAME, LOG_MESSAGES_RB);
@LogMessageInfo(message = "Adding instance {0} to health history table.", level="INFO")
private static final String GMS_ADDING_INSTANCE="NCLS-CLSTR-20001";
@LogMessageInfo(message = "Instance {0} was not in map when deleted from health history table.",
level="WARNING",
cause="More than one call may have been made to remove this instance" +
" from the cluster. This has no other effect on the health history information.",
action="No action is necessary.")
private static final String GMS_INSTANCE_NOT_PRESENT="NCLS-CLSTR-20002";
// deleting_instance=GMSBS2003: Deleting instance {0} from health history table.
@LogMessageInfo(message = "Deleting instance {0} from health history table.", level="INFO")
private static final String GMS_DELETE_INSTANCE="NCLS-CLSTR-20003";
// duplicate_instance=GMSBS2004: Duplicate instance {0} ignored in health history.
@LogMessageInfo(message = "Duplicate instance {0} ignored in health history.",
level="WARNING",
cause="There may be more than one instance in the cluster with the same name.",
action="Check that instance names are unique within the cluster.")
private static final String GMS_DUPLICATE_INSTANCE="NCLS-CLSTR-20004";
// key_already.present=GMSBS2005: State already known for instance {0}. Not adding to health history table.
@LogMessageInfo(message = "State already known for instance {0}. Not adding to health history table.", level="INFO")
private static final String GMS_INSTANCE_ALREADY_PRESENT="NCLS-CLSTR-20005";
// unknown_instance=GMSBS2006: New state {0} added for unknown instance {1}
@LogMessageInfo(message = "New state {0} added for unknown instance {1}", level="INFO")
private static final String GMS_INSTANCE_UNKNOWN_STATE="NCLS-CLSTR-20006";
// NOT_RUNNING means there is no time information associated
public static enum STATE {
NOT_RUNNING (strings.getString("state.not_running")),
RUNNING (strings.getString("state.running")),
REJOINED (strings.getString("state.rejoined")),
FAILURE (strings.getString("state.failure")),
SHUTDOWN (strings.getString("state.shutdown"));
private final String stringVal;
STATE(String stringVal) {
this.stringVal = stringVal;
}
@Override
public String toString() {
return stringVal;
}
};
/**
* Used when no time information is known, for instance at
* cluster startup before an instance has started.
*/
public static final long NOTIME = -1l;
private final ConcurrentMap<String, InstanceHealth> healthMap;
/*
* Creates a health history that knows about the expected
* list of instances. This is called from the GMS adapter
* during initialization, before
*/
public HealthHistory(Cluster cluster) {
healthMap = new ConcurrentHashMap<String, InstanceHealth>(
cluster.getInstances().size());
for (Server server : cluster.getInstances()) {
if (server.isDas()) {
continue;
}
if (GMSBS_LOGGER.isLoggable(LogLevel.FINE)) {
GMSBS_LOGGER.log(LogLevel.FINE, String.format(
"instance name in HealthHistory constructor %s",
server.getName()));
}
if (healthMap.putIfAbsent(server.getName(),
new InstanceHealth(STATE.NOT_RUNNING, NOTIME)) != null) {
GMSBS_LOGGER.log(LogLevel.WARNING, GMS_DUPLICATE_INSTANCE, server.getName());
}
}
}
/**
* Returns the state/time of a specific instance.
*/
public InstanceHealth getHealthByInstance(String name) {
return healthMap.get(name);
}
/**
* The returned list may be modified without affecting
* the information in the HealthHistory object.
*/
public List<String> getInstancesByState(STATE targetState) {
List<String> retVal = new ArrayList<String>(healthMap.size());
for (String name : healthMap.keySet()) {
if (healthMap.get(name).state == targetState) {
retVal.add(name);
}
}
return retVal;
}
/**
* Returns a copy of the instance names.
*/
public Set<String> getInstances() {
return Collections.unmodifiableSet(healthMap.keySet());
}
/**
* Called by GMS subsystem to update the health of an instance.
*
* TODO: add try/catch around everything for safety
*/
public void updateHealth(Signal signal) {
if (GMSBS_LOGGER.isLoggable(LogLevel.FINE)) {
GMSBS_LOGGER.log(LogLevel.FINE, "signal: " + signal.toString());
}
String name = signal.getMemberToken();
long time = signal.getStartTime();
STATE state = null;
// a little if-elsie....
if (signal instanceof JoinNotificationSignal) {
/*
* Means an instance is running. We will usually get a
* JoinedAndReadyNotificationSignal after this in a usual
* startup. If not, it means the DAS is restarting and
* the cluster is already up. In that case, we need
* the original startup time, not the time of the signal.
*/
state = STATE.RUNNING;
} else if (signal instanceof JoinedAndReadyNotificationSignal) {
/*
* During a normal startup, this will occur after the
* JoinNotificationSignal. If it's not a Rejoin, we
* don't need to process the data since it's already
* happened during the Join event. But it doesn't hurt.
*/
JoinedAndReadyNotificationSignal jar =
(JoinedAndReadyNotificationSignal) signal;
RejoinSubevent sub = jar.getRejoinSubevent();
if (sub == null) {
if (GMSBS_LOGGER.isLoggable(LogLevel.FINE)) {
GMSBS_LOGGER.log(LogLevel.FINE, "it's a joined and ready");
}
state = STATE.RUNNING;
} else {
if (GMSBS_LOGGER.isLoggable(LogLevel.FINE)) {
GMSBS_LOGGER.log(LogLevel.FINE, "it's a rejoin");
}
state = STATE.REJOINED;
time = sub.getGroupJoinTime();
}
} else if (signal instanceof FailureNotificationSignal) {
state = STATE.FAILURE;
time = System.currentTimeMillis();
} else if (signal instanceof PlannedShutdownSignal) {
state = STATE.SHUTDOWN;
time = System.currentTimeMillis();
} else {
if (GMSBS_LOGGER.isLoggable(LogLevel.FINE)) {
GMSBS_LOGGER.log(LogLevel.FINE, String.format(
"Signal %s not handled in updateHealth",
signal.toString()));
}
return;
}
InstanceHealth ih = new InstanceHealth(state, time);
if (GMSBS_LOGGER.isLoggable(LogLevel.FINE)) {
GMSBS_LOGGER.log(LogLevel.FINE, String.format(
"updating health with %s : %s for signal %s",
name, ih.toString(), signal.toString()));
}
if (healthMap.put(name, ih) == null) {
GMSBS_LOGGER.log(LogLevel.INFO, GMS_INSTANCE_UNKNOWN_STATE,
new Object [] {state, name});
}
}
@Override
public UnprocessedChangeEvents changed(PropertyChangeEvent[] events) {
Object oldVal;
Object newVal;
for (PropertyChangeEvent event : events) {
oldVal = event.getOldValue();
newVal = event.getNewValue();
if (oldVal instanceof ServerRef && newVal == null) {
ServerRef instance = (ServerRef) oldVal;
deleteInstance(instance.getRef());
} else if (newVal instanceof ServerRef && oldVal == null) {
ServerRef instance = (ServerRef) newVal;
addInstance(instance.getRef());
}
}
return null;
}
private void deleteInstance(String name) {
GMSBS_LOGGER.log(LogLevel.INFO, GMS_DELETE_INSTANCE, name);
InstanceHealth oldHealth = healthMap.remove(name);
if (oldHealth == null) {
GMSBS_LOGGER.log(LogLevel.WARNING, GMS_INSTANCE_NOT_PRESENT, name);
}
}
/*
* We only want to add the instance if it's not already
* in the map. It could exist already if some trick of time
* caused a GMS message to be received from the instance
* before the config changes were processed. We could use
* current time in the instance health object, but we should
* be consistent with startup behavior.
*/
private void addInstance(String name) {
GMSBS_LOGGER.log(LogLevel.INFO, GMS_ADDING_INSTANCE, name);
InstanceHealth oldHealth = healthMap.putIfAbsent(name,
new InstanceHealth(STATE.NOT_RUNNING, NOTIME));
if (oldHealth != null) {
GMSBS_LOGGER.log(LogLevel.INFO, GMS_INSTANCE_ALREADY_PRESENT, name);
}
}
/*
* Information in an InstanceHealth object is immutable. For
* convenience, the fields are public for direct access.
*/
public static final class InstanceHealth {
/**
* The last-known state of the instance.
*/
public final STATE state;
/**
* The time, if known, corresponding to the last change in state.
*/
public final long time;
InstanceHealth(STATE state, long time) {
this.state = state;
this.time = time;
}
@Override
public String toString() {
return String.format("InstanceHealth: state '%s' time '%s'",
state, new Date(time).toString());
}
}
}