/* (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.addressString; import static org.geoserver.cluster.hazelcast.HazelcastUtil.localAddress; import java.lang.reflect.Method; import java.lang.reflect.Proxy; import java.net.InetSocketAddress; import java.util.Set; import java.util.UUID; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.atomic.AtomicInteger; import java.util.logging.Level; import javax.annotation.Nullable; import org.geoserver.catalog.Catalog; import org.geoserver.catalog.CatalogInfo; import org.geoserver.catalog.Info; import org.geoserver.catalog.LayerGroupInfo; import org.geoserver.catalog.LayerInfo; import org.geoserver.catalog.NamespaceInfo; import org.geoserver.catalog.ResourceInfo; import org.geoserver.catalog.StoreInfo; import org.geoserver.catalog.StyleInfo; import org.geoserver.catalog.WorkspaceInfo; import org.geoserver.catalog.event.CatalogAddEvent; import org.geoserver.catalog.event.CatalogListener; import org.geoserver.catalog.event.CatalogPostModifyEvent; import org.geoserver.catalog.event.CatalogRemoveEvent; import org.geoserver.catalog.event.impl.CatalogAddEventImpl; import org.geoserver.catalog.event.impl.CatalogEventImpl; import org.geoserver.catalog.event.impl.CatalogPostModifyEventImpl; import org.geoserver.catalog.event.impl.CatalogRemoveEventImpl; import org.geoserver.cluster.ConfigChangeEvent; import org.geoserver.cluster.ConfigChangeEvent.Type; import org.geoserver.cluster.Event; import org.geoserver.config.ConfigurationListener; import org.geoserver.config.GeoServer; import org.geoserver.config.GeoServerInfo; import org.geoserver.config.LoggingInfo; import org.geoserver.config.ServiceInfo; import org.geoserver.config.SettingsInfo; import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableList; import com.google.common.collect.Maps; import com.hazelcast.core.ITopic; import com.hazelcast.core.Member; import com.hazelcast.core.Message; import com.hazelcast.core.MessageListener; import com.yammer.metrics.Metrics; /** * Synchronizer that converts cluster events and dispatches them the GeoServer config/catalog. * <p> * This synchronizer assumes a shared data directory among nodes in the cluster. * </p> * * @author Justin Deoliveira, OpenGeo */ public class EventHzSynchronizer extends HzSynchronizer { private final ITopic<UUID> ackTopic; private final AckListener ackListener; public EventHzSynchronizer(HzCluster cluster, GeoServer gs) { super(cluster, gs); ackTopic = cluster.getHz().getTopic("geoserver.config.ack"); ackTopic.addMessageListener(ackListener = new AckListener()); } @Override protected void dispatch(Event e) { if (LOGGER.isLoggable(Level.FINE)) { LOGGER.fine(format("%s - Publishing event %s", nodeId(), e)); } final UUID evendId = e.getUUID(); Set<Member> members = cluster.getHz().getCluster().getMembers(); int expectedAcks = 0; for (Member member : members) { if (!(member.localMember())) { expectedAcks++; } } ackListener.expectedAckCounters.put(evendId, new AtomicInteger(expectedAcks)); e.setSource(localAddress(cluster.getHz())); topic.publish(e); Metrics.newCounter(getClass(), "dispatched").inc(); waitForAck(e); } private class AckListener implements MessageListener<UUID> { final ConcurrentMap<UUID, AtomicInteger> expectedAckCounters = Maps.newConcurrentMap(); @Override public void onMessage(Message<UUID> message) { UUID eventId = message.getMessageObject(); AtomicInteger countDown = expectedAckCounters.get(eventId); if (countDown != null) { countDown.decrementAndGet(); String originAddr = null; Member publishingMember = message.getPublishingMember(); if (publishingMember != null) { InetSocketAddress socketAddress = publishingMember.getSocketAddress(); if (socketAddress != null) { originAddr = addressString(socketAddress); } } LOGGER.finer(format("%s - Got ack on event %s from %s", nodeId(), eventId, originAddr)); } } } protected final void ack(Event event) { UUID uuid = event.getUUID(); ackTopic.publish(uuid); LOGGER.finer(format("%s - Sent ack for event %s", nodeId(), uuid)); } private void waitForAck(Event event) { final UUID evendId = event.getUUID(); final int maxWaitMillis = cluster.getAckTimeoutMillis(); final int waitInterval = 100; LOGGER.fine(format("%s - Waiting for acks on %s", nodeId(), evendId)); final AtomicInteger countDown = ackListener.expectedAckCounters.get(evendId); int waited = 0; try { while (waited < maxWaitMillis) { int remainingAcks = countDown.get(); if (remainingAcks <= 0) { return; } try { Thread.sleep(waitInterval); waited += waitInterval; } catch (InterruptedException ex) { return; } } LOGGER.warning(format("%s - After %dms, %d acks missing for event %s", nodeId(), maxWaitMillis, countDown.get(), event)); } finally { ackListener.expectedAckCounters.remove(evendId); } } @Override protected void processEvent(Event event) throws Exception { Preconditions.checkState(isStarted()); if (!(event instanceof ConfigChangeEvent)) { return; } try { LOGGER.fine(format("%s - Processing event %s", nodeId(), event)); ConfigChangeEvent ce = (ConfigChangeEvent) event; Class<? extends Info> clazz = ce.getObjectInterface(); if (CatalogInfo.class.isAssignableFrom(clazz)) { processCatalogEvent(ce); } else { processGeoServerConfigEvent(ce); } } catch (Exception e) { LOGGER.log(Level.WARNING, format("%s - Error processing event %s", nodeId(), event), e); } finally { ack(event); } } private void processCatalogEvent(final ConfigChangeEvent event) throws NoSuchMethodException, SecurityException { Class<? extends Info> clazz = event.getObjectInterface(); final Type t = event.getChangeType(); final String id = event.getObjectId(); final String name = event.getObjectName(); final @Nullable String nativeName = event.getNativeName(); // catalog event CatalogInfo subj; Method notifyMethod; CatalogEventImpl evt; final Catalog cat = cluster.getRawCatalog(); switch (t) { case ADD: subj = getCatalogInfo(cat, id, clazz); notifyMethod = CatalogListener.class.getMethod("handleAddEvent", CatalogAddEvent.class); evt = new CatalogAddEventImpl(); break; case MODIFY: subj = getCatalogInfo(cat, id, clazz); notifyMethod = CatalogListener.class.getMethod("handlePostModifyEvent", CatalogPostModifyEvent.class); evt = new CatalogPostModifyEventImpl(); break; case REMOVE: notifyMethod = CatalogListener.class.getMethod("handleRemoveEvent", CatalogRemoveEvent.class); evt = new CatalogRemoveEventImpl(); RemovedObjectProxy proxy = new RemovedObjectProxy(id, name, clazz, nativeName); if (ResourceInfo.class.isAssignableFrom(clazz) && event.getStoreId() != null) { proxy.addCatalogCollaborator("store", cat.getStore(event.getStoreId(), StoreInfo.class)); } subj = (CatalogInfo) Proxy.newProxyInstance(getClass().getClassLoader(), new Class[] { clazz }, proxy); break; default: throw new IllegalStateException("Should not happen"); } if (subj == null) {// can't happen if type == DELETE if (subj == null) { String message = format( "%s - Error processing event %s: object not found in catalog", nodeId(), event); LOGGER.warning(message); return; } } evt.setSource(subj); try { for (CatalogListener l : ImmutableList.copyOf(cat.getListeners())) { // Don't notify self otherwise the event bounces back out into the // cluster. if (l != this && isStarted()) { notifyMethod.invoke(l, evt); } } } catch (Exception ex) { LOGGER.log(Level.WARNING, format("%s - Event dispatch failed: %s", nodeId(), event), ex); } } private void processGeoServerConfigEvent(ConfigChangeEvent ce) throws NoSuchMethodException, SecurityException { final Class<? extends Info> clazz = ce.getObjectInterface(); final String id = ce.getObjectId(); final Catalog cat = cluster.getRawCatalog(); Info subj; Method notifyMethod; if (GeoServerInfo.class.isAssignableFrom(clazz)) { subj = gs.getGlobal(); notifyMethod = ConfigurationListener.class.getMethod("handlePostGlobalChange", GeoServerInfo.class); } else if (SettingsInfo.class.isAssignableFrom(clazz)) { WorkspaceInfo ws = ce.getWorkspaceId() != null ? cat.getWorkspace(ce.getWorkspaceId()) : null; subj = ws != null ? gs.getSettings(ws) : gs.getSettings(); notifyMethod = ConfigurationListener.class.getMethod("handleSettingsPostModified", SettingsInfo.class); } else if (LoggingInfo.class.isAssignableFrom(clazz)) { subj = gs.getLogging(); notifyMethod = ConfigurationListener.class.getMethod("handlePostLoggingChange", LoggingInfo.class); } else if (ServiceInfo.class.isAssignableFrom(clazz)) { subj = gs.getService(id, (Class<ServiceInfo>) clazz); notifyMethod = ConfigurationListener.class.getMethod("handlePostServiceChange", ServiceInfo.class); } else { throw new IllegalStateException("Unknown event type " + clazz); } for (ConfigurationListener l : gs.getListeners()) { try { if (l != this) notifyMethod.invoke(l, subj); } catch (Exception ex) { LOGGER.log(Level.WARNING, format("%s - Event dispatch failed: %s", nodeId(), ce), ex); } } } private CatalogInfo getCatalogInfo(Catalog cat, String id, Class<? extends Info> clazz) { CatalogInfo subj = null; if (WorkspaceInfo.class.isAssignableFrom(clazz)) { subj = cat.getWorkspace(id); } else if (NamespaceInfo.class.isAssignableFrom(clazz)) { subj = cat.getNamespace(id); } else if (StoreInfo.class.isAssignableFrom(clazz)) { subj = cat.getStore(id, (Class<StoreInfo>) clazz); } else if (ResourceInfo.class.isAssignableFrom(clazz)) { subj = cat.getResource(id, (Class<ResourceInfo>) clazz); } else if (LayerInfo.class.isAssignableFrom(clazz)) { subj = cat.getLayer(id); } else if (StyleInfo.class.isAssignableFrom(clazz)) { subj = cat.getStyle(id); } else if (LayerGroupInfo.class.isAssignableFrom(clazz)) { subj = cat.getLayerGroup(id); } return subj; } }