diff --git a/compose/.env b/compose/.env index bb18bcbd1..66e465b79 100644 --- a/compose/.env +++ b/compose/.env @@ -15,7 +15,7 @@ GEOSERVER_BASE_PATH=/geoserver/cloud # logging profile, either "default" or "json-logs" #LOGGING_PROFILE=json-logs LOGGING_PROFILE=default -GEOSERVER_DEFAULT_PROFILES="${LOGGING_PROFILE},acl" +GEOSERVER_DEFAULT_PROFILES="${LOGGING_PROFILE},acl,logging_debug_events" GATEWAY_DEFAULT_PROFILES=${LOGGING_PROFILE} DISCOVERY_SERVER_DEFAULT_PROFILES=${LOGGING_PROFILE} diff --git a/config b/config index 464a2a300..85a5df9ff 160000 --- a/config +++ b/config @@ -1 +1 @@ -Subproject commit 464a2a3004ef50b87bf7b3486ea3fc13e0b47a6a +Subproject commit 85a5df9ffce34110fa24ca246842a1cca0a28dfd diff --git a/src/catalog/backends/common/src/main/java/org/geoserver/cloud/autoconfigure/catalog/backend/core/LifecycleEventAutoConfiguration.java b/src/catalog/backends/common/src/main/java/org/geoserver/cloud/autoconfigure/catalog/backend/core/LifecycleEventAutoConfiguration.java new file mode 100644 index 000000000..ce2986a85 --- /dev/null +++ b/src/catalog/backends/common/src/main/java/org/geoserver/cloud/autoconfigure/catalog/backend/core/LifecycleEventAutoConfiguration.java @@ -0,0 +1,30 @@ +/* + * (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.autoconfigure.catalog.backend.core; + +import org.geoserver.cloud.autoconfigure.catalog.event.ConditionalOnCatalogEvents; +import org.geoserver.cloud.event.lifecycle.LifecycleEvent; +import org.geoserver.cloud.event.remote.lifecycle.LifecycleEventProcessor; +import org.geoserver.config.plugin.GeoServerImpl; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.context.annotation.Bean; + +/** + * @since 1.0 + */ +@AutoConfiguration +@ConditionalOnClass(LifecycleEvent.class) +@ConditionalOnCatalogEvents +public class LifecycleEventAutoConfiguration { + + @Bean + LifecycleEventProcessor lifecycleEventProcessor( + @Qualifier("geoServer") GeoServerImpl rawGeoServer) { + + return new LifecycleEventProcessor(rawGeoServer); + } +} diff --git a/src/catalog/backends/common/src/main/java/org/geoserver/cloud/autoconfigure/catalog/backend/core/RemoteEventResourcePoolCleaupUpAutoConfiguration.java b/src/catalog/backends/common/src/main/java/org/geoserver/cloud/autoconfigure/catalog/backend/core/RemoteEventResourcePoolCleanupUpAutoConfiguration.java similarity index 94% rename from src/catalog/backends/common/src/main/java/org/geoserver/cloud/autoconfigure/catalog/backend/core/RemoteEventResourcePoolCleaupUpAutoConfiguration.java rename to src/catalog/backends/common/src/main/java/org/geoserver/cloud/autoconfigure/catalog/backend/core/RemoteEventResourcePoolCleanupUpAutoConfiguration.java index aece69479..832bbb3b7 100644 --- a/src/catalog/backends/common/src/main/java/org/geoserver/cloud/autoconfigure/catalog/backend/core/RemoteEventResourcePoolCleaupUpAutoConfiguration.java +++ b/src/catalog/backends/common/src/main/java/org/geoserver/cloud/autoconfigure/catalog/backend/core/RemoteEventResourcePoolCleanupUpAutoConfiguration.java @@ -22,7 +22,7 @@ @AutoConfiguration @ConditionalOnClass(InfoEvent.class) @ConditionalOnCatalogEvents -public class RemoteEventResourcePoolCleaupUpAutoConfiguration { +public class RemoteEventResourcePoolCleanupUpAutoConfiguration { @Bean RemoteEventResourcePoolProcessor remoteEventResourcePoolProcessor( diff --git a/src/catalog/backends/common/src/main/java/org/geoserver/cloud/event/remote/lifecycle/LifecycleEventProcessor.java b/src/catalog/backends/common/src/main/java/org/geoserver/cloud/event/remote/lifecycle/LifecycleEventProcessor.java new file mode 100644 index 000000000..95daeacf3 --- /dev/null +++ b/src/catalog/backends/common/src/main/java/org/geoserver/cloud/event/remote/lifecycle/LifecycleEventProcessor.java @@ -0,0 +1,69 @@ +/* + * (c) 2020 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.event.remote.lifecycle; + +import lombok.extern.slf4j.Slf4j; + +import org.geoserver.cloud.config.catalog.events.CatalogApplicationEventPublisher; +import org.geoserver.cloud.event.GeoServerEvent; +import org.geoserver.cloud.event.lifecycle.ReloadEvent; +import org.geoserver.cloud.event.lifecycle.ResetEvent; +import org.geoserver.config.plugin.GeoServerImpl; +import org.springframework.context.event.EventListener; + +/** + * Listens for and processes {@link GeoServerEvent#isRemote() remote} {@link ResetEvent} and {@link + * ReloadEvent} events. + * + * @since 1.0 + */ +@Slf4j +public class LifecycleEventProcessor { + + private final GeoServerImpl rawGeoServer; + + /** + * @param rawGeoServer used to reset or reload + */ + public LifecycleEventProcessor(GeoServerImpl rawGeoServer) { + this.rawGeoServer = rawGeoServer; + } + + @EventListener(ResetEvent.class) + public void onReset(ResetEvent event) { + + if (event.isRemote()) { + log.debug("Disabling event publishing while processing {}", event); + CatalogApplicationEventPublisher.disable(); + try { + rawGeoServer.reset(); + log.debug("Reenabling event publishing after {}", event); + } finally { + CatalogApplicationEventPublisher.enable(); + } + } else { + log.debug("Ignoring local {}", event); + } + } + + @EventListener(ReloadEvent.class) + public void onReload(ReloadEvent event) { + + if (event.isRemote()) { + log.debug("Disabling event publishing while processing {}", event); + CatalogApplicationEventPublisher.disable(); + try { + rawGeoServer.reload(); + log.debug("Reenabling event publishing after {}", event); + } catch (Exception e) { + log.error("Error reloading catalog: ", e); + } finally { + CatalogApplicationEventPublisher.enable(); + } + } else { + log.debug("Ignoring local {}", event); + } + } +} diff --git a/src/catalog/backends/common/src/main/resources/META-INF/spring.factories b/src/catalog/backends/common/src/main/resources/META-INF/spring.factories index 051b41561..afd113177 100644 --- a/src/catalog/backends/common/src/main/resources/META-INF/spring.factories +++ b/src/catalog/backends/common/src/main/resources/META-INF/spring.factories @@ -8,6 +8,7 @@ org.geoserver.cloud.autoconfigure.geotools.GeoToolsHttpClientAutoConfiguration,\ org.geoserver.cloud.autoconfigure.catalog.backend.core.GeoServerBackendAutoConfiguration,\ org.geoserver.cloud.autoconfigure.catalog.backend.core.DefaultUpdateSequenceAutoConfiguration,\ org.geoserver.cloud.autoconfigure.catalog.backend.core.XstreamServiceLoadersAutoConfiguration,\ -org.geoserver.cloud.autoconfigure.catalog.backend.core.RemoteEventResourcePoolCleaupUpAutoConfiguration,\ +org.geoserver.cloud.autoconfigure.catalog.backend.core.RemoteEventResourcePoolCleanupUpAutoConfiguration,\ +org.geoserver.cloud.autoconfigure.catalog.backend.core.LifecycleEventAutoConfiguration,\ org.geoserver.cloud.autoconfigure.security.GeoServerSecurityAutoConfiguration,\ org.geoserver.cloud.autoconfigure.metrics.catalog.CatalogMetricsAutoConfiguration diff --git a/src/catalog/backends/common/src/test/java/org/geoserver/cloud/autoconfigure/catalog/backend/core/LifecycleEventAutoConfigurationTest.java b/src/catalog/backends/common/src/test/java/org/geoserver/cloud/autoconfigure/catalog/backend/core/LifecycleEventAutoConfigurationTest.java new file mode 100644 index 000000000..893bcb287 --- /dev/null +++ b/src/catalog/backends/common/src/test/java/org/geoserver/cloud/autoconfigure/catalog/backend/core/LifecycleEventAutoConfigurationTest.java @@ -0,0 +1,39 @@ +/* + * (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.autoconfigure.catalog.backend.core; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.geoserver.cloud.event.lifecycle.LifecycleEvent; +import org.geoserver.config.plugin.GeoServerImpl; +import org.junit.jupiter.api.Test; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.test.context.FilteredClassLoader; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; + +class LifecycleEventAutoConfigurationTest { + + private final ApplicationContextRunner runner = + new ApplicationContextRunner() + .withBean("geoServer", GeoServerImpl.class) + .withConfiguration( + AutoConfigurations.of(LifecycleEventAutoConfiguration.class)); + + @Test + void testDefaultAppContextContributions() { + runner.run( + context -> assertThat(context).hasNotFailed().hasBean("lifecycleEventProcessor")); + } + + @Test + void whenDependentClassesAreNotPresent_thenBeanMissing() { + runner.withClassLoader(new FilteredClassLoader(LifecycleEvent.class)) + .run( + context -> + assertThat(context) + .hasNotFailed() + .doesNotHaveBean("lifecycleEventProcessor")); + } +} diff --git a/src/catalog/backends/common/src/test/java/org/geoserver/cloud/autoconfigure/catalog/backend/core/RemoteEventResourcePoolCleanupUpAutoConfigurationTest.java b/src/catalog/backends/common/src/test/java/org/geoserver/cloud/autoconfigure/catalog/backend/core/RemoteEventResourcePoolCleanupUpAutoConfigurationTest.java new file mode 100644 index 000000000..2d0c8eddf --- /dev/null +++ b/src/catalog/backends/common/src/test/java/org/geoserver/cloud/autoconfigure/catalog/backend/core/RemoteEventResourcePoolCleanupUpAutoConfigurationTest.java @@ -0,0 +1,43 @@ +/* + * (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.autoconfigure.catalog.backend.core; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.geoserver.catalog.plugin.CatalogPlugin; +import org.geoserver.cloud.event.info.InfoEvent; +import org.junit.jupiter.api.Test; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.test.context.FilteredClassLoader; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; + +class RemoteEventResourcePoolCleanupUpAutoConfigurationTest { + + private final ApplicationContextRunner runner = + new ApplicationContextRunner() + .withBean("rawCatalog", CatalogPlugin.class) + .withConfiguration( + AutoConfigurations.of( + RemoteEventResourcePoolCleanupUpAutoConfiguration.class)); + + @Test + void testDefaultAppContextContributions() { + runner.run( + context -> + assertThat(context) + .hasNotFailed() + .hasBean("remoteEventResourcePoolProcessor")); + } + + @Test + void whenDependentClassesAreNotPresent_thenBeanMissing() { + runner.withClassLoader(new FilteredClassLoader(InfoEvent.class)) + .run( + context -> + assertThat(context) + .hasNotFailed() + .doesNotHaveBean("remoteEventResourcePoolProcessor")); + } +} diff --git a/src/catalog/backends/datadir/src/main/java/org/geoserver/cloud/event/remote/datadir/RemoteEventDataDirectoryProcessor.java b/src/catalog/backends/datadir/src/main/java/org/geoserver/cloud/event/remote/datadir/RemoteEventDataDirectoryProcessor.java index 922d5e532..f1e10478b 100644 --- a/src/catalog/backends/datadir/src/main/java/org/geoserver/cloud/event/remote/datadir/RemoteEventDataDirectoryProcessor.java +++ b/src/catalog/backends/datadir/src/main/java/org/geoserver/cloud/event/remote/datadir/RemoteEventDataDirectoryProcessor.java @@ -59,6 +59,7 @@ void onRemoteUpdateSequenceEvent(UpdateSequenceEvent event) { } final long updateSequence = event.getUpdateSequence(); GeoServerInfo info = ModificationProxy.unwrap(configFacade.getGlobal()); + if (null == info) return; final long current = info.getUpdateSequence(); if (updateSequence > current) { info.setUpdateSequence(updateSequence); diff --git a/src/catalog/backends/datadir/src/test/java/org/geoserver/cloud/autoconfigure/catalog/backend/datadir/DataDirectoryAutoConfigurationTest.java b/src/catalog/backends/datadir/src/test/java/org/geoserver/cloud/autoconfigure/catalog/backend/datadir/DataDirectoryAutoConfigurationTest.java index 257a6e9ed..ca49bfa42 100644 --- a/src/catalog/backends/datadir/src/test/java/org/geoserver/cloud/autoconfigure/catalog/backend/datadir/DataDirectoryAutoConfigurationTest.java +++ b/src/catalog/backends/datadir/src/test/java/org/geoserver/cloud/autoconfigure/catalog/backend/datadir/DataDirectoryAutoConfigurationTest.java @@ -48,7 +48,8 @@ class DataDirectoryAutoConfigurationTest { org.geoserver.cloud.autoconfigure.catalog.backend.core .XstreamServiceLoadersAutoConfiguration.class, org.geoserver.cloud.autoconfigure.catalog.backend.core - .RemoteEventResourcePoolCleaupUpAutoConfiguration.class, + .RemoteEventResourcePoolCleanupUpAutoConfiguration + .class, org.geoserver.cloud.autoconfigure.security .GeoServerSecurityAutoConfiguration.class, org.geoserver.cloud.autoconfigure.metrics.catalog diff --git a/src/catalog/event-bus/src/test/java/org/geoserver/cloud/event/bus/BusAmqpIntegrationTests.java b/src/catalog/event-bus/src/test/java/org/geoserver/cloud/event/bus/BusAmqpIntegrationTests.java index 7fe22e989..4f78075aa 100644 --- a/src/catalog/event-bus/src/test/java/org/geoserver/cloud/event/bus/BusAmqpIntegrationTests.java +++ b/src/catalog/event-bus/src/test/java/org/geoserver/cloud/event/bus/BusAmqpIntegrationTests.java @@ -52,6 +52,7 @@ import org.geoserver.cloud.event.info.InfoEvent; import org.geoserver.cloud.event.info.InfoModified; import org.geoserver.cloud.event.info.InfoRemoved; +import org.geoserver.cloud.event.lifecycle.LifecycleEvent; import org.geoserver.config.GeoServer; import org.geoserver.config.GeoServerInfo; import org.geoserver.config.LoggingInfo; @@ -93,7 +94,7 @@ public abstract class BusAmqpIntegrationTests { @Container private static final RabbitMQContainer rabbitMQContainer = - new RabbitMQContainer("rabbitmq:3.11-management"); + new RabbitMQContainer("rabbitmq:3.13-management"); protected static ConfigurableApplicationContext remoteAppContext; private @Autowired ConfigurableApplicationContext localAppContext; @@ -113,6 +114,7 @@ static void properties(DynamicPropertyRegistry registry) { @BeforeAll static void setUpRemoteApplicationContext() { + remoteAppContext = new SpringApplicationBuilder( TestConfigurationAutoConfiguration.class, BusEventCollector.class) @@ -430,6 +432,12 @@ public EventsCaptor captureEventsOf(Class type) { return this; } + public EventsCaptor captureLifecycleEventsOf(Class type) { + local.captureLifecycle(type); + remote.captureLifecycle(type); + return this; + } + public EventsCaptor stop() { remote.stop(); local.stop(); diff --git a/src/catalog/event-bus/src/test/java/org/geoserver/cloud/event/bus/BusEventCollector.java b/src/catalog/event-bus/src/test/java/org/geoserver/cloud/event/bus/BusEventCollector.java index 2f991c2b2..7c71d403c 100644 --- a/src/catalog/event-bus/src/test/java/org/geoserver/cloud/event/bus/BusEventCollector.java +++ b/src/catalog/event-bus/src/test/java/org/geoserver/cloud/event/bus/BusEventCollector.java @@ -15,6 +15,7 @@ import org.geoserver.cloud.event.GeoServerEvent; import org.geoserver.cloud.event.info.ConfigInfoType; import org.geoserver.cloud.event.info.InfoEvent; +import org.geoserver.cloud.event.lifecycle.LifecycleEvent; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Configuration; @@ -38,7 +39,7 @@ public class BusEventCollector { private @Value("${spring.cloud.bus.id}") String busId; private @Autowired RemoteGeoServerEventBridge bridge; - private @NonNull Class eventType = InfoEvent.class; + private @NonNull Class eventType = GeoServerEvent.class; private volatile boolean capturing = false; @@ -66,6 +67,10 @@ public void capture(@NonNull Class type) { this.eventType = type; } + public void captureLifecycle(@NonNull Class type) { + this.eventType = type; + } + public RemoteGeoServerEvent expectOne(Class payloadType) { return expectOne(payloadType, x -> true); @@ -80,7 +85,7 @@ public RemoteGeoServerEvent expectOne( Class payloadType, Predicate filter) { List matches = - await().atMost(Duration.ofSeconds(500)) // + await().atMost(Duration.ofSeconds(10)) // .until(() -> allOf(payloadType, filter), not(List::isEmpty)); Supplier message = @@ -92,7 +97,22 @@ public RemoteGeoServerEvent expectOne( return matches.get(0); } - public List allOf( + public RemoteGeoServerEvent expectOneLifecycleEvent( + Class payloadType) { + + List matches = + await().atMost(Duration.ofSeconds(10)) // + .until( + () -> allOfLifecycle(payloadType, filter -> true), + not(List::isEmpty)); + + assertThat(matches.size()).isOne(); + + //noinspection OptionalGetWithoutIsPresent + return matches.stream().findFirst().get(); + } + + public List allOf( Class payloadEventType, Predicate eventFilter) { return capturedEvents(payloadEventType) @@ -102,15 +122,30 @@ public List allOf( .toList(); } - public List allOf(Class payloadType) { + public List allOfLifecycle( + Class payloadEventType, Predicate eventFilter) { + + return capturedLifecycleEvents(payloadEventType) + .filter( + remoteEvent -> + eventFilter.test(payloadEventType.cast(remoteEvent.getEvent()))) + .toList(); + } + + public List allOf(Class payloadType) { return capturedEvents(payloadType).toList(); } - public Optional first(Class payloadType) { + public Optional first(Class payloadType) { return capturedEvents(payloadType).findFirst(); } - private Stream capturedEvents( + private Stream capturedEvents( + Class payloadType) { + return capturedEvents().filter(remote -> payloadType.isInstance(remote.getEvent())); + } + + private Stream capturedLifecycleEvents( Class payloadType) { return capturedEvents().filter(remote -> payloadType.isInstance(remote.getEvent())); } diff --git a/src/catalog/event-bus/src/test/java/org/geoserver/cloud/event/bus/LifecycleRemoteApplicationEventsIT.java b/src/catalog/event-bus/src/test/java/org/geoserver/cloud/event/bus/LifecycleRemoteApplicationEventsIT.java new file mode 100644 index 000000000..2fe691964 --- /dev/null +++ b/src/catalog/event-bus/src/test/java/org/geoserver/cloud/event/bus/LifecycleRemoteApplicationEventsIT.java @@ -0,0 +1,63 @@ +/* + * (c) 2020 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.event.bus; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.geoserver.cloud.event.lifecycle.LifecycleEvent; +import org.geoserver.cloud.event.lifecycle.ReloadEvent; +import org.geoserver.cloud.event.lifecycle.ResetEvent; +import org.geoserver.config.GeoServer; +import org.geoserver.platform.GeoServerExtensions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import java.util.function.Consumer; + +class LifecycleRemoteApplicationEventsIT extends BusAmqpIntegrationTests { + + @BeforeAll + static void handleGsExtensions() { + GeoServerExtensions gse = new GeoServerExtensions(); + gse.setApplicationContext(remoteAppContext); + } + + @Test + void testGeoServerHasExecutedReset() { + + this.eventsCaptor.stop().clear().captureLifecycleEventsOf(LifecycleEvent.class).start(); + + Consumer modifier = GeoServer::reset; + modifier.accept(geoserver); + + eventsCaptor.local().expectOneLifecycleEvent(ResetEvent.class); + eventsCaptor.remote().expectOneLifecycleEvent(ResetEvent.class); + } + + @Test + void testGeoServerHasExecutedReload() { + + this.eventsCaptor.stop().clear().captureLifecycleEventsOf(LifecycleEvent.class).start(); + + Consumer modifier = + geoServer -> { + try { + geoServer.reload(); + } catch (Exception e) { + throw new RuntimeException(e); + } + }; + modifier.accept(geoserver); + + eventsCaptor.local().expectOneLifecycleEvent(ReloadEvent.class); + eventsCaptor.remote().expectOneLifecycleEvent(ReloadEvent.class); + + // reload implies reset, so shall not trigger a reset event + assertThat(eventsCaptor.local().allOf(ResetEvent.class)).isEmpty(); + ; + assertThat(eventsCaptor.remote().allOf(ResetEvent.class)).isEmpty(); + ; + } +} diff --git a/src/catalog/event-bus/src/test/java/org/geoserver/cloud/event/bus/TestConfigurationAutoConfiguration.java b/src/catalog/event-bus/src/test/java/org/geoserver/cloud/event/bus/TestConfigurationAutoConfiguration.java index 7042d0b80..adc5e45ff 100644 --- a/src/catalog/event-bus/src/test/java/org/geoserver/cloud/event/bus/TestConfigurationAutoConfiguration.java +++ b/src/catalog/event-bus/src/test/java/org/geoserver/cloud/event/bus/TestConfigurationAutoConfiguration.java @@ -6,9 +6,12 @@ import org.geoserver.catalog.Catalog; import org.geoserver.catalog.plugin.CatalogPlugin; +import org.geoserver.config.DefaultGeoServerLoader; import org.geoserver.config.GeoServer; +import org.geoserver.config.GeoServerLoader; import org.geoserver.config.plugin.GeoServerImpl; import org.geoserver.config.util.XStreamPersisterFactory; +import org.geoserver.platform.GeoServerResourceLoader; import org.geoserver.platform.config.DefaultUpdateSequence; import org.geoserver.platform.config.UpdateSequence; import org.springframework.beans.factory.annotation.Qualifier; @@ -41,4 +44,18 @@ GeoServer geoServer(@Qualifier("catalog") Catalog catalog) { gs.setCatalog(catalog); return gs; } + + @Bean + GeoServerResourceLoader geoServerResourceLoader() { + return new GeoServerResourceLoader(); + } + + @Bean + GeoServerLoader geoserverLoader( + @Qualifier("geoServer") GeoServer geoServer, + @Qualifier("geoServerResourceLoader") GeoServerResourceLoader geoServerResourceLoader) { + DefaultGeoServerLoader loader = new DefaultGeoServerLoader(geoServerResourceLoader); + loader.postProcessBeforeInitialization(geoServer, "geoserver"); + return loader; + } } diff --git a/src/catalog/events/README.md b/src/catalog/events/README.md index 7fe1bd5b8..ca5582af9 100644 --- a/src/catalog/events/README.md +++ b/src/catalog/events/README.md @@ -26,6 +26,9 @@ dependency. classDiagram direction LR GeoServerEvent <|-- UpdateSequenceEvent + GeoServerEvent <|-- LifecycleEvent + LifecycleEvent <|-- ReloadEvent + LifecycleEvent <|-- ResetEvent UpdateSequenceEvent <|-- InfoEvent UpdateSequenceEvent <|-- SecurityConfigChanged InfoEvent <|-- InfoAdded @@ -58,6 +61,13 @@ classDiagram String author String id } + class LifecycleEvent{ + <> + } + class ReloadEvent{ + } + class ResetEvent{ + } class UpdateSequenceEvent{ Long updateSequence } diff --git a/src/catalog/events/src/main/java/org/geoserver/cloud/config/catalog/events/CatalogApplicationEventPublisher.java b/src/catalog/events/src/main/java/org/geoserver/cloud/config/catalog/events/CatalogApplicationEventPublisher.java index 403ab346f..3ba36015b 100644 --- a/src/catalog/events/src/main/java/org/geoserver/cloud/config/catalog/events/CatalogApplicationEventPublisher.java +++ b/src/catalog/events/src/main/java/org/geoserver/cloud/config/catalog/events/CatalogApplicationEventPublisher.java @@ -45,6 +45,7 @@ import java.util.Map; import java.util.Objects; import java.util.UUID; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.Consumer; import java.util.function.Supplier; @@ -63,7 +64,7 @@ * @see InfoEvent LocalInfoEvent's class hierarchy */ @RequiredArgsConstructor -class CatalogApplicationEventPublisher { +public class CatalogApplicationEventPublisher { private final @NonNull Consumer eventPublisher; private final @NonNull Catalog catalog; @@ -73,6 +74,8 @@ class CatalogApplicationEventPublisher { private LocalCatalogEventPublisher publishingCatalogListener; private LocalConfigEventPublisher publishingConfigListener; + private static final AtomicBoolean ENABLED = new AtomicBoolean(true); + public @PostConstruct void initialize() { publishingCatalogListener = new LocalCatalogEventPublisher(this); publishingConfigListener = new LocalConfigEventPublisher(this); @@ -81,13 +84,31 @@ class CatalogApplicationEventPublisher { geoServer.addListener(publishingConfigListener); } + /** + * Disables event publishing. Make sure to call enable() once done forcing not to publish events + */ + public static void disable() { + ENABLED.set(false); + } + + /** Re-enables event publishing */ + public static void enable() { + ENABLED.set(true); + } + + public static boolean enabled() { + return ENABLED.get(); + } + void publish(@NonNull InfoEvent event) { - eventPublisher.accept(event); + if (enabled()) { + eventPublisher.accept(event); + } } @NonNull Long incrementSequence() { - return this.updateSequenceIncrementor.get(); + return enabled() ? this.updateSequenceIncrementor.get() : -1L; } @RequiredArgsConstructor diff --git a/src/catalog/events/src/main/java/org/geoserver/cloud/config/catalog/events/CatalogApplicationEventsConfiguration.java b/src/catalog/events/src/main/java/org/geoserver/cloud/config/catalog/events/CatalogApplicationEventsConfiguration.java index 52f447d89..604431f7e 100644 --- a/src/catalog/events/src/main/java/org/geoserver/cloud/config/catalog/events/CatalogApplicationEventsConfiguration.java +++ b/src/catalog/events/src/main/java/org/geoserver/cloud/config/catalog/events/CatalogApplicationEventsConfiguration.java @@ -6,6 +6,7 @@ import org.geoserver.catalog.Catalog; import org.geoserver.cloud.event.info.InfoEvent; +import org.geoserver.cloud.event.lifecycle.LifecycleEvent; import org.geoserver.config.GeoServer; import org.geoserver.platform.config.UpdateSequence; import org.springframework.beans.factory.annotation.Qualifier; @@ -32,4 +33,12 @@ CatalogApplicationEventPublisher localApplicationEventPublisher( // return new CatalogApplicationEventPublisher( publisher, catalog, geoServer, updateSequenceIncrementor); } + + @Bean + GeoServerLifecycleEventPublisher localGeoServerLifecycleEventPublisher( + ApplicationEventPublisher localContextPublisher) { + Consumer publisher = localContextPublisher::publishEvent; + + return new GeoServerLifecycleEventPublisher(publisher); + } } diff --git a/src/catalog/events/src/main/java/org/geoserver/cloud/config/catalog/events/GeoServerLifecycleEventPublisher.java b/src/catalog/events/src/main/java/org/geoserver/cloud/config/catalog/events/GeoServerLifecycleEventPublisher.java new file mode 100644 index 000000000..838fcce28 --- /dev/null +++ b/src/catalog/events/src/main/java/org/geoserver/cloud/config/catalog/events/GeoServerLifecycleEventPublisher.java @@ -0,0 +1,72 @@ +/* + * (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.config.catalog.events; + +import lombok.NonNull; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +import org.geoserver.catalog.Catalog; +import org.geoserver.cloud.event.lifecycle.LifecycleEvent; +import org.geoserver.cloud.event.lifecycle.ReloadEvent; +import org.geoserver.cloud.event.lifecycle.ResetEvent; +import org.geoserver.config.GeoServer; +import org.geoserver.config.impl.GeoServerLifecycleHandler; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationEvent; + +import java.util.function.Consumer; + +/** + * Implements the {@link GeoServerLifecycleHandler} interface to notify lifecycle events + * (reload/reset) as regular spring {@link ApplicationEvent application events}, and publishes them + * to the local {@link ApplicationContext}, so other components interested in these kind of events + * don't need to register themselves to the {@link Catalog} and {@link GeoServer} as listeners. + * + * @see ResetEvent + * @see ReloadEvent + */ +@RequiredArgsConstructor +@Slf4j +class GeoServerLifecycleEventPublisher implements GeoServerLifecycleHandler { + + private final @NonNull Consumer eventPublisher; + + void publish(@NonNull LifecycleEvent event) { + log.debug("Publishing {}", event); + eventPublisher.accept(event); + } + + @Override + public void onReset() { + if (CatalogApplicationEventPublisher.enabled()) { + publish(new ResetEvent()); + } + } + + @Override + public void beforeReload() { + if (CatalogApplicationEventPublisher.enabled()) { + // Thus, we want to inform all connected services as early as possible + // to activate reloading in parallel. + publish(new ReloadEvent()); + log.debug("Disabling event publishing during reload()"); + CatalogApplicationEventPublisher.disable(); + } + } + + @Override + public void onReload() { + if (!CatalogApplicationEventPublisher.enabled()) { + log.debug("Reenabling event publishing after reload()"); + CatalogApplicationEventPublisher.enable(); + } + } + + @Override + public void onDispose() { + // no-op + } +} diff --git a/src/catalog/events/src/main/java/org/geoserver/cloud/event/GeoServerEvent.java b/src/catalog/events/src/main/java/org/geoserver/cloud/event/GeoServerEvent.java index c1ca64df8..27f5390be 100644 --- a/src/catalog/events/src/main/java/org/geoserver/cloud/event/GeoServerEvent.java +++ b/src/catalog/events/src/main/java/org/geoserver/cloud/event/GeoServerEvent.java @@ -15,6 +15,7 @@ import org.geoserver.catalog.Catalog; import org.geoserver.catalog.Info; import org.geoserver.cloud.event.info.ConfigInfoType; +import org.geoserver.cloud.event.lifecycle.LifecycleEvent; import org.geoserver.config.GeoServerInfo; import org.geoserver.config.LoggingInfo; import org.springframework.core.style.ToStringCreator; @@ -23,7 +24,10 @@ import java.util.Optional; @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.WRAPPER_OBJECT) -@JsonSubTypes({@JsonSubTypes.Type(value = UpdateSequenceEvent.class)}) +@JsonSubTypes({ + @JsonSubTypes.Type(value = UpdateSequenceEvent.class), + @JsonSubTypes.Type(value = LifecycleEvent.class) +}) @SuppressWarnings("serial") public abstract class GeoServerEvent implements Serializable { diff --git a/src/catalog/events/src/main/java/org/geoserver/cloud/event/lifecycle/LifecycleEvent.java b/src/catalog/events/src/main/java/org/geoserver/cloud/event/lifecycle/LifecycleEvent.java new file mode 100644 index 000000000..aa4f5365f --- /dev/null +++ b/src/catalog/events/src/main/java/org/geoserver/cloud/event/lifecycle/LifecycleEvent.java @@ -0,0 +1,26 @@ +/* + * (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.event.lifecycle; + +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; + +import org.geoserver.cloud.event.GeoServerEvent; + +@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.WRAPPER_OBJECT) +@JsonSubTypes({ + @JsonSubTypes.Type(value = ReloadEvent.class), + @JsonSubTypes.Type(value = ResetEvent.class) +}) +@SuppressWarnings("serial") +public abstract class LifecycleEvent extends GeoServerEvent { + + @Override + public String toShortString() { + String originService = getOrigin(); + String type = getClass().getSimpleName(); + return "%s[origin: %s]".formatted(type, originService); + } +} diff --git a/src/catalog/events/src/main/java/org/geoserver/cloud/event/lifecycle/ReloadEvent.java b/src/catalog/events/src/main/java/org/geoserver/cloud/event/lifecycle/ReloadEvent.java new file mode 100644 index 000000000..0dd20db78 --- /dev/null +++ b/src/catalog/events/src/main/java/org/geoserver/cloud/event/lifecycle/ReloadEvent.java @@ -0,0 +1,17 @@ +/* + * (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.event.lifecycle; + +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import com.fasterxml.jackson.annotation.JsonTypeName; + +@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.WRAPPER_OBJECT) +@JsonTypeName("ReloadEvent") +@SuppressWarnings("serial") +public class ReloadEvent extends LifecycleEvent { + public ReloadEvent() { + // no-op, for serialization + } +} diff --git a/src/catalog/events/src/main/java/org/geoserver/cloud/event/lifecycle/ResetEvent.java b/src/catalog/events/src/main/java/org/geoserver/cloud/event/lifecycle/ResetEvent.java new file mode 100644 index 000000000..1c1c90458 --- /dev/null +++ b/src/catalog/events/src/main/java/org/geoserver/cloud/event/lifecycle/ResetEvent.java @@ -0,0 +1,17 @@ +/* + * (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.event.lifecycle; + +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import com.fasterxml.jackson.annotation.JsonTypeName; + +@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.WRAPPER_OBJECT) +@JsonTypeName("ResetEvent") +@SuppressWarnings("serial") +public class ResetEvent extends LifecycleEvent { + public ResetEvent() { + // no-op, for serialization + } +} diff --git a/src/catalog/events/src/test/java/org/geoserver/cloud/config/catalog/events/CatalogApplicationEventsConfigurationTest.java b/src/catalog/events/src/test/java/org/geoserver/cloud/config/catalog/events/CatalogApplicationEventsConfigurationTest.java index cee4e2ac4..6ffcb1973 100644 --- a/src/catalog/events/src/test/java/org/geoserver/cloud/config/catalog/events/CatalogApplicationEventsConfigurationTest.java +++ b/src/catalog/events/src/test/java/org/geoserver/cloud/config/catalog/events/CatalogApplicationEventsConfigurationTest.java @@ -6,9 +6,6 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.*; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertSame; -import static org.junit.jupiter.api.Assertions.assertTrue; import lombok.NonNull; @@ -21,26 +18,20 @@ import org.geoserver.catalog.plugin.Patch; import org.geoserver.catalog.plugin.Patch.Property; import org.geoserver.catalog.plugin.PropertyDiff; +import org.geoserver.cloud.event.GeoServerEvent; import org.geoserver.cloud.event.catalog.CatalogInfoAdded; import org.geoserver.cloud.event.catalog.CatalogInfoModified; import org.geoserver.cloud.event.catalog.CatalogInfoRemoved; import org.geoserver.cloud.event.config.ConfigInfoAdded; import org.geoserver.cloud.event.config.ConfigInfoModified; import org.geoserver.cloud.event.config.ServiceRemoved; -import org.geoserver.cloud.event.info.ConfigInfoType; -import org.geoserver.cloud.event.info.InfoAdded; -import org.geoserver.cloud.event.info.InfoEvent; -import org.geoserver.cloud.event.info.InfoModified; -import org.geoserver.cloud.event.info.InfoRemoved; +import org.geoserver.cloud.event.info.*; +import org.geoserver.cloud.event.lifecycle.LifecycleEvent; +import org.geoserver.cloud.event.lifecycle.ReloadEvent; +import org.geoserver.cloud.event.lifecycle.ResetEvent; import org.geoserver.cloud.test.ApplicationEventCapturingListener; -import org.geoserver.config.ConfigurationListener; -import org.geoserver.config.CoverageAccessInfo; +import org.geoserver.config.*; import org.geoserver.config.CoverageAccessInfo.QueueType; -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 org.geoserver.config.impl.CoverageAccessInfoImpl; import org.geoserver.config.impl.SettingsInfoImpl; import org.geoserver.wms.WMSInfoImpl; @@ -71,7 +62,7 @@ class CatalogApplicationEventsConfigurationTest { private CatalogTestData testData; public @BeforeEach void before() { - listener.setCaptureEventsOf(InfoEvent.class); + listener.setCaptureEventsOf(GeoServerEvent.class); catalog.dispose(); listener.clear(); testData = CatalogTestData.empty(() -> catalog, () -> geoserver).initialize(); @@ -95,6 +86,41 @@ void testCatalogEventBroadcasterHasSetUpItself() { assertTrue(publisherListener.isPresent()); } + @Test + void testGSLifeCycleDispatchOnReset() { + geoserver.reset(); + + // Check that there is no other event being triggered, we expect a single one. + List allEvents = listener.allOf(LifecycleEvent.class); + assertEquals(1, allEvents.size()); + + // And we expect it to be a (local) ResetEvent. + ResetEvent resetEvent = listener.expectOne(ResetEvent.class); + assertTrue(resetEvent.isLocal()); + } + + @Test + void testGSLifeCycleIgnoreOnDispose() { + geoserver.dispose(); + + // We don't expect any event on disposal. + List allEvents = listener.allOf(LifecycleEvent.class); + assertEquals(0, allEvents.size()); + } + + @Test + void testGSLifeCycleDispatchOnReload() throws Exception { + geoserver.reload(); + + // Check that there is no other event being triggered, we expect two ones. + List allEvents = listener.allOf(LifecycleEvent.class); + assertEquals(1, allEvents.size()); + + // And we expect them to be a (local) ResetEvent and a (local) ReloadEvent. + ReloadEvent reloadEvent = listener.expectOne(ReloadEvent.class); + assertTrue(reloadEvent.isLocal()); + } + @Test void testConfigEventBroadcasterHasSetUpItself() { Optional publisherListener = diff --git a/src/catalog/events/src/test/java/org/geoserver/cloud/config/catalog/events/TestConfigurationAutoConfiguration.java b/src/catalog/events/src/test/java/org/geoserver/cloud/config/catalog/events/TestConfigurationAutoConfiguration.java index 091581dea..a22b002c6 100644 --- a/src/catalog/events/src/test/java/org/geoserver/cloud/config/catalog/events/TestConfigurationAutoConfiguration.java +++ b/src/catalog/events/src/test/java/org/geoserver/cloud/config/catalog/events/TestConfigurationAutoConfiguration.java @@ -6,9 +6,13 @@ import org.geoserver.catalog.Catalog; import org.geoserver.catalog.plugin.CatalogPlugin; +import org.geoserver.config.DefaultGeoServerLoader; import org.geoserver.config.GeoServer; +import org.geoserver.config.GeoServerLoader; import org.geoserver.config.plugin.GeoServerImpl; import org.geoserver.config.util.XStreamPersisterFactory; +import org.geoserver.platform.GeoServerExtensions; +import org.geoserver.platform.GeoServerResourceLoader; import org.geoserver.platform.config.DefaultUpdateSequence; import org.geoserver.platform.config.UpdateSequence; import org.springframework.beans.factory.annotation.Qualifier; @@ -42,4 +46,23 @@ GeoServer geoServer(@Qualifier("catalog") Catalog catalog) { gs.setCatalog(catalog); return gs; } + + @Bean + GeoServerExtensions geoserverExtensions() { + return new GeoServerExtensions(); + } + + @Bean + GeoServerResourceLoader geoServerResourceLoader() { + return new GeoServerResourceLoader(); + } + + @Bean + GeoServerLoader geoserverLoader( + @Qualifier("geoServer") GeoServer geoServer, + @Qualifier("geoServerResourceLoader") GeoServerResourceLoader geoServerResourceLoader) { + DefaultGeoServerLoader loader = new DefaultGeoServerLoader(geoServerResourceLoader); + loader.postProcessBeforeInitialization(geoServer, "geoserver"); + return loader; + } }