Skip to content

Commit

Permalink
Distributed event notification of GWC config changes
Browse files Browse the repository at this point in the history
Send a distributed event upon GWC config change and react accordingly
on the receiving end by forcing a reload of the `GWCConfig` and applying
the change to the `ConfigurableBlobStore`, as done in
`GWCSettingsPage.save()`.
  • Loading branch information
groldan committed Oct 17, 2024
1 parent 53dedeb commit d59b17e
Show file tree
Hide file tree
Showing 6 changed files with 217 additions and 10 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,13 @@
import lombok.extern.slf4j.Slf4j;

import org.geoserver.cloud.config.factory.FilteringXmlBeanDefinitionReader;
import org.geoserver.cloud.gwc.event.ConfigChangeEvent;
import org.geoserver.config.util.XStreamPersisterFactory;
import org.geoserver.gwc.config.CloudGwcConfigPersister;
import org.geoserver.gwc.config.GWCConfigPersister;
import org.geoserver.platform.GeoServerResourceLoader;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.ImportResource;

Expand All @@ -32,6 +39,7 @@ public class GeoServerIntegrationConfiguration {
|gwcTransactionListener\
|gwcWMSExtendedCapabilitiesProvider\
|gwcInitializer\
|gwcGeoServervConfigPersister\
).*$\
""";

Expand All @@ -42,4 +50,20 @@ public class GeoServerIntegrationConfiguration {
public void log() {
log.info("GeoWebCache core GeoServer integration enabled");
}

/**
* Overrides {@code gwcGeoServervConfigPersister} with a cluster-aware {@link
* GWCConfigPersister} that sends {@link ConfigChangeEvent}s upon {@link
* GWCConfigPersister#save(org.geoserver.gwc.config.GWCConfig)}
*
* @param xsfp
* @param resourceLoader
*/
@Bean
GWCConfigPersister gwcGeoServervConfigPersister(
XStreamPersisterFactory xsfp,
GeoServerResourceLoader resourceLoader,
ApplicationEventPublisher publisher) {
return new CloudGwcConfigPersister(xsfp, resourceLoader, publisher::publishEvent);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@
@ImportResource(
reader = FilteringXmlBeanDefinitionReader.class, //
locations = {
"jar:gs-gwc-[0-9]+.*!/geowebcache-core-context.xml#name=^(?!gwcXmlConfig|gwcDefaultStorageFinder|gwcGeoServervConfigPersister|metastoreRemover).*$"
"jar:gs-gwc-[0-9]+.*!/geowebcache-core-context.xml#name=^(?!gwcXmlConfig|gwcDefaultStorageFinder|metastoreRemover).*$"
})
@Slf4j(topic = "org.geoserver.cloud.gwc.config.core")
public class GeoWebCacheCoreConfiguration {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package org.geoserver.cloud.gwc.event;

@SuppressWarnings("serial")
public class ConfigChangeEvent extends GeoWebCacheEvent {

public static final String OBJECT_ID = "gwcConfig";

public ConfigChangeEvent(Object source) {
super(source, Type.MODIFIED);
}

@Override
protected String getObjectId() {
return OBJECT_ID;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
/*
* (c) 2024 Open Source Geospatial Foundation - all rights reserved This code is licensed under the
* GPL 2.0 license, available at the root application directory.
*/
package org.geoserver.gwc.config;

import com.thoughtworks.xstream.XStream;

import lombok.extern.slf4j.Slf4j;

import org.geoserver.cloud.gwc.event.ConfigChangeEvent;
import org.geoserver.config.util.XStreamPersister;
import org.geoserver.config.util.XStreamPersisterFactory;
import org.geoserver.gwc.ConfigurableBlobStore;
import org.geoserver.platform.GeoServerExtensions;
import org.geoserver.platform.GeoServerResourceLoader;
import org.geoserver.platform.resource.Resource;
import org.geoserver.platform.resource.Resource.Type;
import org.geoserver.util.DimensionWarning;
import org.geowebcache.storage.blobstore.memory.CacheConfiguration;
import org.springframework.context.event.EventListener;

import java.io.IOException;
import java.io.InputStream;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.Set;
import java.util.function.Consumer;

/**
* extends {@link GWCConfigPersister} to send {@link ConfigChangeEvent}s upon {@link
* #save(org.geoserver.gwc.config.GWCConfig)}
*/
@Slf4j
public class CloudGwcConfigPersister extends GWCConfigPersister {

private Consumer<ConfigChangeEvent> eventPublisher;
private XStreamPersisterFactory xspf;
private GWCConfig configuration;

public CloudGwcConfigPersister(
XStreamPersisterFactory xspf,
GeoServerResourceLoader resourceLoader,
Consumer<ConfigChangeEvent> eventPublisher) {
super(xspf, resourceLoader);
this.xspf = xspf;
this.eventPublisher = eventPublisher;
}

/**
* Override to get a hold to the config and be able of re-loading it upon {@link
* ConfigChangeEvent}, since super.config is private
*/
@Override
public GWCConfig getConfig() {
if (null == configuration) {
configuration = super.getConfig();
}
return configuration;
}

/**
* Override to publish a {@link ConfigChangeEvent}
*
* @see #reloadOnRemoteConfigChangeEvent(ConfigChangeEvent)
*/
@Override
public void save(final GWCConfig config) throws IOException {
super.save(config);
this.configuration = config;
eventPublisher.accept(new ConfigChangeEvent(this));
}

@EventListener(ConfigChangeEvent.class)
public void reloadOnRemoteConfigChangeEvent(ConfigChangeEvent event) throws IOException {
final boolean isRemote = event.getSource() != this;
if (isRemote) {
log.info("Reloading gwc configuration upon remote config change event");
GWCConfig config = reload();
this.configuration = config;

// Update ConfigurableBlobStore
ConfigurableBlobStore blobstore = GeoServerExtensions.bean(ConfigurableBlobStore.class);
if (blobstore != null) {
blobstore.setChanged(config, false);
}
}
}

// super.loadConfig() is private
private synchronized GWCConfig reload() throws IOException {
Resource configFile = findConfigFile();
if (configFile == null || configFile.getType() == Type.UNDEFINED) {
throw new IllegalStateException(
"gwc config resource does not exist: %s".formatted(GWC_CONFIG_FILE));
}

XStreamPersister xmlPersister = this.xspf.createXMLPersister();
configure(xmlPersister.getXStream());
try (InputStream in = configFile.in()) {
return xmlPersister.load(in, GWCConfig.class);
}
}

// super.configureXstream() is private
private void configure(XStream xs) {
xs.alias("GeoServerGWCConfig", GWCConfig.class);
xs.alias("defaultCachingGridSetIds", HashSet.class);
xs.alias("defaultCoverageCacheFormats", HashSet.class);
xs.alias("defaultVectorCacheFormats", HashSet.class);
xs.alias("defaultOtherCacheFormats", HashSet.class);
xs.alias("InnerCacheConfiguration", CacheConfiguration.class);
xs.alias("warning", DimensionWarning.WarningType.class);
xs.allowTypes(
new Class[] {
GWCConfig.class, CacheConfiguration.class, DimensionWarning.WarningType.class
});
xs.addDefaultImplementation(LinkedHashSet.class, Set.class);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/*
* (c) 2024 Open Source Geospatial Foundation - all rights reserved This code is licensed under the
* GPL 2.0 license, available at the root application directory.
*/
package org.geoserver.cloud.gwc.bus;

import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import lombok.NonNull;

import org.geoserver.cloud.gwc.event.ConfigChangeEvent;

/**
* @since 1.9
*/
@NoArgsConstructor
@EqualsAndHashCode(callSuper = true)
@SuppressWarnings("serial")
public class RemoteConfigChangeEvent extends RemoteGeoWebCacheEvent {

public RemoteConfigChangeEvent(Object source, @NonNull String originService) {
super(source, originService);
}

protected @Override String getObjectId() {
return ConfigChangeEvent.OBJECT_ID;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import lombok.NonNull;

import org.geoserver.cloud.gwc.event.BlobStoreEvent;
import org.geoserver.cloud.gwc.event.ConfigChangeEvent;
import org.geoserver.cloud.gwc.event.GeoWebCacheEvent;
import org.geoserver.cloud.gwc.event.GridsetEvent;
import org.geoserver.cloud.gwc.event.TileLayerEvent;
Expand All @@ -33,6 +34,7 @@ default RemoteGeoWebCacheEvent toRemote(
case TileLayerEvent tle -> toRemote(tle, source, originService);
case GridsetEvent gse -> toRemote(gse, source, originService);
case BlobStoreEvent bse -> toRemote(bse, source, originService);
case ConfigChangeEvent ce -> toRemote(ce, source, originService);
default -> throw new IllegalArgumentException(
"unknown GeoWebCacheEvent type: " + local);
};
Expand All @@ -44,6 +46,7 @@ default GeoWebCacheEvent toLocal(
case RemoteTileLayerEvent tle -> toLocal(tle, source);
case RemoteGridsetEvent gse -> toLocal(gse, source);
case RemoteBlobStoreEvent bse -> toLocal(bse, source);
case RemoteConfigChangeEvent ce -> toLocal(ce, source);
default -> throw new IllegalArgumentException(
"unknown RemoteGeoWebCacheEvent type: " + remote);
};
Expand All @@ -64,36 +67,52 @@ RemoteGridsetEvent toRemote(
RemoteBlobStoreEvent toRemote(
BlobStoreEvent local, @Context Object source, @Context String originService);

ConfigChangeEvent toLocal(RemoteConfigChangeEvent remote, @Context Object source);

RemoteConfigChangeEvent toRemote(
ConfigChangeEvent local, @Context Object source, @Context String originService);

@ObjectFactory
default TileLayerEvent newTileEvent(@Context Object source) {
return new TileLayerEvent(source);
}

@ObjectFactory
default RemoteTileLayerEvent newRemoteTileEvent(
@Context Object source, @Context String originService) {
return new RemoteTileLayerEvent(source, originService);
}

@ObjectFactory
default GridsetEvent newGridsetEvent(@Context Object source) {
return new GridsetEvent(source);
}

@ObjectFactory
default RemoteGridsetEvent newRemoteGridsetEvent(
@Context Object source, @Context String originService) {
return new RemoteGridsetEvent(source, originService);
}

@ObjectFactory
default RemoteBlobStoreEvent newRemoteBlobStoreEvent(
@Context Object source, @Context String originService) {
return new RemoteBlobStoreEvent(source, originService);
default BlobStoreEvent newBlobStoreEvent(@Context Object source) {
return new BlobStoreEvent(source);
}

@ObjectFactory
default TileLayerEvent newTileEvent(@Context Object source) {
return new TileLayerEvent(source);
default RemoteBlobStoreEvent newRemoteBlobStoreEvent(
@Context Object source, @Context String originService) {
return new RemoteBlobStoreEvent(source, originService);
}

@ObjectFactory
default GridsetEvent newGridsetEvent(@Context Object source) {
return new GridsetEvent(source);
default ConfigChangeEvent newConfigChangeEvent(@Context Object source) {
return new ConfigChangeEvent(source);
}

@ObjectFactory
default BlobStoreEvent newBlobStoreEvent(@Context Object source) {
return new BlobStoreEvent(source);
default RemoteConfigChangeEvent newConfigChangeEvent(
@Context Object source, @Context String originService) {
return new RemoteConfigChangeEvent(source, originService);
}
}

0 comments on commit d59b17e

Please sign in to comment.