From 4b71df8193a730562ea4ea3650a8f2cc1ac87617 Mon Sep 17 00:00:00 2001 From: Gabriel Roldan Date: Sun, 13 Oct 2024 11:51:34 -0300 Subject: [PATCH] Distributed event notification of GWC config changes 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()`. --- .../GeoServerIntegrationConfiguration.java | 24 ++++ .../core/GeoWebCacheCoreConfiguration.java | 2 +- .../cloud/gwc/event/ConfigChangeEvent.java | 16 +++ .../gwc/config/CloudGwcConfigPersister.java | 120 ++++++++++++++++++ .../gwc/bus/RemoteConfigChangeEvent.java | 28 ++++ .../cloud/gwc/bus/RemoteEventMapper.java | 37 ++++-- 6 files changed, 217 insertions(+), 10 deletions(-) create mode 100644 src/gwc/core/src/main/java/org/geoserver/cloud/gwc/event/ConfigChangeEvent.java create mode 100644 src/gwc/core/src/main/java/org/geoserver/gwc/config/CloudGwcConfigPersister.java create mode 100644 src/gwc/integration-bus/src/main/java/org/geoserver/cloud/gwc/bus/RemoteConfigChangeEvent.java diff --git a/src/gwc/core/src/main/java/org/geoserver/cloud/gwc/config/core/GeoServerIntegrationConfiguration.java b/src/gwc/core/src/main/java/org/geoserver/cloud/gwc/config/core/GeoServerIntegrationConfiguration.java index 2d8073157..d09a4da44 100644 --- a/src/gwc/core/src/main/java/org/geoserver/cloud/gwc/config/core/GeoServerIntegrationConfiguration.java +++ b/src/gwc/core/src/main/java/org/geoserver/cloud/gwc/config/core/GeoServerIntegrationConfiguration.java @@ -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; @@ -32,6 +39,7 @@ public class GeoServerIntegrationConfiguration { |gwcTransactionListener\ |gwcWMSExtendedCapabilitiesProvider\ |gwcInitializer\ + |gwcGeoServervConfigPersister\ ).*$\ """; @@ -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); + } } diff --git a/src/gwc/core/src/main/java/org/geoserver/cloud/gwc/config/core/GeoWebCacheCoreConfiguration.java b/src/gwc/core/src/main/java/org/geoserver/cloud/gwc/config/core/GeoWebCacheCoreConfiguration.java index fd65e5e1f..2c1e01836 100644 --- a/src/gwc/core/src/main/java/org/geoserver/cloud/gwc/config/core/GeoWebCacheCoreConfiguration.java +++ b/src/gwc/core/src/main/java/org/geoserver/cloud/gwc/config/core/GeoWebCacheCoreConfiguration.java @@ -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 { diff --git a/src/gwc/core/src/main/java/org/geoserver/cloud/gwc/event/ConfigChangeEvent.java b/src/gwc/core/src/main/java/org/geoserver/cloud/gwc/event/ConfigChangeEvent.java new file mode 100644 index 000000000..d45286bff --- /dev/null +++ b/src/gwc/core/src/main/java/org/geoserver/cloud/gwc/event/ConfigChangeEvent.java @@ -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; + } +} diff --git a/src/gwc/core/src/main/java/org/geoserver/gwc/config/CloudGwcConfigPersister.java b/src/gwc/core/src/main/java/org/geoserver/gwc/config/CloudGwcConfigPersister.java new file mode 100644 index 000000000..54733386e --- /dev/null +++ b/src/gwc/core/src/main/java/org/geoserver/gwc/config/CloudGwcConfigPersister.java @@ -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 eventPublisher; + private XStreamPersisterFactory xspf; + private GWCConfig configuration; + + public CloudGwcConfigPersister( + XStreamPersisterFactory xspf, + GeoServerResourceLoader resourceLoader, + Consumer 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); + } +} diff --git a/src/gwc/integration-bus/src/main/java/org/geoserver/cloud/gwc/bus/RemoteConfigChangeEvent.java b/src/gwc/integration-bus/src/main/java/org/geoserver/cloud/gwc/bus/RemoteConfigChangeEvent.java new file mode 100644 index 000000000..533494eb0 --- /dev/null +++ b/src/gwc/integration-bus/src/main/java/org/geoserver/cloud/gwc/bus/RemoteConfigChangeEvent.java @@ -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; + } +} diff --git a/src/gwc/integration-bus/src/main/java/org/geoserver/cloud/gwc/bus/RemoteEventMapper.java b/src/gwc/integration-bus/src/main/java/org/geoserver/cloud/gwc/bus/RemoteEventMapper.java index 5f0eb3a7a..672a8cf0e 100644 --- a/src/gwc/integration-bus/src/main/java/org/geoserver/cloud/gwc/bus/RemoteEventMapper.java +++ b/src/gwc/integration-bus/src/main/java/org/geoserver/cloud/gwc/bus/RemoteEventMapper.java @@ -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; @@ -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); }; @@ -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); }; @@ -64,12 +67,27 @@ 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) { @@ -77,23 +95,24 @@ default RemoteGridsetEvent newRemoteGridsetEvent( } @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); } }