From 7b4dd59c58b0db0367544b9fe5e988dfcf081b9c Mon Sep 17 00:00:00 2001 From: guqing <38999863+guqing@users.noreply.github.com> Date: Mon, 24 Oct 2022 15:44:10 +0800 Subject: [PATCH] refactor: cascade delete snapshots and comments when post or page deleted (#2601) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #### What type of PR is this? /kind bug /area core /milestone 2.0 #### What this PR does / why we need it: 当删除文章或自定义页面时级联删除内容快照和评论及计数器 see also #2602 #### Which issue(s) this PR fixes: Fixes #2599 #### Special notes for your reviewer: how to test it? 1. 新建一篇文章并创建一些评论(需要安装评论插件 [plugin-comment-widget](https://github.com/halo-sigs/plugin-comment-widget)) 2. 逻辑删除文章时评论不会被删除,真实删除则会 3. 新建一个文章,再删除它,然后查询 snaphost 模型看关联文章的数据有没有被删除 4. 自定义页面同上 5. 删除文章或自定义页面时会删除对应的 Counter 记录(`GET /apis/metrics.halo.run/v1alpha1/counters`) /cc @halo-dev/sig-halo #### Does this PR introduce a user-facing change? ```release-note 修复删除文章或自定义页面时没有级联删除内容快照和评论的问题 ``` --- .../app/config/ExtensionConfiguration.java | 10 +++-- .../extension/reconciler/PostReconciler.java | 38 ++++++++++++++---- .../reconciler/SinglePageReconciler.java | 35 ++++++++++++++++- .../halo/app/extension/ExtensionOperator.java | 5 +++ .../run/halo/app/metrics/CounterService.java | 3 ++ .../halo/app/metrics/CounterServiceImpl.java | 29 +++++++++++--- .../reconciler/SinglePageReconcilerTest.java | 7 +++- .../app/extension/ExtensionOperatorTest.java | 16 ++++++++ .../app/metrics/CounterServiceImplTest.java | 39 ++++++++++++++++++- 9 files changed, 161 insertions(+), 21 deletions(-) diff --git a/src/main/java/run/halo/app/config/ExtensionConfiguration.java b/src/main/java/run/halo/app/config/ExtensionConfiguration.java index b147689e2..ee7a32d43 100644 --- a/src/main/java/run/halo/app/config/ExtensionConfiguration.java +++ b/src/main/java/run/halo/app/config/ExtensionConfiguration.java @@ -55,6 +55,7 @@ import run.halo.app.extension.router.ExtensionCompositeRouterFunction; import run.halo.app.infra.ExternalUrlSupplier; import run.halo.app.infra.SystemConfigurableEnvironmentFetcher; import run.halo.app.infra.properties.HaloProperties; +import run.halo.app.metrics.CounterService; import run.halo.app.plugin.ExtensionComponentsFinder; import run.halo.app.plugin.HaloPluginManager; import run.halo.app.plugin.resources.ReverseProxyRouterFunctionRegistry; @@ -147,9 +148,10 @@ public class ExtensionConfiguration { @Bean Controller postController(ExtensionClient client, ContentService contentService, - PostPermalinkPolicy postPermalinkPolicy) { + PostPermalinkPolicy postPermalinkPolicy, CounterService counterService) { return new ControllerBuilder("post-controller", client) - .reconciler(new PostReconciler(client, contentService, postPermalinkPolicy)) + .reconciler(new PostReconciler(client, contentService, postPermalinkPolicy, + counterService)) .extension(new Post()) .build(); } @@ -195,10 +197,10 @@ public class ExtensionConfiguration { @Bean Controller singlePageController(ExtensionClient client, ContentService contentService, - ApplicationContext applicationContext) { + ApplicationContext applicationContext, CounterService counterService) { return new ControllerBuilder("single-page-controller", client) .reconciler(new SinglePageReconciler(client, contentService, - applicationContext) + applicationContext, counterService) ) .extension(new SinglePage()) .build(); diff --git a/src/main/java/run/halo/app/core/extension/reconciler/PostReconciler.java b/src/main/java/run/halo/app/core/extension/reconciler/PostReconciler.java index 6f0247625..56dad9d26 100644 --- a/src/main/java/run/halo/app/core/extension/reconciler/PostReconciler.java +++ b/src/main/java/run/halo/app/core/extension/reconciler/PostReconciler.java @@ -12,13 +12,18 @@ import org.jsoup.Jsoup; import org.springframework.util.Assert; import run.halo.app.content.ContentService; import run.halo.app.content.permalinks.PostPermalinkPolicy; +import run.halo.app.core.extension.Comment; import run.halo.app.core.extension.Post; import run.halo.app.core.extension.Snapshot; import run.halo.app.extension.ExtensionClient; +import run.halo.app.extension.ExtensionOperator; +import run.halo.app.extension.Ref; import run.halo.app.extension.controller.Reconciler; import run.halo.app.infra.Condition; import run.halo.app.infra.ConditionStatus; import run.halo.app.infra.utils.JsonUtils; +import run.halo.app.metrics.CounterService; +import run.halo.app.metrics.MeterUtils; /** *

Reconciler for {@link Post}.

@@ -37,22 +42,29 @@ public class PostReconciler implements Reconciler { private final ExtensionClient client; private final ContentService contentService; private final PostPermalinkPolicy postPermalinkPolicy; + private final CounterService counterService; public PostReconciler(ExtensionClient client, ContentService contentService, - PostPermalinkPolicy postPermalinkPolicy) { + PostPermalinkPolicy postPermalinkPolicy, CounterService counterService) { this.client = client; this.contentService = contentService; this.postPermalinkPolicy = postPermalinkPolicy; + this.counterService = counterService; } @Override public Result reconcile(Request request) { client.fetch(Post.class, request.name()) .ifPresent(post -> { - if (isDeleted(post)) { + if (ExtensionOperator.isDeleted(post)) { cleanUpResourcesAndRemoveFinalizer(request.name()); return; } + if (Objects.equals(true, post.getSpec().getDeleted())) { + // remove permalink from permalink indexer + postPermalinkPolicy.onPermalinkDelete(post); + return; + } addFinalizerIfNecessary(post); reconcileMetadata(request.name()); @@ -198,6 +210,23 @@ public class PostReconciler implements Reconciler { private void cleanUpResources(Post post) { // remove permalink from permalink indexer postPermalinkPolicy.onPermalinkDelete(post); + + // clean up snapshots + Snapshot.SubjectRef subjectRef = + Snapshot.SubjectRef.of(Post.KIND, post.getMetadata().getName()); + client.list(Snapshot.class, + snapshot -> subjectRef.equals(snapshot.getSpec().getSubjectRef()), null) + .forEach(client::delete); + + // clean up comments + Ref ref = Ref.of(post); + client.list(Comment.class, comment -> comment.getSpec().getSubjectRef().equals(ref), + null) + .forEach(client::delete); + + // delete counter + counterService.deleteByName(MeterUtils.nameOf(Post.class, post.getMetadata().getName())) + .block(); } private Map getLabelsOrDefault(Post post) { @@ -224,9 +253,4 @@ public class PostReconciler implements Reconciler { private boolean isPublished(Post post) { return Objects.equals(true, post.getSpec().getPublished()); } - - private boolean isDeleted(Post post) { - return Objects.equals(true, post.getSpec().getDeleted()) - || post.getMetadata().getDeletionTimestamp() != null; - } } diff --git a/src/main/java/run/halo/app/core/extension/reconciler/SinglePageReconciler.java b/src/main/java/run/halo/app/core/extension/reconciler/SinglePageReconciler.java index 197228748..6e51ca0ac 100644 --- a/src/main/java/run/halo/app/core/extension/reconciler/SinglePageReconciler.java +++ b/src/main/java/run/halo/app/core/extension/reconciler/SinglePageReconciler.java @@ -16,16 +16,21 @@ import org.springframework.context.ApplicationContext; import org.springframework.util.Assert; import run.halo.app.content.ContentService; import run.halo.app.content.permalinks.ExtensionLocator; +import run.halo.app.core.extension.Comment; import run.halo.app.core.extension.Post; import run.halo.app.core.extension.SinglePage; import run.halo.app.core.extension.Snapshot; import run.halo.app.extension.ExtensionClient; +import run.halo.app.extension.ExtensionOperator; import run.halo.app.extension.GroupVersionKind; +import run.halo.app.extension.Ref; import run.halo.app.extension.controller.Reconciler; import run.halo.app.infra.Condition; import run.halo.app.infra.ConditionStatus; import run.halo.app.infra.utils.JsonUtils; import run.halo.app.infra.utils.PathUtils; +import run.halo.app.metrics.CounterService; +import run.halo.app.metrics.MeterUtils; import run.halo.app.theme.router.PermalinkIndexAddCommand; import run.halo.app.theme.router.PermalinkIndexDeleteCommand; @@ -47,12 +52,14 @@ public class SinglePageReconciler implements Reconciler { private final ExtensionClient client; private final ContentService contentService; private final ApplicationContext applicationContext; + private final CounterService counterService; public SinglePageReconciler(ExtensionClient client, ContentService contentService, - ApplicationContext applicationContext) { + ApplicationContext applicationContext, CounterService counterService) { this.client = client; this.contentService = contentService; this.applicationContext = applicationContext; + this.counterService = counterService; } @Override @@ -60,10 +67,16 @@ public class SinglePageReconciler implements Reconciler { client.fetch(SinglePage.class, request.name()) .ifPresent(singlePage -> { SinglePage oldPage = JsonUtils.deepCopy(singlePage); - if (isDeleted(oldPage)) { + if (ExtensionOperator.isDeleted(singlePage)) { cleanUpResourcesAndRemoveFinalizer(request.name()); return; } + + if (Objects.equals(true, singlePage.getSpec().getDeleted())) { + // remove permalink from permalink indexer + permalinkOnDelete(singlePage); + return; + } addFinalizerIfNecessary(oldPage); reconcileStatus(request.name()); @@ -92,6 +105,24 @@ public class SinglePageReconciler implements Reconciler { private void cleanUpResources(SinglePage singlePage) { // remove permalink from permalink indexer permalinkOnDelete(singlePage); + + // clean up snapshot + Snapshot.SubjectRef subjectRef = + Snapshot.SubjectRef.of(SinglePage.KIND, singlePage.getMetadata().getName()); + client.list(Snapshot.class, + snapshot -> subjectRef.equals(snapshot.getSpec().getSubjectRef()), null) + .forEach(client::delete); + + // clean up comments + Ref ref = Ref.of(singlePage); + client.list(Comment.class, comment -> comment.getSpec().getSubjectRef().equals(ref), + null) + .forEach(client::delete); + + // delete counter for single page + counterService.deleteByName( + MeterUtils.nameOf(SinglePage.class, singlePage.getMetadata().getName())) + .block(); } private void cleanUpResourcesAndRemoveFinalizer(String pageName) { diff --git a/src/main/java/run/halo/app/extension/ExtensionOperator.java b/src/main/java/run/halo/app/extension/ExtensionOperator.java index 49005dcc1..75b361f21 100644 --- a/src/main/java/run/halo/app/extension/ExtensionOperator.java +++ b/src/main/java/run/halo/app/extension/ExtensionOperator.java @@ -95,4 +95,9 @@ public interface ExtensionOperator { static Predicate isNotDeleted() { return ext -> ext.getMetadata().getDeletionTimestamp() == null; } + + static boolean isDeleted(ExtensionOperator extension) { + return extension.getMetadata() != null + && extension.getMetadata().getDeletionTimestamp() != null; + } } diff --git a/src/main/java/run/halo/app/metrics/CounterService.java b/src/main/java/run/halo/app/metrics/CounterService.java index f1fb9c12c..3fb70acd9 100644 --- a/src/main/java/run/halo/app/metrics/CounterService.java +++ b/src/main/java/run/halo/app/metrics/CounterService.java @@ -1,5 +1,6 @@ package run.halo.app.metrics; +import reactor.core.publisher.Mono; import run.halo.app.core.extension.Counter; /** @@ -9,4 +10,6 @@ import run.halo.app.core.extension.Counter; public interface CounterService { Counter getByName(String counterName); + + Mono deleteByName(String counterName); } diff --git a/src/main/java/run/halo/app/metrics/CounterServiceImpl.java b/src/main/java/run/halo/app/metrics/CounterServiceImpl.java index d9d44a295..f391bfc8c 100644 --- a/src/main/java/run/halo/app/metrics/CounterServiceImpl.java +++ b/src/main/java/run/halo/app/metrics/CounterServiceImpl.java @@ -4,8 +4,10 @@ import io.micrometer.core.instrument.MeterRegistry; import io.micrometer.core.instrument.Tag; import java.util.Collection; import org.springframework.stereotype.Service; +import reactor.core.publisher.Mono; import run.halo.app.core.extension.Counter; import run.halo.app.extension.Metadata; +import run.halo.app.extension.ReactiveExtensionClient; /** * Counter service implementation. @@ -17,24 +19,39 @@ import run.halo.app.extension.Metadata; public class CounterServiceImpl implements CounterService { private final MeterRegistry meterRegistry; + private final ReactiveExtensionClient client; - public CounterServiceImpl(MeterRegistry meterRegistry) { + public CounterServiceImpl(MeterRegistry meterRegistry, ReactiveExtensionClient client) { this.meterRegistry = meterRegistry; + this.client = client; } @Override public Counter getByName(String counterName) { - Tag commonTag = MeterUtils.METRICS_COMMON_TAG; - Collection counters = meterRegistry.find(counterName) - .tag(commonTag.getKey(), - valueMatch -> commonTag.getValue().equals(valueMatch)) - .counters(); + Collection counters = + findCounters(counterName); Counter counter = emptyCounter(counterName); counter.populateFrom(counters); return counter; } + private Collection findCounters(String counterName) { + Tag commonTag = MeterUtils.METRICS_COMMON_TAG; + return meterRegistry.find(counterName) + .tag(commonTag.getKey(), + valueMatch -> commonTag.getValue().equals(valueMatch)) + .counters(); + } + + @Override + public Mono deleteByName(String counterName) { + return client.fetch(Counter.class, counterName) + .flatMap(counter -> client.delete(counter) + .doOnNext(deleted -> findCounters(counterName).forEach(meterRegistry::remove)) + .thenReturn(counter)); + } + private Counter emptyCounter(String name) { Counter counter = new Counter(); counter.setMetadata(new Metadata()); diff --git a/src/test/java/run/halo/app/core/extension/reconciler/SinglePageReconcilerTest.java b/src/test/java/run/halo/app/core/extension/reconciler/SinglePageReconcilerTest.java index a9a3a5949..dee2d22e5 100644 --- a/src/test/java/run/halo/app/core/extension/reconciler/SinglePageReconcilerTest.java +++ b/src/test/java/run/halo/app/core/extension/reconciler/SinglePageReconcilerTest.java @@ -30,6 +30,7 @@ import run.halo.app.core.extension.Snapshot; import run.halo.app.extension.ExtensionClient; import run.halo.app.extension.Metadata; import run.halo.app.extension.controller.Reconciler; +import run.halo.app.metrics.CounterService; import run.halo.app.theme.router.PermalinkIndexAddCommand; import run.halo.app.theme.router.PermalinkIndexDeleteCommand; import run.halo.app.theme.router.PermalinkIndexUpdateCommand; @@ -50,11 +51,15 @@ class SinglePageReconcilerTest { @Mock private ApplicationContext applicationContext; + @Mock + private CounterService counterService; + private SinglePageReconciler singlePageReconciler; @BeforeEach void setUp() { - singlePageReconciler = new SinglePageReconciler(client, contentService, applicationContext); + singlePageReconciler = new SinglePageReconciler(client, contentService, applicationContext, + counterService); } @Test diff --git a/src/test/java/run/halo/app/extension/ExtensionOperatorTest.java b/src/test/java/run/halo/app/extension/ExtensionOperatorTest.java index 08534e89e..bdb20f1a5 100644 --- a/src/test/java/run/halo/app/extension/ExtensionOperatorTest.java +++ b/src/test/java/run/halo/app/extension/ExtensionOperatorTest.java @@ -22,4 +22,20 @@ class ExtensionOperatorTest { when(metadata.getDeletionTimestamp()).thenReturn(Instant.now()); assertFalse(ExtensionOperator.isNotDeleted().test(ext)); } + + @Test + void testIsDeleted() { + var ext = mock(ExtensionOperator.class); + + when(ext.getMetadata()).thenReturn(null); + assertFalse(ExtensionOperator.isDeleted(ext)); + + var metadata = mock(Metadata.class); + when(ext.getMetadata()).thenReturn(metadata); + when(metadata.getDeletionTimestamp()).thenReturn(null); + assertFalse(ExtensionOperator.isDeleted(ext)); + + when(metadata.getDeletionTimestamp()).thenReturn(Instant.now()); + assertTrue(ExtensionOperator.isDeleted(ext)); + } } \ No newline at end of file diff --git a/src/test/java/run/halo/app/metrics/CounterServiceImplTest.java b/src/test/java/run/halo/app/metrics/CounterServiceImplTest.java index e5b12343f..9e3ea80d1 100644 --- a/src/test/java/run/halo/app/metrics/CounterServiceImplTest.java +++ b/src/test/java/run/halo/app/metrics/CounterServiceImplTest.java @@ -1,11 +1,24 @@ package run.halo.app.metrics; 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.lenient; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; import io.micrometer.core.instrument.simple.SimpleMeterRegistry; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; +import run.halo.app.core.extension.Counter; import run.halo.app.core.extension.Post; +import run.halo.app.extension.Metadata; +import run.halo.app.extension.ReactiveExtensionClient; /** * Tests for {@link CounterServiceImpl}. @@ -13,20 +26,32 @@ import run.halo.app.core.extension.Post; * @author guqing * @since 2.0.0 */ +@ExtendWith(MockitoExtension.class) class CounterServiceImplTest { + @Mock + private ReactiveExtensionClient client; private CounterServiceImpl counterService; @BeforeEach void setUp() { SimpleMeterRegistry simpleMeterRegistry = new SimpleMeterRegistry(); - counterService = new CounterServiceImpl(simpleMeterRegistry); + counterService = new CounterServiceImpl(simpleMeterRegistry, client); String counterName = MeterUtils.nameOf(Post.class, "fake-post"); MeterUtils.visitCounter(simpleMeterRegistry, counterName).increment(); MeterUtils.approvedCommentCounter(simpleMeterRegistry, counterName) .increment(2); + + Counter counter = new Counter(); + counter.setMetadata(new Metadata()); + counter.getMetadata().setName(counterName); + counter.setVisit(2); + + lenient().when(client.delete(any())).thenReturn(Mono.just(counter)); + lenient().when(client.fetch(eq(Counter.class), eq(counterName))) + .thenReturn(Mono.just(counter)); } @Test @@ -38,4 +63,16 @@ class CounterServiceImplTest { assertThat(counter.getTotalComment()).isEqualTo(0); assertThat(counter.getApprovedComment()).isEqualTo(2); } + + @Test + void deleteByName() { + String counterName = MeterUtils.nameOf(Post.class, "fake-post"); + counterService.deleteByName(counterName) + .as(StepVerifier::create) + .consumeNextWith(counter -> { + verify(client, times(1)).delete(any(Counter.class)); + assertThat(counterService.getByName(counterName).getVisit()).isEqualTo(0); + }) + .verifyComplete(); + } } \ No newline at end of file