/* (c) 2014 Open Source Geospatial Foundation - all rights reserved
* (c) 2014 Boundless
* This code is licensed under the GPL 2.0 license, available at the root
* application directory.
*/
package org.geoserver.cluster.hazelcast;
import static java.lang.String.format;
import static org.geoserver.cluster.hazelcast.HazelcastUtil.localAddress;
import java.util.List;
import java.util.Queue;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.geoserver.catalog.CatalogException;
import org.geoserver.catalog.Info;
import org.geoserver.catalog.ResourceInfo;
import org.geoserver.catalog.StoreInfo;
import org.geoserver.catalog.WorkspaceInfo;
import org.geoserver.catalog.event.CatalogAddEvent;
import org.geoserver.catalog.event.CatalogEvent;
import org.geoserver.catalog.event.CatalogPostModifyEvent;
import org.geoserver.catalog.event.CatalogRemoveEvent;
import org.geoserver.cluster.ConfigChangeEvent;
import org.geoserver.cluster.ConfigChangeEvent.Type;
import org.geoserver.cluster.Event;
import org.geoserver.cluster.GeoServerSynchronizer;
import org.geoserver.config.GeoServer;
import org.geoserver.config.GeoServerInfo;
import org.geoserver.config.ServiceInfo;
import org.geoserver.config.SettingsInfo;
import org.geoserver.ows.util.OwsUtils;
import org.geotools.util.logging.Logging;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import com.hazelcast.core.ITopic;
import com.hazelcast.core.Message;
import com.hazelcast.core.MessageListener;
import com.yammer.metrics.Metrics;
/**
* Base hazelcast based synchronizer that does event collapsing.
* <p>
* This synchronizer maintains a thread safe queue that is populated with events as they occur. Upon
* receiving of an event a new runnable is scheduled and run after a short delay (default 5 sec).
* The runnable calls the {@link #processEvent(Queue)} method to be implemented by subclasses.
* </p>
* <p>
* This synchronizer events messages received from the same source.
* </p>
*
* @author Justin Deoliveira, OpenGeo
*
*/
public abstract class HzSynchronizer extends GeoServerSynchronizer implements
MessageListener<Event> {
protected static Logger LOGGER = Logging.getLogger("org.geoserver.cluster.hazelcast");
protected final HzCluster cluster;
protected final ITopic<Event> topic;
/** event processor */
private final ScheduledExecutorService executor;
/** geoserver configuration */
protected final GeoServer gs;
private volatile boolean started;
ScheduledExecutorService getNewExecutor() {
return Executors.newSingleThreadScheduledExecutor(new ThreadFactoryBuilder().setNameFormat(
"HzSynchronizer-%d").build());
}
public HzSynchronizer(HzCluster cluster, GeoServer gs) {
this.cluster = cluster;
this.gs = gs;
topic = cluster.getHz().getTopic("geoserver.config");
topic.addMessageListener(this);
executor = getNewExecutor();
gs.addListener(this);
gs.getCatalog().addListener(this);
}
@Override
public void onMessage(Message<Event> message) {
Event event = message.getMessageObject();
if (!isStarted()) {
// wait for service to be fully started before processing events.
if (LOGGER.isLoggable(Level.FINER)) {
LOGGER.finer(format("Ignoring message: %s. Service is not started.", event));
}
return;
}
Metrics.newCounter(getClass(), "recieved").inc();
if (localAddress(cluster.getHz()).equals(event.getSource())) {
if (LOGGER.isLoggable(Level.FINER)) {
LOGGER.finer(format("%s - Skipping message generated locally: %s", nodeId(), event));
}
return;
}
if (LOGGER.isLoggable(Level.FINE)) {
LOGGER.fine(format("%s - Received event %s", nodeId(), event));
}
// schedule job to process the event with a short delay
final int syncDelay = configWatcher.get().getSyncDelay();
executor.schedule(new EventWorker(event), syncDelay, TimeUnit.SECONDS);
}
private class EventWorker implements Runnable {
private Event event;
public EventWorker(Event event) {
this.event = event;
}
@Override
public void run() {
if (!isStarted()) {
return;
}
try {
processEvent(event);
} catch (Exception e) {
LOGGER.log(Level.WARNING, format("%s - Event processing failed", nodeId()), e);
}
Metrics.newCounter(getClass(), "reloads").inc();
}
}
protected abstract void dispatch(Event e);
/**
* Processes the event queue.
* <p>
* <b>Note:</b> It is the responsibility of subclasses to clear events from the queue as they
* are processed.
* </p>
*/
protected abstract void processEvent(Event event) throws Exception;
ConfigChangeEvent newChangeEvent(CatalogEvent evt, Type type) {
return newChangeEvent(evt.getSource(), type);
}
ConfigChangeEvent newChangeEvent(Info subj, Type type) {
String name = (String) (OwsUtils.has(subj, "name") ? OwsUtils.get(subj, "name") : null);
WorkspaceInfo ws = (WorkspaceInfo) (OwsUtils.has(subj, "workspace") ? OwsUtils.get(subj,
"workspace") : null);
StoreInfo store = (StoreInfo) (OwsUtils.has(subj, "store") ? OwsUtils.get(subj,
"store") : null);
ConfigChangeEvent ev = new ConfigChangeEvent(subj.getId(), name, subj.getClass(), type);
if (ws != null) {
ev.setWorkspaceId(ws.getId());
}
if (store !=null) {
ev.setStoreId(store.getId());
}
if (subj instanceof ResourceInfo) {
ev.setNativeName(((ResourceInfo) subj).getNativeName());
}
return ev;
}
@Override
public void handleAddEvent(CatalogAddEvent event) throws CatalogException {
dispatch(newChangeEvent(event, Type.ADD));
}
@Override
public void handlePostModifyEvent(CatalogPostModifyEvent event) throws CatalogException {
dispatch(newChangeEvent(event, Type.MODIFY));
}
@Override
public void handleRemoveEvent(CatalogRemoveEvent event) throws CatalogException {
dispatch(newChangeEvent(event, Type.REMOVE));
}
@Override
public void handleGlobalChange(GeoServerInfo global, List<String> propertyNames,
List<Object> oldValues, List<Object> newValues) {
// optimization for update sequence
if (propertyNames.size() == 1 && propertyNames.contains("updateSequence")) {
return;
}
dispatch(newChangeEvent(global, Type.MODIFY));
}
@Override
public void handlePostServiceChange(ServiceInfo service) {
dispatch(newChangeEvent(service, Type.MODIFY));
}
@Override
public void handleServiceRemove(ServiceInfo service) {
dispatch(newChangeEvent(service, Type.REMOVE));
}
@Override
public void handleSettingsAdded(SettingsInfo settings) {
dispatch(newChangeEvent(settings, Type.ADD));
}
@Override
public void handleSettingsPostModified(SettingsInfo settings) {
dispatch(newChangeEvent(settings, Type.MODIFY));
}
@Override
public void handleSettingsRemoved(SettingsInfo settings) {
dispatch(newChangeEvent(settings, Type.REMOVE));
}
protected String nodeId() {
return HazelcastUtil.nodeId(cluster);
}
public void start() {
LOGGER.info(format("%s - Enabling processing of configuration change events", nodeId()));
this.started = true;
}
public boolean isStarted() {
return this.started;
}
public void stop() {
LOGGER.info("Disabling processing of configuration change events");
this.started = false;
}
}