mirror of https://github.com/halo-dev/halo
refactor: add retry operation to single page publishing (#3422)
#### What type of PR is this? /kind improvement /area core /milestone 2.3.x #### What this PR does / why we need it: 修复初始化时自定义页面会发布失败的问题 #### Which issue(s) this PR fixes: Fixes #3279 #### Does this PR introduce a user-facing change? ```release-note None ```pull/3420/head
parent
3146589d25
commit
aba151f54c
|
@ -214,19 +214,20 @@ public class PostEndpoint implements CustomEndpoint {
|
|||
boolean asyncPublish = request.queryParam("async")
|
||||
.map(Boolean::parseBoolean)
|
||||
.orElse(false);
|
||||
return client.get(Post.class, name)
|
||||
.doOnNext(post -> {
|
||||
var spec = post.getSpec();
|
||||
request.queryParam("headSnapshot").ifPresent(spec::setHeadSnapshot);
|
||||
spec.setPublish(true);
|
||||
if (spec.getHeadSnapshot() == null) {
|
||||
spec.setHeadSnapshot(spec.getBaseSnapshot());
|
||||
}
|
||||
// TODO Provide release snapshot query param to control
|
||||
spec.setReleaseSnapshot(spec.getHeadSnapshot());
|
||||
})
|
||||
.flatMap(client::update)
|
||||
.retryWhen(Retry.backoff(3, Duration.ofMillis(100))
|
||||
return Mono.defer(() -> client.get(Post.class, name)
|
||||
.doOnNext(post -> {
|
||||
var spec = post.getSpec();
|
||||
request.queryParam("headSnapshot").ifPresent(spec::setHeadSnapshot);
|
||||
spec.setPublish(true);
|
||||
if (spec.getHeadSnapshot() == null) {
|
||||
spec.setHeadSnapshot(spec.getBaseSnapshot());
|
||||
}
|
||||
// TODO Provide release snapshot query param to control
|
||||
spec.setReleaseSnapshot(spec.getHeadSnapshot());
|
||||
})
|
||||
.flatMap(client::update)
|
||||
)
|
||||
.retryWhen(Retry.backoff(5, Duration.ofMillis(100))
|
||||
.filter(t -> t instanceof OptimisticLockingFailureException))
|
||||
.flatMap(post -> {
|
||||
if (asyncPublish) {
|
||||
|
@ -243,7 +244,7 @@ public class PostEndpoint implements CustomEndpoint {
|
|||
}
|
||||
throw new RetryException("Post publishing status is not as expected");
|
||||
})
|
||||
.retryWhen(Retry.fixedDelay(10, Duration.ofMillis(100))
|
||||
.retryWhen(Retry.fixedDelay(10, Duration.ofMillis(200))
|
||||
.filter(t -> t instanceof RetryException))
|
||||
.doOnError(IllegalStateException.class, err -> {
|
||||
log.error("Failed to publish post [{}]", name, err);
|
||||
|
|
|
@ -11,6 +11,7 @@ import lombok.AllArgsConstructor;
|
|||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springdoc.core.fn.builders.schema.Builder;
|
||||
import org.springdoc.webflux.core.fn.SpringdocRouteBuilder;
|
||||
import org.springframework.dao.OptimisticLockingFailureException;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.retry.RetryException;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
@ -189,16 +190,19 @@ public class SinglePageEndpoint implements CustomEndpoint {
|
|||
boolean asyncPublish = request.queryParam("async")
|
||||
.map(Boolean::parseBoolean)
|
||||
.orElse(false);
|
||||
return client.fetch(SinglePage.class, name)
|
||||
.flatMap(singlePage -> {
|
||||
SinglePage.SinglePageSpec spec = singlePage.getSpec();
|
||||
spec.setPublish(true);
|
||||
if (spec.getHeadSnapshot() == null) {
|
||||
spec.setHeadSnapshot(spec.getBaseSnapshot());
|
||||
}
|
||||
spec.setReleaseSnapshot(spec.getHeadSnapshot());
|
||||
return client.update(singlePage);
|
||||
})
|
||||
return Mono.defer(() -> client.get(SinglePage.class, name)
|
||||
.flatMap(singlePage -> {
|
||||
SinglePage.SinglePageSpec spec = singlePage.getSpec();
|
||||
spec.setPublish(true);
|
||||
if (spec.getHeadSnapshot() == null) {
|
||||
spec.setHeadSnapshot(spec.getBaseSnapshot());
|
||||
}
|
||||
spec.setReleaseSnapshot(spec.getHeadSnapshot());
|
||||
return client.update(singlePage);
|
||||
})
|
||||
)
|
||||
.retryWhen(Retry.backoff(5, Duration.ofMillis(100))
|
||||
.filter(t -> t instanceof OptimisticLockingFailureException))
|
||||
.flatMap(post -> {
|
||||
if (asyncPublish) {
|
||||
return Mono.just(post);
|
||||
|
|
|
@ -2,6 +2,9 @@ package run.halo.app.core.extension.endpoint;
|
|||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
|
@ -11,12 +14,14 @@ import org.mockito.InjectMocks;
|
|||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.context.ApplicationEventPublisher;
|
||||
import org.springframework.dao.OptimisticLockingFailureException;
|
||||
import org.springframework.test.web.reactive.server.WebTestClient;
|
||||
import reactor.core.publisher.Mono;
|
||||
import run.halo.app.content.PostRequest;
|
||||
import run.halo.app.content.PostService;
|
||||
import run.halo.app.content.TestPost;
|
||||
import run.halo.app.core.extension.content.Post;
|
||||
import run.halo.app.extension.Metadata;
|
||||
import run.halo.app.extension.ReactiveExtensionClient;
|
||||
|
||||
/**
|
||||
|
@ -75,6 +80,53 @@ class PostEndpointTest {
|
|||
.value(post -> assertThat(post).isEqualTo(TestPost.postV1()));
|
||||
}
|
||||
|
||||
@Test
|
||||
void publishRetryOnOptimisticLockingFailure() {
|
||||
var post = new Post();
|
||||
post.setMetadata(new Metadata());
|
||||
post.getMetadata().setName("post-1");
|
||||
post.setSpec(new Post.PostSpec());
|
||||
when(client.get(eq(Post.class), eq("post-1"))).thenReturn(Mono.just(post));
|
||||
|
||||
when(client.update(any(Post.class)))
|
||||
.thenReturn(Mono.error(new OptimisticLockingFailureException("fake-error")));
|
||||
|
||||
// Send request
|
||||
webTestClient.put()
|
||||
.uri("/posts/{name}/publish?async=false", "post-1")
|
||||
.exchange()
|
||||
.expectStatus()
|
||||
.is5xxServerError();
|
||||
|
||||
// Verify WebClient retry behavior
|
||||
verify(client, times(6)).get(eq(Post.class), eq("post-1"));
|
||||
verify(client, times(6)).update(any(Post.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void publishSuccess() {
|
||||
var post = new Post();
|
||||
post.setMetadata(new Metadata());
|
||||
post.getMetadata().setName("post-1");
|
||||
post.setSpec(new Post.PostSpec());
|
||||
when(client.get(eq(Post.class), eq("post-1"))).thenReturn(Mono.just(post));
|
||||
when(client.fetch(eq(Post.class), eq("post-1"))).thenReturn(Mono.empty());
|
||||
|
||||
when(client.update(any(Post.class)))
|
||||
.thenReturn(Mono.just(post));
|
||||
|
||||
// Send request
|
||||
webTestClient.put()
|
||||
.uri("/posts/{name}/publish?async=false", "post-1")
|
||||
.exchange()
|
||||
.expectStatus()
|
||||
.is2xxSuccessful();
|
||||
|
||||
// Verify WebClient retry behavior
|
||||
verify(client, times(1)).get(eq(Post.class), eq("post-1"));
|
||||
verify(client, times(1)).update(any(Post.class));
|
||||
}
|
||||
|
||||
PostRequest postRequest(Post post) {
|
||||
return new PostRequest(post, new PostRequest.Content("B", "<p>B</p>", "MARKDOWN"));
|
||||
}
|
||||
|
|
|
@ -0,0 +1,92 @@
|
|||
package run.halo.app.core.extension.endpoint;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
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.dao.OptimisticLockingFailureException;
|
||||
import org.springframework.test.web.reactive.server.WebTestClient;
|
||||
import reactor.core.publisher.Mono;
|
||||
import run.halo.app.core.extension.content.SinglePage;
|
||||
import run.halo.app.extension.Metadata;
|
||||
import run.halo.app.extension.ReactiveExtensionClient;
|
||||
|
||||
/**
|
||||
* Tests for @{@link SinglePageEndpoint}.
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.3.0
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class SinglePageEndpointTest {
|
||||
|
||||
@Mock
|
||||
private ReactiveExtensionClient client;
|
||||
|
||||
@InjectMocks
|
||||
SinglePageEndpoint singlePageEndpoint;
|
||||
|
||||
WebTestClient webTestClient;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
webTestClient = WebTestClient
|
||||
.bindToRouterFunction(singlePageEndpoint.endpoint())
|
||||
.build();
|
||||
}
|
||||
|
||||
@Test
|
||||
void publishRetryOnOptimisticLockingFailure() {
|
||||
var page = new SinglePage();
|
||||
page.setMetadata(new Metadata());
|
||||
page.getMetadata().setName("page-1");
|
||||
page.setSpec(new SinglePage.SinglePageSpec());
|
||||
when(client.get(eq(SinglePage.class), eq("page-1"))).thenReturn(Mono.just(page));
|
||||
|
||||
when(client.update(any(SinglePage.class)))
|
||||
.thenReturn(Mono.error(new OptimisticLockingFailureException("fake-error")));
|
||||
|
||||
// Send request
|
||||
webTestClient.put()
|
||||
.uri("/singlepages/{name}/publish?async=false", "page-1")
|
||||
.exchange()
|
||||
.expectStatus()
|
||||
.is5xxServerError();
|
||||
|
||||
// Verify WebClient retry behavior
|
||||
verify(client, times(6)).get(eq(SinglePage.class), eq("page-1"));
|
||||
verify(client, times(6)).update(any(SinglePage.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void publishSuccess() {
|
||||
var page = new SinglePage();
|
||||
page.setMetadata(new Metadata());
|
||||
page.getMetadata().setName("page-1");
|
||||
page.setSpec(new SinglePage.SinglePageSpec());
|
||||
|
||||
when(client.get(eq(SinglePage.class), eq("page-1"))).thenReturn(Mono.just(page));
|
||||
when(client.fetch(eq(SinglePage.class), eq("page-1"))).thenReturn(Mono.empty());
|
||||
|
||||
when(client.update(any(SinglePage.class))).thenReturn(Mono.just(page));
|
||||
|
||||
// Send request
|
||||
webTestClient.put()
|
||||
.uri("/singlepages/{name}/publish?async=false", "page-1")
|
||||
.exchange()
|
||||
.expectStatus()
|
||||
.is2xxSuccessful();
|
||||
|
||||
// Verify WebClient retry behavior
|
||||
verify(client, times(1)).get(eq(SinglePage.class), eq("page-1"));
|
||||
verify(client, times(1)).update(any(SinglePage.class));
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue