From bf3551686667d63560346f520bd019060fe38576 Mon Sep 17 00:00:00 2001 From: Gabriel Roldan Date: Sat, 12 Oct 2024 21:19:49 -0300 Subject: [PATCH 1/3] Add missing gs-gwc.xml initialization for pgconfig --- config | 2 +- .../gwc/backend/PgconfigGwcInitializer.java | 8 +-- ...nfigTileLayerCatalogAutoConfiguration.java | 12 +++-- .../config}/AbstractGwcInitializer.java | 48 ++++++++++++++++-- .../gwc/config/DefaultGwcInitializer.java | 49 +------------------ .../bus/GeoWebCacheRemoteEventsBroker.java | 2 +- 6 files changed, 61 insertions(+), 60 deletions(-) rename src/gwc/core/src/main/java/org/geoserver/{cloud/gwc/config/core => gwc/config}/AbstractGwcInitializer.java (83%) diff --git a/config b/config index e5540755c..38f4d0343 160000 --- a/config +++ b/config @@ -1 +1 @@ -Subproject commit e5540755cc134c5a8e036914530907f321b23155 +Subproject commit 38f4d03430939fb144bf5be2cc0b5de608d8c406 diff --git a/src/gwc/autoconfigure/src/main/java/org/geoserver/cloud/autoconfigure/gwc/backend/PgconfigGwcInitializer.java b/src/gwc/autoconfigure/src/main/java/org/geoserver/cloud/autoconfigure/gwc/backend/PgconfigGwcInitializer.java index 6ce29894d..af2fe4681 100644 --- a/src/gwc/autoconfigure/src/main/java/org/geoserver/cloud/autoconfigure/gwc/backend/PgconfigGwcInitializer.java +++ b/src/gwc/autoconfigure/src/main/java/org/geoserver/cloud/autoconfigure/gwc/backend/PgconfigGwcInitializer.java @@ -7,12 +7,13 @@ import lombok.NonNull; import lombok.extern.slf4j.Slf4j; +import org.geoserver.GeoServerConfigurationLock; import org.geoserver.cloud.gwc.backend.pgconfig.PgconfigTileLayerCatalog; -import org.geoserver.cloud.gwc.config.core.AbstractGwcInitializer; import org.geoserver.cloud.gwc.event.TileLayerEvent; import org.geoserver.cloud.gwc.repository.GeoServerTileLayerConfiguration; import org.geoserver.config.GeoServerReinitializer; import org.geoserver.gwc.ConfigurableBlobStore; +import org.geoserver.gwc.config.AbstractGwcInitializer; import org.geoserver.gwc.config.GWCConfigPersister; import org.geoserver.gwc.config.GWCInitializer; import org.geoserver.gwc.layer.TileLayerCatalog; @@ -46,8 +47,9 @@ class PgconfigGwcInitializer extends AbstractGwcInitializer { public PgconfigGwcInitializer( @NonNull GWCConfigPersister configPersister, @NonNull ConfigurableBlobStore blobStore, - @NonNull GeoServerTileLayerConfiguration geoseverTileLayers) { - super(configPersister, blobStore, geoseverTileLayers); + @NonNull GeoServerTileLayerConfiguration geoseverTileLayers, + @NonNull GeoServerConfigurationLock configLock) { + super(configPersister, blobStore, geoseverTileLayers, configLock); } @Override diff --git a/src/gwc/autoconfigure/src/main/java/org/geoserver/cloud/autoconfigure/gwc/backend/PgconfigTileLayerCatalogAutoConfiguration.java b/src/gwc/autoconfigure/src/main/java/org/geoserver/cloud/autoconfigure/gwc/backend/PgconfigTileLayerCatalogAutoConfiguration.java index b2cc42f16..4d3560bf9 100644 --- a/src/gwc/autoconfigure/src/main/java/org/geoserver/cloud/autoconfigure/gwc/backend/PgconfigTileLayerCatalogAutoConfiguration.java +++ b/src/gwc/autoconfigure/src/main/java/org/geoserver/cloud/autoconfigure/gwc/backend/PgconfigTileLayerCatalogAutoConfiguration.java @@ -7,6 +7,7 @@ import lombok.NonNull; import lombok.extern.slf4j.Slf4j; +import org.geoserver.GeoServerConfigurationLock; import org.geoserver.catalog.Catalog; import org.geoserver.cloud.autoconfigure.catalog.backend.pgconfig.ConditionalOnPgconfigBackendEnabled; import org.geoserver.cloud.autoconfigure.catalog.backend.pgconfig.PgconfigDataSourceAutoConfiguration; @@ -58,13 +59,18 @@ void log() { log.info("GeoWebCache TileLayerCatalog using PostgreSQL config backend"); } - /** Replacement for {@link GWCInitializer} when using {@link GeoServerTileLayerConfiguration} */ + /** + * Replacement for {@link GWCInitializer} when using {@link GeoServerTileLayerConfiguration} + * + * @param configLock + */ @Bean PgconfigGwcInitializer gwcInitializer( GWCConfigPersister configPersister, ConfigurableBlobStore blobStore, - GeoServerTileLayerConfiguration tileLayerCatalog) { - return new PgconfigGwcInitializer(configPersister, blobStore, tileLayerCatalog); + GeoServerTileLayerConfiguration tileLayerCatalog, + GeoServerConfigurationLock configLock) { + return new PgconfigGwcInitializer(configPersister, blobStore, tileLayerCatalog, configLock); } @Bean(name = "gwcCatalogConfiguration") diff --git a/src/gwc/core/src/main/java/org/geoserver/cloud/gwc/config/core/AbstractGwcInitializer.java b/src/gwc/core/src/main/java/org/geoserver/gwc/config/AbstractGwcInitializer.java similarity index 83% rename from src/gwc/core/src/main/java/org/geoserver/cloud/gwc/config/core/AbstractGwcInitializer.java rename to src/gwc/core/src/main/java/org/geoserver/gwc/config/AbstractGwcInitializer.java index 673b2cfdd..ac3e8600b 100644 --- a/src/gwc/core/src/main/java/org/geoserver/cloud/gwc/config/core/AbstractGwcInitializer.java +++ b/src/gwc/core/src/main/java/org/geoserver/gwc/config/AbstractGwcInitializer.java @@ -2,7 +2,7 @@ * (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.config.core; +package org.geoserver.gwc.config; import static com.google.common.base.Preconditions.checkNotNull; @@ -11,21 +11,23 @@ import lombok.NonNull; import lombok.RequiredArgsConstructor; +import org.geoserver.GeoServerConfigurationLock; +import org.geoserver.GeoServerConfigurationLock.LockType; import org.geoserver.cloud.gwc.event.TileLayerEvent; import org.geoserver.cloud.gwc.repository.GeoServerTileLayerConfiguration; import org.geoserver.config.GeoServer; import org.geoserver.config.GeoServerReinitializer; import org.geoserver.gwc.ConfigurableBlobStore; -import org.geoserver.gwc.config.GWCConfig; -import org.geoserver.gwc.config.GWCConfigPersister; -import org.geoserver.gwc.config.GWCInitializer; import org.geoserver.gwc.layer.GeoServerTileLayer; import org.geoserver.gwc.layer.TileLayerCatalog; +import org.geoserver.platform.resource.Resource; +import org.geoserver.platform.resource.Resources; import org.geowebcache.layer.TileLayer; import org.geowebcache.storage.blobstore.memory.CacheConfiguration; import org.geowebcache.storage.blobstore.memory.CacheProvider; import org.geowebcache.storage.blobstore.memory.guava.GuavaCacheProvider; import org.slf4j.Logger; +import org.springframework.beans.factory.InitializingBean; import org.springframework.context.event.EventListener; import org.springframework.util.StringUtils; @@ -54,14 +56,20 @@ * @since 1.8 */ @RequiredArgsConstructor -public abstract class AbstractGwcInitializer implements GeoServerReinitializer { +public abstract class AbstractGwcInitializer implements GeoServerReinitializer, InitializingBean { protected final @NonNull GWCConfigPersister configPersister; protected final @NonNull ConfigurableBlobStore blobStore; protected final @NonNull GeoServerTileLayerConfiguration geoseverTileLayers; + protected final @NonNull GeoServerConfigurationLock configLock; protected abstract Logger logger(); + @Override + public void afterPropertiesSet() throws IOException { + initializeGeoServerIntegrationConfigFile(); + } + /** * @see org.geoserver.config.GeoServerInitializer#initialize(org.geoserver.config.GeoServer) */ @@ -80,6 +88,36 @@ public void initialize(final GeoServer geoServer) throws Exception { setUpNonMemoryCacheableLayers(); } + /** + * Initialize the datadir/gs-gwc.xml file before {@link + * #initialize(org.geoserver.config.GeoServer) super.initialize(GeoServer)} + */ + private void initializeGeoServerIntegrationConfigFile() throws IOException { + if (configFileExists()) { + return; + } + final boolean lockAcquired = configLock.tryLock(LockType.WRITE); + if (lockAcquired) { + try { + if (!configFileExists()) { + logger().info( + "Initializing GeoServer specific GWC configuration {}", + configPersister.findConfigFile()); + GWCConfig defaults = new GWCConfig(); + defaults.setVersion("1.1.0"); + configPersister.save(defaults); + } + } finally { + configLock.unlock(); + } + } + } + + private boolean configFileExists() throws IOException { + Resource configFile = configPersister.findConfigFile(); + return Resources.exists(configFile); + } + @EventListener(TileLayerEvent.class) void onTileLayerEvent(TileLayerEvent event) { cacheProvider().ifPresent(cache -> onTileLayerEvent(event, cache)); diff --git a/src/gwc/core/src/main/java/org/geoserver/gwc/config/DefaultGwcInitializer.java b/src/gwc/core/src/main/java/org/geoserver/gwc/config/DefaultGwcInitializer.java index 42447d268..01c79b31d 100644 --- a/src/gwc/core/src/main/java/org/geoserver/gwc/config/DefaultGwcInitializer.java +++ b/src/gwc/core/src/main/java/org/geoserver/gwc/config/DefaultGwcInitializer.java @@ -8,18 +8,11 @@ import lombok.extern.slf4j.Slf4j; import org.geoserver.GeoServerConfigurationLock; -import org.geoserver.GeoServerConfigurationLock.LockType; -import org.geoserver.cloud.gwc.config.core.AbstractGwcInitializer; import org.geoserver.cloud.gwc.repository.GeoServerTileLayerConfiguration; import org.geoserver.gwc.ConfigurableBlobStore; import org.geoserver.gwc.layer.CatalogConfiguration; import org.geoserver.gwc.layer.TileLayerCatalog; -import org.geoserver.platform.resource.Resource; -import org.geoserver.platform.resource.Resources; import org.slf4j.Logger; -import org.springframework.beans.factory.InitializingBean; - -import java.io.IOException; /** * Replaces {@link GWCInitializer} @@ -37,9 +30,7 @@ * */ @Slf4j(topic = "org.geoserver.cloud.gwc.config.core") -public class DefaultGwcInitializer extends AbstractGwcInitializer implements InitializingBean { - - @NonNull private final GeoServerConfigurationLock configLock; +public class DefaultGwcInitializer extends AbstractGwcInitializer { public DefaultGwcInitializer( @NonNull GWCConfigPersister configPersister, @@ -47,47 +38,11 @@ public DefaultGwcInitializer( @NonNull GeoServerTileLayerConfiguration geoseverTileLayers, @NonNull GeoServerConfigurationLock configLock) { - super(configPersister, blobStore, geoseverTileLayers); - this.configLock = configLock; + super(configPersister, blobStore, geoseverTileLayers, configLock); } @Override protected Logger logger() { return log; } - - /** - * Initialize the datadir/gs-gwc.xml file before {@link - * #initialize(org.geoserver.config.GeoServer) super.initialize(GeoServer)} - */ - @Override - public void afterPropertiesSet() throws Exception { - initializeGeoServerIntegrationConfigFile(); - } - - private void initializeGeoServerIntegrationConfigFile() throws IOException { - if (configFileExists()) { - return; - } - final boolean lockAcquired = configLock.tryLock(LockType.WRITE); - if (lockAcquired) { - try { - if (!configFileExists()) { - log.info( - "Initializing GeoServer specific GWC configuration {}", - configPersister.findConfigFile()); - GWCConfig defaults = new GWCConfig(); - defaults.setVersion("1.1.0"); - configPersister.save(defaults); - } - } finally { - configLock.unlock(); - } - } - } - - private boolean configFileExists() throws IOException { - Resource configFile = configPersister.findConfigFile(); - return Resources.exists(configFile); - } } diff --git a/src/gwc/integration-bus/src/main/java/org/geoserver/cloud/gwc/bus/GeoWebCacheRemoteEventsBroker.java b/src/gwc/integration-bus/src/main/java/org/geoserver/cloud/gwc/bus/GeoWebCacheRemoteEventsBroker.java index aabef620a..a34d0622f 100644 --- a/src/gwc/integration-bus/src/main/java/org/geoserver/cloud/gwc/bus/GeoWebCacheRemoteEventsBroker.java +++ b/src/gwc/integration-bus/src/main/java/org/geoserver/cloud/gwc/bus/GeoWebCacheRemoteEventsBroker.java @@ -24,7 +24,7 @@ * to the cluster as {@link RemoteGeoWebCacheEvent} on the event bus, and vice-versa. */ @RequiredArgsConstructor -@Slf4j(topic = "org.geoserver.cloud.bus.outgoing.gwc") +@Slf4j(topic = "org.geoserver.cloud.gwc.bus") public class GeoWebCacheRemoteEventsBroker { /** From cc9065d9faef2bffd9d36221a86de5491276e3b4 Mon Sep 17 00:00:00 2001 From: Gabriel Roldan Date: Sun, 13 Oct 2024 11:33:40 -0300 Subject: [PATCH 2/3] Configure GWC lock provider with the cluster-aware geoserver lock provider --- .../core/GwcCoreAutoConfigurationTest.java | 36 +++++++++++ .../core/GeoWebCacheCoreConfiguration.java | 23 ++++++- .../gwc/config/AbstractGwcInitializer.java | 64 ++++++++++++++----- 3 files changed, 104 insertions(+), 19 deletions(-) diff --git a/src/gwc/autoconfigure/src/test/java/org/geoserver/cloud/autoconfigure/gwc/core/GwcCoreAutoConfigurationTest.java b/src/gwc/autoconfigure/src/test/java/org/geoserver/cloud/autoconfigure/gwc/core/GwcCoreAutoConfigurationTest.java index dbc2d0bb5..d6cf35b56 100644 --- a/src/gwc/autoconfigure/src/test/java/org/geoserver/cloud/autoconfigure/gwc/core/GwcCoreAutoConfigurationTest.java +++ b/src/gwc/autoconfigure/src/test/java/org/geoserver/cloud/autoconfigure/gwc/core/GwcCoreAutoConfigurationTest.java @@ -17,8 +17,15 @@ import org.geoserver.cloud.gwc.repository.CloudDefaultStorageFinder; import org.geoserver.cloud.gwc.repository.CloudGwcXmlConfiguration; import org.geoserver.cloud.gwc.repository.CloudXMLResourceProvider; +import org.geoserver.gwc.ConfigurableLockProvider; +import org.geoserver.gwc.GWC; +import org.geoserver.gwc.GeoServerLockProvider; +import org.geoserver.gwc.config.AbstractGwcInitializer; import org.geoserver.gwc.config.DefaultGwcInitializer; +import org.geoserver.gwc.config.GWCConfig; +import org.geoserver.gwc.config.GWCConfigPersister; import org.geoserver.platform.GeoServerExtensionsHelper; +import org.geowebcache.locks.LockProvider; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; @@ -65,6 +72,35 @@ void defaultCacheDirectoryIsAFile(@TempDir File tmpDir) throws IOException { assertContextLoadFails(BeanInitializationException.class, "is not a directory"); } + @Test + void lockProviderDelegatesStoGeoSeverLockProvider() { + runner.run( + context -> { + GeoServerExtensionsHelper.init(context); + assertThat(context) + .hasNotFailed() + .hasBean(AbstractGwcInitializer.GWC_LOCK_PROVIDER_BEAN_NAME) + .getBean(AbstractGwcInitializer.GWC_LOCK_PROVIDER_BEAN_NAME) + .isInstanceOf(GeoServerLockProvider.class); + + GWCConfigPersister persister = context.getBean(GWCConfigPersister.class); + GWCConfig config = persister.getConfig(); + assertThat(config.getLockProviderName()) + .isEqualTo(AbstractGwcInitializer.GWC_LOCK_PROVIDER_BEAN_NAME); + + GWC gwc = GWC.get(); + + LockProvider lockProvider = gwc.getLockProvider(); + assertThat(lockProvider).isInstanceOf(ConfigurableLockProvider.class); + GeoServerLockProvider expected = + context.getBean( + AbstractGwcInitializer.GWC_LOCK_PROVIDER_BEAN_NAME, + GeoServerLockProvider.class); + assertThat(((ConfigurableLockProvider) lockProvider).getDelegate()) + .isSameAs(expected); + }); + } + @Test void contextLoads() { runner.run( 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 0ed23a98a..fd65e5e1f 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 @@ -14,12 +14,15 @@ import org.geoserver.cloud.gwc.repository.CloudDefaultStorageFinder; import org.geoserver.cloud.gwc.repository.CloudGwcXmlConfiguration; import org.geoserver.cloud.gwc.repository.CloudXMLResourceProvider; +import org.geoserver.gwc.GeoServerLockProvider; +import org.geoserver.gwc.config.AbstractGwcInitializer; import org.geoserver.platform.resource.Resource; import org.geoserver.platform.resource.ResourceStore; import org.geoserver.platform.resource.Resources; import org.geowebcache.config.ConfigurationResourceProvider; import org.geowebcache.config.XMLConfiguration; import org.geowebcache.config.XMLFileResourceProvider; +import org.geowebcache.locks.LockProvider; import org.geowebcache.storage.DefaultStorageFinder; import org.geowebcache.util.ApplicationContextProvider; import org.springframework.beans.FatalBeanException; @@ -61,6 +64,17 @@ @Slf4j(topic = "org.geoserver.cloud.gwc.config.core") public class GeoWebCacheCoreConfiguration { + /** + * @return a {@link GeoServerLockProvider} delegating the the {@link ResourceStore}, whose + * {@link ResourceStore#getLockProvider()} is known to be cluster-capable + */ + @Bean(name = AbstractGwcInitializer.GWC_LOCK_PROVIDER_BEAN_NAME) + LockProvider gwcLockProvider(@Qualifier("resourceStoreImpl") ResourceStore resourceStore) { + var provider = new GeoServerLockProvider(); + provider.setDelegate(resourceStore); + return provider; + } + @Bean SetRequestPathInfoFilter setRequestPathInfoFilter() { return new SetRequestPathInfoFilter(); @@ -283,9 +297,12 @@ public void doFilter(ServletRequest request, ServletResponse response, FilterCha * */ protected ServletRequest adaptRequest(HttpServletRequest request) { - // full request URI (e.g. '/geoserver/cloud/{workspace}/gwc/service/tms/1.0.0', where - // '/geoserver/cloud' is the context path as given by the gateway's base uri, and - // '/{workspace}/gwc' the suffix after which comes the pathInfo '/service/tms/1.0.0') + // full request URI (e.g. '/geoserver/cloud/{workspace}/gwc/service/tms/1.0.0', + // where + // '/geoserver/cloud' is the context path as given by the gateway's base uri, + // and + // '/{workspace}/gwc' the suffix after which comes the pathInfo + // '/service/tms/1.0.0') final String requestURI = request.getRequestURI(); final String gwc = "/gwc"; diff --git a/src/gwc/core/src/main/java/org/geoserver/gwc/config/AbstractGwcInitializer.java b/src/gwc/core/src/main/java/org/geoserver/gwc/config/AbstractGwcInitializer.java index ac3e8600b..3bdd25f36 100644 --- a/src/gwc/core/src/main/java/org/geoserver/gwc/config/AbstractGwcInitializer.java +++ b/src/gwc/core/src/main/java/org/geoserver/gwc/config/AbstractGwcInitializer.java @@ -18,11 +18,13 @@ import org.geoserver.config.GeoServer; import org.geoserver.config.GeoServerReinitializer; import org.geoserver.gwc.ConfigurableBlobStore; +import org.geoserver.gwc.GWC; import org.geoserver.gwc.layer.GeoServerTileLayer; import org.geoserver.gwc.layer.TileLayerCatalog; import org.geoserver.platform.resource.Resource; import org.geoserver.platform.resource.Resources; import org.geowebcache.layer.TileLayer; +import org.geowebcache.locks.LockProvider; import org.geowebcache.storage.blobstore.memory.CacheConfiguration; import org.geowebcache.storage.blobstore.memory.CacheProvider; import org.geowebcache.storage.blobstore.memory.guava.GuavaCacheProvider; @@ -58,10 +60,18 @@ @RequiredArgsConstructor public abstract class AbstractGwcInitializer implements GeoServerReinitializer, InitializingBean { + /** + * {@link GWC#saveConfig(GWCConfig)} will lookup for the {@link LockProvider} named after {@link + * GWCConfig#getLockProviderName()}. We need it to be a cluster-aware lock provider. This is the + * bean name to be registered by the configuration, and we'll set it to {@link + * GWCConfig#setLockProviderName(String)} during initialization. + */ + public static final String GWC_LOCK_PROVIDER_BEAN_NAME = "gwcClusteringLockProvider"; + protected final @NonNull GWCConfigPersister configPersister; protected final @NonNull ConfigurableBlobStore blobStore; protected final @NonNull GeoServerTileLayerConfiguration geoseverTileLayers; - protected final @NonNull GeoServerConfigurationLock configLock; + protected final @NonNull GeoServerConfigurationLock globalConfigLock; protected abstract Logger logger(); @@ -93,23 +103,45 @@ public void initialize(final GeoServer geoServer) throws Exception { * #initialize(org.geoserver.config.GeoServer) super.initialize(GeoServer)} */ private void initializeGeoServerIntegrationConfigFile() throws IOException { - if (configFileExists()) { - return; + globalConfigLock.lock(LockType.WRITE); + try { + if (configFileExists()) { + updateLockProviderName(); + } else { + logger().info( + "Initializing GeoServer specific GWC configuration {}", + configPersister.findConfigFile()); + GWCConfig defaults = new GWCConfig(); + defaults.setVersion("1.1.0"); + defaults.setLockProviderName(GWC_LOCK_PROVIDER_BEAN_NAME); + configPersister.save(defaults); + } + } finally { + globalConfigLock.unlock(); } - final boolean lockAcquired = configLock.tryLock(LockType.WRITE); - if (lockAcquired) { - try { - if (!configFileExists()) { - logger().info( - "Initializing GeoServer specific GWC configuration {}", - configPersister.findConfigFile()); - GWCConfig defaults = new GWCConfig(); - defaults.setVersion("1.1.0"); - configPersister.save(defaults); - } - } finally { - configLock.unlock(); + } + + /** + * In case the {@link GWCConfig} exists and its lock provider name is not {@link + * #GWC_LOCK_PROVIDER_BEAN_NAME}, updates and saves the configuration. + * + *

At this point, {@link #configFileExists()} is known to be true. + */ + private void updateLockProviderName() throws IOException { + final GWCConfig gwcConfig = configPersister.getConfig(); + if (!GWC_LOCK_PROVIDER_BEAN_NAME.equals(gwcConfig.getLockProviderName())) { + if (null == gwcConfig.getLockProviderName()) { + logger().info( + "Setting GeoWebCache lock provider to {}", + GWC_LOCK_PROVIDER_BEAN_NAME); + } else { + logger().warn( + "Updating GeoWebCache lock provider from {} to {}", + gwcConfig.getLockProviderName(), + GWC_LOCK_PROVIDER_BEAN_NAME); } + gwcConfig.setLockProviderName(GWC_LOCK_PROVIDER_BEAN_NAME); + configPersister.save(gwcConfig); } } From 695600906b4fca9e988ec84ccdb15ee7aca0360d Mon Sep 17 00:00:00 2001 From: Gabriel Roldan Date: Sun, 13 Oct 2024 11:51:34 -0300 Subject: [PATCH 3/3] 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()`. --- config | 2 +- .../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 ++++-- 7 files changed, 218 insertions(+), 11 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/config b/config index 38f4d0343..3cd0890a6 160000 --- a/config +++ b/config @@ -1 +1 @@ -Subproject commit 38f4d03430939fb144bf5be2cc0b5de608d8c406 +Subproject commit 3cd0890a60a9c00f61d05b7f5e5ff911b5317462 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); } }