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..f130ddae7 --- /dev/null +++ b/src/catalog/backends/common/src/main/java/org/geoserver/cloud/event/remote/lifecycle/LifecycleEventProcessor.java @@ -0,0 +1,52 @@ +/* + * (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.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 ResetEvent} and {@link ReloadEvent} events. + * + * @since 1.0 + */ +@Slf4j(topic = "org.geoserver.cloud.event.remote.lifecycle") +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("Received a remote ResetEvent, triggering a GeoServer reset ({})", event); + rawGeoServer.reset(true); + } + } + + @EventListener(ReloadEvent.class) + public void onReload(ReloadEvent event) { + + if (event.isRemote()) { + log.debug("Received a remote ReloadEvent, triggering a GeoServer reload ({})", event); + try { + rawGeoServer.reload(null, true); + } catch (Exception e) { + log.error("Error reloading catalog: ", e); + } + } + } +} 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/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..0f5373ce6 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,6 +97,21 @@ public RemoteGeoServerEvent expectOne( return matches.get(0); } + 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) { @@ -102,6 +122,16 @@ public List allOf( .toList(); } + 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(); } @@ -115,6 +145,11 @@ private Stream capturedEvents( return capturedEvents().filter(remote -> payloadType.isInstance(remote.getEvent())); } + private Stream capturedLifecycleEvents( + Class payloadType) { + return capturedEvents().filter(remote -> payloadType.isInstance(remote.getEvent())); + } + private Stream capturedEvents() { return events.stream(); } 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..269619710 --- /dev/null +++ b/src/catalog/event-bus/src/test/java/org/geoserver/cloud/event/bus/LifecycleRemoteApplicationEventsIT.java @@ -0,0 +1,58 @@ +/* + * (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 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); + + // reload also triggers reset! + eventsCaptor.local().expectOneLifecycleEvent(ReloadEvent.class); + eventsCaptor.local().expectOneLifecycleEvent(ResetEvent.class); + eventsCaptor.remote().expectOneLifecycleEvent(ReloadEvent.class); + eventsCaptor.remote().expectOneLifecycleEvent(ResetEvent.class); + } +} 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/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..4efd93506 --- /dev/null +++ b/src/catalog/events/src/main/java/org/geoserver/cloud/config/catalog/events/GeoServerLifecycleEventPublisher.java @@ -0,0 +1,66 @@ +/* + * (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) { + eventPublisher.accept(event); + } + + @Override + public void onReset() { + log.debug("Publishing the onReset event"); + + publish(new ResetEvent()); + } + + @Override + public void onDispose() { + log.debug("Ignoring the onDispose event"); + } + + @Override + public void beforeReload() { + // Thus, we want to inform all connected services as early as possible + // to activate reloading in parallel. + log.debug("Publishing the beforeReload event"); + + publish(new ReloadEvent()); + } + + @Override + public void onReload() { + log.debug("Ignoring the onReload event"); + } +} 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..c7b2020f3 --- /dev/null +++ b/src/catalog/events/src/main/java/org/geoserver/cloud/event/lifecycle/LifecycleEvent.java @@ -0,0 +1,25 @@ +/* + * (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) +}) +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..27302b40a --- /dev/null +++ b/src/catalog/events/src/main/java/org/geoserver/cloud/event/lifecycle/ReloadEvent.java @@ -0,0 +1,16 @@ +/* + * (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") +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..0c41c570a --- /dev/null +++ b/src/catalog/events/src/main/java/org/geoserver/cloud/event/lifecycle/ResetEvent.java @@ -0,0 +1,16 @@ +/* + * (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") +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..c84fb2edc 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,43 @@ 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(2, allEvents.size()); + + // And we expect them to be a (local) ResetEvent and a (local) ReloadEvent. + ReloadEvent reloadEvent = listener.expectOne(ReloadEvent.class); + assertTrue(reloadEvent.isLocal()); + ResetEvent resetEvent = listener.expectOne(ResetEvent.class); + assertTrue(resetEvent.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; + } } diff --git a/src/catalog/plugin/src/main/java/org/geoserver/config/plugin/GeoServerImpl.java b/src/catalog/plugin/src/main/java/org/geoserver/config/plugin/GeoServerImpl.java index 3a7cf4dc6..0721a3a49 100644 --- a/src/catalog/plugin/src/main/java/org/geoserver/config/plugin/GeoServerImpl.java +++ b/src/catalog/plugin/src/main/java/org/geoserver/config/plugin/GeoServerImpl.java @@ -14,16 +14,7 @@ import org.geoserver.catalog.impl.ModificationProxy; import org.geoserver.catalog.impl.ResolvingProxy; import org.geoserver.catalog.plugin.Patch; -import org.geoserver.config.ConfigurationListener; -import org.geoserver.config.GeoServer; -import org.geoserver.config.GeoServerFacade; -import org.geoserver.config.GeoServerFactory; -import org.geoserver.config.GeoServerInfo; -import org.geoserver.config.GeoServerLoader; -import org.geoserver.config.GeoServerLoaderProxy; -import org.geoserver.config.LoggingInfo; -import org.geoserver.config.ServiceInfo; -import org.geoserver.config.SettingsInfo; +import org.geoserver.config.*; import org.geoserver.config.impl.GeoServerFactoryImpl; import org.geoserver.config.impl.GeoServerLifecycleHandler; import org.geoserver.ows.LocalWorkspace; @@ -483,6 +474,58 @@ void fireServiceRemoved(ServiceInfo service) { } } + void fireBeforeReload(List handlers) { + for (GeoServerLifecycleHandler handler : handlers) { + try { + handler.beforeReload(); + } catch (RuntimeException t) { + LOGGER.log( + Level.SEVERE, + "A GeoServer lifecycle handler threw an exception during reload", + t); + } + } + } + + void fireOnReload(List handlers) { + for (GeoServerLifecycleHandler handler : handlers) { + try { + handler.onReload(); + } catch (Exception t) { + LOGGER.log( + Level.SEVERE, + "A GeoServer lifecycle handler threw an exception during reload", + t); + } + } + } + + void fireOnDispose(List handlers) { + for (GeoServerLifecycleHandler handler : handlers) { + try { + handler.onDispose(); + } catch (RuntimeException t) { + LOGGER.log( + Level.SEVERE, + "A GeoServer lifecycle handler threw an exception during dispose", + t); + } + } + } + + void fireOnReset(List handlers) { + for (GeoServerLifecycleHandler handler : handlers) { + try { + handler.onReset(); + } catch (RuntimeException t) { + LOGGER.log( + Level.SEVERE, + "A GeoServer lifecycle handler threw an exception during reset", + t); + } + } + } + @Override public void addListener(ConfigurationListener listener) { listeners.add(listener); @@ -500,17 +543,19 @@ public Collection getListeners() { @Override public void dispose() { + dispose(false); + } + + public void dispose(boolean silent) { // look for pluggable handlers - for (GeoServerLifecycleHandler handler : - GeoServerExtensions.extensions(GeoServerLifecycleHandler.class)) { - try { - handler.onDispose(); - } catch (RuntimeException t) { - LOGGER.log( - Level.SEVERE, - "A GeoServer lifecycle handler threw an exception during dispose", - t); - } + if (!silent) { + List handlers = + GeoServerExtensions.extensions(GeoServerLifecycleHandler.class); + fireOnDispose(handlers); + } else { + LOGGER.log( + Level.FINE, + "Reloading GeoServer configuration, but not notifying lifecycle dispose handlers"); } // internal cleanup @@ -521,36 +566,37 @@ public void dispose() { @Override public void reload() throws Exception { - this.reload(null); + this.reload(null, false); } @Override public void reload(Catalog newCatalog) throws Exception { + this.reload(newCatalog, false); + } + + public void reload(Catalog newCatalog, boolean silent) throws Exception { // notify start of reload List handlers = GeoServerExtensions.extensions(GeoServerLifecycleHandler.class); - for (GeoServerLifecycleHandler handler : handlers) { - try { - handler.beforeReload(); - } catch (RuntimeException t) { - LOGGER.log( - Level.SEVERE, - "A GeoServer lifecycle handler threw an exception during reload", - t); - } + if (!silent) { + fireBeforeReload(handlers); + } else { + LOGGER.log( + Level.FINE, + "Reloading GeoServer configuration, but not notifying lifecycle beforeReload handlers"); } // perform the reload try { // flush caches - reset(); + reset(silent); // reload configuration synchronized (org.geoserver.config.GeoServer.CONFIGURATION_LOCK) { getCatalog().getResourcePool().dispose(); if (newCatalog != null) { - dispose(); + dispose(silent); // reload catalog, make sure we reload the underlying catalog, not any wrappers Catalog catalog = getCatalog(); @@ -566,15 +612,12 @@ public void reload(Catalog newCatalog) throws Exception { } } finally { // notify end of reload - for (GeoServerLifecycleHandler handler : handlers) { - try { - handler.onReload(); - } catch (Exception t) { - LOGGER.log( - Level.SEVERE, - "A GeoServer lifecycle handler threw an exception during reload", - t); - } + if (!silent) { + fireOnReload(handlers); + } else { + LOGGER.log( + Level.FINE, + "Reloading GeoServer configuration, but not notifying lifecycle onReload handlers"); } } } @@ -591,6 +634,10 @@ private void callGeoServerLoaderReload() throws Exception { @Override public void reset() { + this.reset(false); + } + + public void reset(boolean silent) { // drop all the catalog store/feature types/raster caches catalog.getResourcePool().dispose(); @@ -598,16 +645,14 @@ public void reset() { CRS.reset("all"); // look for pluggable handlers - for (GeoServerLifecycleHandler handler : - GeoServerExtensions.extensions(GeoServerLifecycleHandler.class)) { - try { - handler.onReset(); - } catch (RuntimeException t) { - LOGGER.log( - Level.SEVERE, - "A GeoServer lifecycle handler threw an exception during reset", - t); - } + if (!silent) { + List handlers = + GeoServerExtensions.extensions(GeoServerLifecycleHandler.class); + fireOnReset(handlers); + } else { + LOGGER.log( + Level.FINE, + "Reloading GeoServer configuration, but not notifying lifecycle onReset handlers"); } }