Initialize schemes before refreshing application context (#5032)

#### What type of PR is this?

/kind improvement
/area core

#### What this PR does / why we need it:

Prior to this proposal, we encountered an error requesting any page before Halo is in ready state. That is because the timing of schemes initialization is incorrect.

The current proposal is to advance schemes initialization before refreshing application and removes `SchemeInitializedEvent` because it cannot be listened by other beans.

#### Which issue(s) this PR fixes:

Fixes https://github.com/halo-dev/halo/issues/4946

#### Special notes for your reviewer:

#### Does this PR introduce a user-facing change?

```release-note
修复 Halo 还未处于准备就绪时访问页面或接口出现“Scheme not found”错误的问题
```
pull/5692/head^2
John Niang 2023-12-13 14:40:10 +08:00 committed by GitHub
parent 8b405faa57
commit d777dbf7ed
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 98 additions and 97 deletions

View File

@ -5,8 +5,6 @@ import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.ServerResponse;
import run.halo.app.extension.DefaultSchemeManager;
import run.halo.app.extension.DefaultSchemeWatcherManager;
import run.halo.app.extension.ExtensionClient;
import run.halo.app.extension.ReactiveExtensionClient;
import run.halo.app.extension.SchemeManager;
@ -19,18 +17,8 @@ public class ExtensionConfiguration {
@Bean
RouterFunction<ServerResponse> extensionsRouterFunction(ReactiveExtensionClient client,
SchemeWatcherManager watcherManager) {
return new ExtensionCompositeRouterFunction(client, watcherManager);
}
@Bean
SchemeManager schemeManager(SchemeWatcherManager watcherManager) {
return new DefaultSchemeManager(watcherManager);
}
@Bean
SchemeWatcherManager schemeWatcherManager() {
return new DefaultSchemeWatcherManager();
SchemeWatcherManager watcherManager, SchemeManager schemeManager) {
return new ExtensionCompositeRouterFunction(client, watcherManager, schemeManager);
}
@Configuration(proxyBeanMethods = false)

View File

@ -7,6 +7,7 @@ import java.util.Set;
import java.util.concurrent.locks.StampedLock;
import java.util.function.BiConsumer;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.boot.context.event.ApplicationStartedEvent;
import org.springframework.context.ApplicationListener;
import org.springframework.lang.NonNull;
import org.springframework.stereotype.Component;
@ -19,7 +20,6 @@ import run.halo.app.extension.MetadataUtil;
import run.halo.app.extension.Unstructured;
import run.halo.app.extension.Watcher;
import run.halo.app.extension.controller.RequestSynchronizer;
import run.halo.app.infra.SchemeInitializedEvent;
/**
* <p>Monitor changes to {@link Post} resources and establish a local, in-memory cache in an
@ -33,7 +33,7 @@ import run.halo.app.infra.SchemeInitializedEvent;
* @since 2.0.0
*/
@Component
public class PostIndexInformer implements ApplicationListener<SchemeInitializedEvent>,
public class PostIndexInformer implements ApplicationListener<ApplicationStartedEvent>,
DisposableBean {
public static final String TAG_POST_INDEXER = "tag-post-indexer";
public static final String LABEL_INDEXER_NAME = "post-label-indexer";
@ -71,10 +71,6 @@ public class PostIndexInformer implements ApplicationListener<SchemeInitializedE
};
}
public Set<String> getByIndex(String indexName, String indexKey) {
return postIndexer.getByIndex(indexName, indexKey);
}
public Set<String> getByTagName(String tagName) {
return postIndexer.getByIndex(TAG_POST_INDEXER, tagName);
}
@ -104,10 +100,6 @@ public class PostIndexInformer implements ApplicationListener<SchemeInitializedE
return labelName + "=" + labelValue;
}
public Set<String> getByLabel(String labelName, String labelValue) {
return postIndexer.getByIndex(LABEL_INDEXER_NAME, labelKey(labelName, labelValue));
}
@Override
public void destroy() throws Exception {
if (postWatcher != null) {
@ -119,7 +111,7 @@ public class PostIndexInformer implements ApplicationListener<SchemeInitializedE
}
@Override
public void onApplicationEvent(@NonNull SchemeInitializedEvent event) {
public void onApplicationEvent(@NonNull ApplicationStartedEvent event) {
if (!synchronizer.isStarted()) {
synchronizer.start();
}

View File

@ -3,6 +3,9 @@ package run.halo.app.extension.router;
import java.util.Collections;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.boot.context.event.ApplicationStartedEvent;
import org.springframework.context.ApplicationListener;
import org.springframework.lang.NonNull;
import org.springframework.web.reactive.function.server.HandlerFunction;
import org.springframework.web.reactive.function.server.RouterFunction;
@ -13,23 +16,31 @@ import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import run.halo.app.extension.ReactiveExtensionClient;
import run.halo.app.extension.Scheme;
import run.halo.app.extension.SchemeManager;
import run.halo.app.extension.SchemeWatcherManager;
import run.halo.app.extension.SchemeWatcherManager.SchemeWatcher;
public class ExtensionCompositeRouterFunction implements
RouterFunction<ServerResponse>, SchemeWatcher {
RouterFunction<ServerResponse>,
SchemeWatcher,
InitializingBean,
ApplicationListener<ApplicationStartedEvent> {
private final Map<Scheme, RouterFunction<ServerResponse>> schemeRouterFuncMapper;
private final ReactiveExtensionClient client;
private final SchemeManager schemeManager;
private final SchemeWatcherManager watcherManager;
public ExtensionCompositeRouterFunction(ReactiveExtensionClient client,
SchemeWatcherManager watcherManager) {
SchemeWatcherManager watcherManager,
SchemeManager schemeManager) {
this.client = client;
this.schemeManager = schemeManager;
this.watcherManager = watcherManager;
schemeRouterFuncMapper = new ConcurrentHashMap<>();
if (watcherManager != null) {
watcherManager.register(this);
}
}
@Override
@ -60,4 +71,17 @@ public class ExtensionCompositeRouterFunction implements
this.schemeRouterFuncMapper.remove(unregisteredEvent.getDeletedScheme());
}
}
@Override
public void onApplicationEvent(ApplicationStartedEvent event) {
schemeManager.schemes().forEach(scheme -> {
var factory = new ExtensionRouterFunctionFactory(scheme, client);
this.schemeRouterFuncMapper.put(scheme, factory.create());
});
}
@Override
public void afterPropertiesSet() {
watcherManager.register(this);
}
}

View File

@ -2,6 +2,7 @@ package run.halo.app.infra;
import java.io.IOException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.context.event.ApplicationStartedEvent;
import org.springframework.context.ApplicationListener;
import org.springframework.core.io.UrlResource;
import org.springframework.core.io.buffer.DataBufferUtils;
@ -16,7 +17,7 @@ import run.halo.app.infra.utils.FileUtils;
@Slf4j
@Component
public class DefaultThemeInitializer implements ApplicationListener<SchemeInitializedEvent> {
public class DefaultThemeInitializer implements ApplicationListener<ApplicationStartedEvent> {
private final ThemeService themeService;
@ -32,7 +33,7 @@ public class DefaultThemeInitializer implements ApplicationListener<SchemeInitia
}
@Override
public void onApplicationEvent(SchemeInitializedEvent event) {
public void onApplicationEvent(ApplicationStartedEvent event) {
if (themeProps.getInitializer().isDisabled()) {
log.debug("Skipped initializing default theme due to disabled");
return;

View File

@ -1,12 +1,14 @@
package run.halo.app.infra;
import java.io.IOException;
import java.time.Duration;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.context.event.ApplicationStartedEvent;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.event.EventListener;
import org.springframework.context.ApplicationListener;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.stereotype.Component;
@ -28,7 +30,7 @@ import run.halo.app.infra.utils.YamlUnstructuredLoader;
*/
@Slf4j
@Component
public class ExtensionResourceInitializer {
public class ExtensionResourceInitializer implements ApplicationListener<ApplicationStartedEvent> {
public static final Set<String> REQUIRED_EXTENSION_LOCATIONS =
Set.of("classpath:/extensions/*.yaml", "classpath:/extensions/*.yml");
@ -45,8 +47,7 @@ public class ExtensionResourceInitializer {
this.eventPublisher = eventPublisher;
}
@EventListener(SchemeInitializedEvent.class)
public Mono<Void> initialize(SchemeInitializedEvent initializedEvent) {
public void onApplicationEvent(ApplicationStartedEvent initializedEvent) {
var locations = new HashSet<String>();
if (!haloProperties.isRequiredExtensionDisabled()) {
locations.addAll(REQUIRED_EXTENSION_LOCATIONS);
@ -55,10 +56,10 @@ public class ExtensionResourceInitializer {
locations.addAll(haloProperties.getInitialExtensionLocations());
}
if (CollectionUtils.isEmpty(locations)) {
return Mono.empty();
return;
}
return Flux.fromIterable(locations)
Flux.fromIterable(locations)
.doOnNext(location ->
log.debug("Trying to initialize extension resources from location: {}", location))
.map(this::listResources)
@ -82,7 +83,8 @@ public class ExtensionResourceInitializer {
}
})
.then(Mono.fromRunnable(
() -> eventPublisher.publishEvent(new ExtensionInitializedEvent(this))));
() -> eventPublisher.publishEvent(new ExtensionInitializedEvent(this))))
.block(Duration.ofMinutes(1));
}
private Mono<Unstructured> createOrUpdate(Unstructured extension) {

View File

@ -1,11 +0,0 @@
package run.halo.app.infra;
import org.springframework.context.ApplicationEvent;
public class SchemeInitializedEvent extends ApplicationEvent {
public SchemeInitializedEvent(Object source) {
super(source);
}
}

View File

@ -1,7 +1,6 @@
package run.halo.app.infra;
import org.springframework.boot.context.event.ApplicationStartedEvent;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.boot.context.event.ApplicationContextInitializedEvent;
import org.springframework.context.ApplicationListener;
import org.springframework.lang.NonNull;
import org.springframework.stereotype.Component;
@ -36,7 +35,8 @@ import run.halo.app.core.extension.notification.Reason;
import run.halo.app.core.extension.notification.ReasonType;
import run.halo.app.core.extension.notification.Subscription;
import run.halo.app.extension.ConfigMap;
import run.halo.app.extension.SchemeManager;
import run.halo.app.extension.DefaultSchemeManager;
import run.halo.app.extension.DefaultSchemeWatcherManager;
import run.halo.app.extension.Secret;
import run.halo.app.migration.Backup;
import run.halo.app.plugin.extensionpoint.ExtensionDefinition;
@ -45,20 +45,17 @@ import run.halo.app.search.extension.SearchEngine;
import run.halo.app.security.PersonalAccessToken;
@Component
public class SchemeInitializer implements ApplicationListener<ApplicationStartedEvent> {
private final SchemeManager schemeManager;
private final ApplicationEventPublisher eventPublisher;
public SchemeInitializer(SchemeManager schemeManager,
ApplicationEventPublisher eventPublisher) {
this.schemeManager = schemeManager;
this.eventPublisher = eventPublisher;
}
public class SchemeInitializer implements ApplicationListener<ApplicationContextInitializedEvent> {
@Override
public void onApplicationEvent(@NonNull ApplicationStartedEvent event) {
public void onApplicationEvent(@NonNull ApplicationContextInitializedEvent event) {
var watcherManager = new DefaultSchemeWatcherManager();
var schemeManager = new DefaultSchemeManager(watcherManager);
var beanFactory = event.getApplicationContext().getBeanFactory();
beanFactory.registerSingleton("schemeWatcherManager", watcherManager);
beanFactory.registerSingleton("schemeManager", schemeManager);
schemeManager.register(Role.class);
// plugin.halo.run
@ -108,7 +105,5 @@ public class SchemeInitializer implements ApplicationListener<ApplicationStarted
schemeManager.register(Subscription.class);
schemeManager.register(NotifierDescriptor.class);
schemeManager.register(Notification.class);
eventPublisher.publishEvent(new SchemeInitializedEvent(this));
}
}

View File

@ -1,12 +1,12 @@
package run.halo.app.search;
import java.util.concurrent.CountDownLatch;
import java.time.Duration;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.context.event.ApplicationStartedEvent;
import org.springframework.context.event.EventListener;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
import org.springframework.util.StopWatch;
import run.halo.app.infra.SchemeInitializedEvent;
@Slf4j
@Component
@ -19,17 +19,12 @@ public class IndicesInitializer {
}
@Async
@EventListener(SchemeInitializedEvent.class)
public void whenSchemeInitialized(SchemeInitializedEvent event) throws InterruptedException {
var latch = new CountDownLatch(1);
@EventListener
public void whenSchemeInitialized(ApplicationStartedEvent event) {
log.info("Initialize post indices...");
var watch = new StopWatch("PostIndicesWatch");
watch.start("rebuild");
indicesService.rebuildPostIndices()
.doFinally(signalType -> latch.countDown())
.subscribe();
latch.await();
watch.stop();
indicesService.rebuildPostIndices().block(Duration.ofMinutes(5));
log.info("Initialized post indices. Usage: {}", watch);
}

View File

@ -4,7 +4,7 @@ import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor;
import org.springframework.context.ApplicationEvent;
import org.springframework.boot.context.event.ApplicationStartedEvent;
import org.springframework.context.event.EventListener;
import org.springframework.lang.NonNull;
import org.springframework.stereotype.Component;
@ -15,7 +15,6 @@ import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.reactive.function.server.ServerResponse;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import run.halo.app.infra.SchemeInitializedEvent;
import run.halo.app.infra.SystemConfigurableEnvironmentFetcher;
import run.halo.app.infra.SystemSetting;
import run.halo.app.theme.DefaultTemplateEnum;
@ -90,10 +89,15 @@ public class ThemeCompositeRouterFunction implements RouterFunction<ServerRespon
/**
* Refresh the {@link #cachedRouters} when the permalink rule is changed.
*
* @param event {@link SchemeInitializedEvent} or {@link PermalinkRuleChangedEvent}
* @param event {@link PermalinkRuleChangedEvent}
*/
@EventListener({SchemeInitializedEvent.class, PermalinkRuleChangedEvent.class})
public void onSchemeInitializedEvent(@NonNull ApplicationEvent event) {
@EventListener
public void onPermalinkRuleChanged(PermalinkRuleChangedEvent event) {
this.cachedRouters = routerFunctions();
}
@EventListener
public void onApplicationStarted(ApplicationStartedEvent event) {
this.cachedRouters = routerFunctions();
}

View File

@ -0,0 +1 @@
org.springframework.context.ApplicationListener=run.halo.app.infra.SchemeInitializer

View File

@ -4,13 +4,14 @@ import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.boot.context.event.ApplicationStartedEvent;
import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
import org.springframework.mock.web.server.MockServerWebExchange;
import org.springframework.web.reactive.function.server.HandlerStrategies;
@ -18,6 +19,7 @@ import org.springframework.web.reactive.function.server.ServerRequest;
import run.halo.app.extension.FakeExtension;
import run.halo.app.extension.ReactiveExtensionClient;
import run.halo.app.extension.Scheme;
import run.halo.app.extension.SchemeManager;
import run.halo.app.extension.SchemeWatcherManager;
import run.halo.app.extension.SchemeWatcherManager.SchemeRegistered;
import run.halo.app.extension.SchemeWatcherManager.SchemeUnregistered;
@ -28,10 +30,17 @@ class ExtensionCompositeRouterFunctionTest {
@Mock
ReactiveExtensionClient client;
@Mock
SchemeManager schemeManager;
@Mock
SchemeWatcherManager watcherManager;
@InjectMocks
ExtensionCompositeRouterFunction extensionRouterFunc;
@Test
void shouldRouteWhenSchemeRegistered() {
var extensionRouterFunc = new ExtensionCompositeRouterFunction(client, null);
var exchange = MockServerWebExchange.from(
MockServerHttpRequest.get("/apis/fake.halo.run/v1alpha1/fakes").build());
@ -51,8 +60,6 @@ class ExtensionCompositeRouterFunctionTest {
@Test
void shouldNotRouteWhenSchemeUnregistered() {
var extensionRouterFunc = new ExtensionCompositeRouterFunction(client, null);
var exchange = MockServerWebExchange.from(
MockServerHttpRequest.get("/apis/fake.halo.run/v1alpha1/fakes").build());
@ -74,10 +81,16 @@ class ExtensionCompositeRouterFunctionTest {
}
@Test
void shouldRegisterWatcherIfWatcherManagerIsNotNull() {
var watcherManager = mock(SchemeWatcherManager.class);
var routerFunction = new ExtensionCompositeRouterFunction(client, watcherManager);
verify(watcherManager, times(1)).register(eq(routerFunction));
void shouldRegisterWatcherAfterPropertiesSet() {
extensionRouterFunc.afterPropertiesSet();
verify(watcherManager).register(eq(extensionRouterFunc));
}
@Test
void shouldBuildRouterFunctionsOnApplicationStarted() {
var applicationStartedEvent = mock(ApplicationStartedEvent.class);
extensionRouterFunc.onApplicationEvent(applicationStartedEvent);
verify(schemeManager).schemes();
}
}

View File

@ -23,10 +23,10 @@ import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.skyscreamer.jsonassert.JSONAssert;
import org.springframework.boot.context.event.ApplicationStartedEvent;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.util.FileSystemUtils;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import run.halo.app.extension.GroupVersionKind;
import run.halo.app.extension.ReactiveExtensionClient;
import run.halo.app.extension.Unstructured;
@ -47,7 +47,7 @@ class ExtensionResourceInitializerTest {
@Mock
HaloProperties haloProperties;
@Mock
SchemeInitializedEvent applicationReadyEvent;
ApplicationStartedEvent applicationStartedEvent;
@Mock
ApplicationEventPublisher eventPublisher;
@ -128,10 +128,7 @@ class ExtensionResourceInitializerTest {
.thenReturn(Mono.empty());
when(extensionClient.create(any())).thenReturn(Mono.empty());
var initializeMono = extensionResourceInitializer.initialize(applicationReadyEvent);
StepVerifier.create(initializeMono)
.verifyComplete();
extensionResourceInitializer.onApplicationEvent(applicationStartedEvent);
verify(extensionClient, times(3)).create(argumentCaptor.capture());