refactor: simplify the code of reconciler for comment and optimize performance (#5504)

#### What type of PR is this?
/kind improvement
/area core
/milestone 2.14.x

#### What this PR does / why we need it:
优化评论控制器的实现逻辑以优化代码和性能

Resolves #5435 

how to test it?
- 测试删除评论时能正确连同回复一起删除
- 测试评论下的最新回复的已读功能是否正确
- 删除/审核评论,观察主题端和Console端分别显示的评论数量是否正确

#### Does this PR introduce a user-facing change?
```release-note
None
```
pull/5516/head
guqing 2024-03-15 23:58:09 +08:00 committed by GitHub
parent 0435e42123
commit 7c3f8b9be2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 225 additions and 239 deletions

View File

@ -1,8 +1,10 @@
package run.halo.app.content.comment;
import org.springframework.lang.NonNull;
import reactor.core.publisher.Mono;
import run.halo.app.core.extension.content.Comment;
import run.halo.app.extension.ListResult;
import run.halo.app.extension.Ref;
/**
* An application service for {@link Comment}.
@ -15,4 +17,6 @@ public interface CommentService {
Mono<ListResult<ListedComment>> listComment(CommentQuery query);
Mono<Comment> create(Comment comment);
Mono<Void> removeBySubject(@NonNull Ref subjectRef);
}

View File

@ -1,8 +1,14 @@
package run.halo.app.content.comment;
import static run.halo.app.extension.index.query.QueryFactory.and;
import static run.halo.app.extension.index.query.QueryFactory.equal;
import static run.halo.app.extension.index.query.QueryFactory.isNull;
import java.time.Instant;
import java.util.function.Function;
import org.apache.commons.lang3.BooleanUtils;
import org.springframework.data.domain.Sort;
import org.springframework.lang.NonNull;
import org.springframework.security.core.context.ReactiveSecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.util.Assert;
@ -12,9 +18,13 @@ import run.halo.app.core.extension.User;
import run.halo.app.core.extension.content.Comment;
import run.halo.app.core.extension.service.UserService;
import run.halo.app.extension.Extension;
import run.halo.app.extension.ListOptions;
import run.halo.app.extension.ListResult;
import run.halo.app.extension.PageRequest;
import run.halo.app.extension.PageRequestImpl;
import run.halo.app.extension.ReactiveExtensionClient;
import run.halo.app.extension.Ref;
import run.halo.app.extension.router.selector.FieldSelector;
import run.halo.app.infra.SystemConfigurableEnvironmentFetcher;
import run.halo.app.infra.exception.AccessDeniedException;
import run.halo.app.metrics.CounterService;
@ -114,6 +124,31 @@ public class CommentServiceImpl implements CommentService {
.flatMap(client::create);
}
@Override
public Mono<Void> removeBySubject(@NonNull Ref subjectRef) {
Assert.notNull(subjectRef, "The subjectRef must not be null.");
// ascending order by creation time and name
var pageRequest = PageRequestImpl.of(1, 200,
Sort.by("metadata.creationTimestamp", "metadata.name"));
return Flux.defer(() -> listCommentsByRef(subjectRef, pageRequest))
.expand(page -> page.hasNext()
? listCommentsByRef(subjectRef, pageRequest)
: Mono.empty()
)
.flatMap(page -> Flux.fromIterable(page.getItems()))
.flatMap(client::delete)
.then();
}
Mono<ListResult<Comment>> listCommentsByRef(Ref subjectRef, PageRequest pageRequest) {
var listOptions = new ListOptions();
listOptions.setFieldSelector(FieldSelector.of(
and(equal("spec.subjectRef", Comment.toSubjectRefKey(subjectRef)),
isNull("metadata.deletionTimestamp"))
));
return client.listBy(Comment.class, listOptions, pageRequest);
}
private boolean checkCommentOwner(Comment comment, Boolean onlySystemUser) {
Comment.CommentOwner owner = comment.getSpec().getOwner();
if (Boolean.TRUE.equals(onlySystemUser)) {

View File

@ -1,9 +1,5 @@
package run.halo.app.content.comment;
import java.time.Instant;
import java.util.Comparator;
import java.util.function.Function;
import org.springframework.util.comparator.Comparators;
import reactor.core.publisher.Mono;
import run.halo.app.core.extension.content.Reply;
import run.halo.app.extension.ListResult;
@ -20,19 +16,5 @@ public interface ReplyService {
Mono<ListResult<ListedReply>> list(ReplyQuery query);
/**
* Ascending order by creation time.
*
* @return reply comparator
*/
static Comparator<Reply> creationTimeAscComparator() {
Function<Reply, Instant> creationTime = reply -> reply.getSpec().getCreationTime();
Function<Reply, Instant> metadataCreationTime =
reply -> reply.getMetadata().getCreationTimestamp();
// ascending order by creation time
// asc nulls high will be placed at the end
return Comparator.comparing(creationTime, Comparators.nullsHigh())
.thenComparing(metadataCreationTime)
.thenComparing(reply -> reply.getMetadata().getName());
}
Mono<Void> removeAllByComment(String commentName);
}

View File

@ -1,5 +1,8 @@
package run.halo.app.content.comment;
import static run.halo.app.extension.index.query.QueryFactory.and;
import static run.halo.app.extension.index.query.QueryFactory.equal;
import static run.halo.app.extension.index.query.QueryFactory.isNull;
import static run.halo.app.extension.router.selector.SelectorUtil.labelAndFieldSelectorToPredicate;
import java.time.Instant;
@ -7,6 +10,7 @@ import java.util.function.Function;
import java.util.function.Predicate;
import lombok.RequiredArgsConstructor;
import org.apache.commons.lang3.BooleanUtils;
import org.springframework.data.domain.Sort;
import org.springframework.security.core.context.ReactiveSecurityContextHolder;
import org.springframework.stereotype.Service;
import org.springframework.util.Assert;
@ -17,8 +21,12 @@ import run.halo.app.core.extension.content.Comment;
import run.halo.app.core.extension.content.Reply;
import run.halo.app.core.extension.service.UserService;
import run.halo.app.extension.Extension;
import run.halo.app.extension.ListOptions;
import run.halo.app.extension.ListResult;
import run.halo.app.extension.PageRequest;
import run.halo.app.extension.PageRequestImpl;
import run.halo.app.extension.ReactiveExtensionClient;
import run.halo.app.extension.router.selector.FieldSelector;
import run.halo.app.metrics.CounterService;
import run.halo.app.metrics.MeterUtils;
@ -89,6 +97,31 @@ public class ReplyServiceImpl implements ReplyService {
);
}
@Override
public Mono<Void> removeAllByComment(String commentName) {
Assert.notNull(commentName, "The commentName must not be null.");
// ascending order by creation time and name
var pageRequest = PageRequestImpl.of(1, 200,
Sort.by("metadata.creationTimestamp", "metadata.name"));
return Flux.defer(() -> listRepliesByComment(commentName, pageRequest))
.expand(page -> page.hasNext()
? listRepliesByComment(commentName, pageRequest)
: Mono.empty()
)
.flatMap(page -> Flux.fromIterable(page.getItems()))
.flatMap(client::delete)
.then();
}
Mono<ListResult<Reply>> listRepliesByComment(String commentName, PageRequest pageRequest) {
var listOptions = new ListOptions();
listOptions.setFieldSelector(FieldSelector.of(
and(equal("spec.commentName", commentName),
isNull("metadata.deletionTimestamp"))
));
return client.listBy(Reply.class, listOptions, pageRequest);
}
private Mono<ListedReply> toListedReply(Reply reply) {
ListedReply.ListedReplyBuilder builder = ListedReply.builder()
.reply(reply);

View File

@ -2,33 +2,39 @@ package run.halo.app.core.extension.reconciler;
import static org.apache.commons.lang3.ObjectUtils.defaultIfNull;
import static run.halo.app.extension.ExtensionUtil.addFinalizers;
import static run.halo.app.extension.ExtensionUtil.isDeleted;
import static run.halo.app.extension.ExtensionUtil.removeFinalizers;
import static run.halo.app.extension.index.query.QueryFactory.and;
import static run.halo.app.extension.index.query.QueryFactory.equal;
import static run.halo.app.extension.index.query.QueryFactory.isNull;
import java.time.Instant;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import lombok.RequiredArgsConstructor;
import org.apache.commons.lang3.BooleanUtils;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.lang.Nullable;
import org.springframework.lang.NonNull;
import org.springframework.stereotype.Component;
import run.halo.app.content.comment.ReplyNotificationSubscriptionHelper;
import run.halo.app.content.comment.ReplyService;
import run.halo.app.core.extension.Counter;
import run.halo.app.core.extension.content.Comment;
import run.halo.app.core.extension.content.Constant;
import run.halo.app.core.extension.content.Reply;
import run.halo.app.event.post.CommentCreatedEvent;
import run.halo.app.event.post.CommentUnreadReplyCountChangedEvent;
import run.halo.app.extension.ExtensionClient;
import run.halo.app.extension.GroupVersionKind;
import run.halo.app.extension.ListOptions;
import run.halo.app.extension.MetadataUtil;
import run.halo.app.extension.PageRequestImpl;
import run.halo.app.extension.Ref;
import run.halo.app.extension.SchemeManager;
import run.halo.app.extension.controller.Controller;
import run.halo.app.extension.controller.ControllerBuilder;
import run.halo.app.extension.controller.Reconciler;
import run.halo.app.infra.utils.JsonUtils;
import run.halo.app.extension.index.query.Query;
import run.halo.app.extension.router.selector.FieldSelector;
import run.halo.app.metrics.MeterUtils;
/**
@ -43,6 +49,7 @@ public class CommentReconciler implements Reconciler<Reconciler.Request> {
public static final String FINALIZER_NAME = "comment-protection";
private final ExtensionClient client;
private final SchemeManager schemeManager;
private final ReplyService replyService;
private final ApplicationEventPublisher eventPublisher;
private final ReplyNotificationSubscriptionHelper replyNotificationSubscriptionHelper;
@ -52,7 +59,10 @@ public class CommentReconciler implements Reconciler<Reconciler.Request> {
client.fetch(Comment.class, request.name())
.ifPresent(comment -> {
if (isDeleted(comment)) {
cleanUpResourcesAndRemoveFinalizer(request.name());
if (removeFinalizers(comment.getMetadata(), Set.of(FINALIZER_NAME))) {
cleanUpResources(comment);
client.update(comment);
}
return;
}
if (addFinalizers(comment.getMetadata(), Set.of(FINALIZER_NAME))) {
@ -62,9 +72,14 @@ public class CommentReconciler implements Reconciler<Reconciler.Request> {
replyNotificationSubscriptionHelper.subscribeNewReplyReasonForComment(comment);
compatibleCreationTime(request.name());
reconcileStatus(request.name());
updateSameSubjectRefCommentCounter(comment.getSpec().getSubjectRef());
compatibleCreationTime(comment);
Comment.CommentStatus status = comment.getStatusOrDefault();
status.setHasNewReply(defaultIfNull(status.getUnreadReplyCount(), 0) > 0);
updateUnReplyCountIfNecessary(comment);
updateSameSubjectRefCommentCounter(comment);
client.update(comment);
});
return new Result(false, null);
}
@ -79,39 +94,14 @@ public class CommentReconciler implements Reconciler<Reconciler.Request> {
/**
* If the comment creation time is null, set it to the approved time or the current time.
* TODO remove this method in the future and fill in attributes in hook mode instead.
*
* @param name comment name
*/
void compatibleCreationTime(String name) {
client.fetch(Comment.class, name).ifPresent(comment -> {
Instant creationTime = comment.getSpec().getCreationTime();
Instant oldCreationTime =
creationTime == null ? null : Instant.ofEpochMilli(creationTime.toEpochMilli());
if (creationTime == null) {
creationTime = defaultIfNull(comment.getSpec().getApprovedTime(), Instant.now());
comment.getSpec().setCreationTime(creationTime);
}
if (!Objects.equals(oldCreationTime, comment.getSpec().getCreationTime())) {
client.update(comment);
}
});
}
private boolean isDeleted(Comment comment) {
return comment.getMetadata().getDeletionTimestamp() != null;
}
private void reconcileStatus(String name) {
client.fetch(Comment.class, name).ifPresent(comment -> {
Comment oldComment = JsonUtils.deepCopy(comment);
Comment.CommentStatus status = comment.getStatusOrDefault();
status.setHasNewReply(defaultIfNull(status.getUnreadReplyCount(), 0) > 0);
updateUnReplyCountIfNecessary(comment);
if (!oldComment.equals(comment)) {
client.update(comment);
}
});
void compatibleCreationTime(Comment comment) {
var creationTime = comment.getSpec().getCreationTime();
if (creationTime == null) {
creationTime = defaultIfNull(comment.getSpec().getApprovedTime(),
comment.getMetadata().getCreationTimestamp());
}
comment.getSpec().setCreationTime(creationTime);
}
private void updateUnReplyCountIfNecessary(Comment comment) {
@ -121,80 +111,71 @@ public class CommentReconciler implements Reconciler<Reconciler.Request> {
if (lastReadTime != null && lastReadTime.toString().equals(lastReadTimeAnno)) {
return;
}
// spec.lastReadTime is null or not equal to annotation.lastReadTime
// delegate to other handler though event
String commentName = comment.getMetadata().getName();
List<Reply> replies = client.list(Reply.class,
reply -> commentName.equals(reply.getSpec().getCommentName())
&& reply.getMetadata().getDeletionTimestamp() == null,
ReplyService.creationTimeAscComparator());
// calculate unread reply count
comment.getStatusOrDefault()
.setUnreadReplyCount(Comment.getUnreadReplyCount(replies, lastReadTime));
eventPublisher.publishEvent(new CommentUnreadReplyCountChangedEvent(this, commentName));
// handled flag
if (lastReadTime != null) {
annotations.put(Constant.LAST_READ_TIME_ANNO, lastReadTime.toString());
} else {
annotations.remove(Constant.LAST_READ_TIME_ANNO);
}
}
private void updateSameSubjectRefCommentCounter(Ref commentSubjectRef) {
List<Comment> comments = client.list(Comment.class,
comment -> !isDeleted(comment)
&& commentSubjectRef.equals(comment.getSpec().getSubjectRef()),
null);
private void updateSameSubjectRefCommentCounter(Comment comment) {
var commentSubjectRef = comment.getSpec().getSubjectRef();
GroupVersionKind groupVersionKind = groupVersionKind(commentSubjectRef);
if (groupVersionKind == null) {
return;
}
// approved total count
long approvedTotalCount = comments.stream()
.filter(comment -> BooleanUtils.isTrue(comment.getSpec().getApproved()))
.count();
// total count
int totalCount = comments.size();
var totalCount = countTotalComments(commentSubjectRef);
var approvedTotalCount = countApprovedComments(commentSubjectRef);
schemeManager.fetch(groupVersionKind).ifPresent(scheme -> {
String counterName = MeterUtils.nameOf(commentSubjectRef.getGroup(), scheme.plural(),
commentSubjectRef.getName());
client.fetch(Counter.class, counterName).ifPresentOrElse(counter -> {
counter.setTotalComment(totalCount);
counter.setApprovedComment((int) approvedTotalCount);
counter.setApprovedComment(approvedTotalCount);
client.update(counter);
}, () -> {
Counter counter = Counter.emptyCounter(counterName);
counter.setTotalComment(totalCount);
counter.setApprovedComment((int) approvedTotalCount);
counter.setApprovedComment(approvedTotalCount);
client.create(counter);
});
});
}
private void cleanUpResourcesAndRemoveFinalizer(String commentName) {
client.fetch(Comment.class, commentName).ifPresent(comment -> {
cleanUpResources(comment);
if (comment.getMetadata().getFinalizers() != null) {
comment.getMetadata().getFinalizers().remove(FINALIZER_NAME);
}
client.update(comment);
});
int countTotalComments(Ref commentSubjectRef) {
var totalListOptions = new ListOptions();
totalListOptions.setFieldSelector(FieldSelector.of(getBaseQuery(commentSubjectRef)));
return (int) client.listBy(Comment.class, totalListOptions, PageRequestImpl.ofSize(1))
.getTotal();
}
int countApprovedComments(Ref commentSubjectRef) {
var approvedListOptions = new ListOptions();
approvedListOptions.setFieldSelector(FieldSelector.of(and(
getBaseQuery(commentSubjectRef),
equal("spec.approved", BooleanUtils.TRUE)
)));
return (int) client.listBy(Comment.class, approvedListOptions, PageRequestImpl.ofSize(1))
.getTotal();
}
private static Query getBaseQuery(Ref commentSubjectRef) {
return and(equal("spec.subjectRef", Comment.toSubjectRefKey(commentSubjectRef)),
isNull("metadata.deletionTimestamp"));
}
private void cleanUpResources(Comment comment) {
// delete all replies under current comment
client.list(Reply.class, reply -> comment.getMetadata().getName()
.equals(reply.getSpec().getCommentName()),
null)
.forEach(client::delete);
replyService.removeAllByComment(comment.getMetadata().getName()).block();
// decrement total comment count
updateSameSubjectRefCommentCounter(comment.getSpec().getSubjectRef());
updateSameSubjectRefCommentCounter(comment);
}
@Nullable
private GroupVersionKind groupVersionKind(Ref ref) {
if (ref == null) {
return null;
}
@NonNull
private GroupVersionKind groupVersionKind(@NonNull Ref ref) {
return new GroupVersionKind(ref.getGroup(), ref.getVersion(), ref.getKind());
}
}

View File

@ -25,8 +25,8 @@ import org.springframework.stereotype.Component;
import run.halo.app.content.ContentWrapper;
import run.halo.app.content.NotificationReasonConst;
import run.halo.app.content.PostService;
import run.halo.app.content.comment.CommentService;
import run.halo.app.content.permalinks.PostPermalinkPolicy;
import run.halo.app.core.extension.content.Comment;
import run.halo.app.core.extension.content.Constant;
import run.halo.app.core.extension.content.Post;
import run.halo.app.core.extension.content.Post.PostPhase;
@ -75,6 +75,7 @@ public class PostReconciler implements Reconciler<Reconciler.Request> {
private final PostService postService;
private final PostPermalinkPolicy postPermalinkPolicy;
private final CounterService counterService;
private final CommentService commentService;
private final ApplicationEventPublisher eventPublisher;
private final NotificationCenter notificationCenter;
@ -329,9 +330,7 @@ public class PostReconciler implements Reconciler<Reconciler.Request> {
listSnapshots(ref).forEach(client::delete);
// clean up comments
client.list(Comment.class, comment -> ref.equals(comment.getSpec().getSubjectRef()),
null)
.forEach(client::delete);
commentService.removeBySubject(ref).block();
// delete counter
counterService.deleteByName(MeterUtils.nameOf(Post.class, post.getMetadata().getName()))

View File

@ -19,7 +19,7 @@ import org.springframework.stereotype.Component;
import org.springframework.util.Assert;
import run.halo.app.content.NotificationReasonConst;
import run.halo.app.content.SinglePageService;
import run.halo.app.core.extension.content.Comment;
import run.halo.app.content.comment.CommentService;
import run.halo.app.core.extension.content.Post;
import run.halo.app.core.extension.content.SinglePage;
import run.halo.app.core.extension.content.Snapshot;
@ -64,6 +64,7 @@ public class SinglePageReconciler implements Reconciler<Reconciler.Request> {
private final ExtensionClient client;
private final SinglePageService singlePageService;
private final CounterService counterService;
private final CommentService commentService;
private final ExternalUrlSupplier externalUrlSupplier;
@ -250,9 +251,7 @@ public class SinglePageReconciler implements Reconciler<Reconciler.Request> {
listSnapshots(ref).forEach(client::delete);
// clean up comments
client.list(Comment.class, comment -> comment.getSpec().getSubjectRef().equals(ref),
null)
.forEach(client::delete);
commentService.removeBySubject(ref).block();
// delete counter for single page
counterService.deleteByName(

View File

@ -0,0 +1,22 @@
package run.halo.app.event.post;
import lombok.Getter;
import org.springframework.context.ApplicationEvent;
/**
* <p>This event will be triggered when the unread reply count of the comment is changed.</p>
* <p>It is used to update the unread reply count of the comment,such as when the user reads the
* reply(lastReadTime changed in comment), the unread reply count will be updated.</p>
*
* @author guqing
* @since 2.14.0
*/
@Getter
public class CommentUnreadReplyCountChangedEvent extends ApplicationEvent {
private final String commentName;
public CommentUnreadReplyCountChangedEvent(Object source, String commentName) {
super(source);
this.commentName = commentName;
}
}

View File

@ -21,8 +21,6 @@ import lombok.extern.slf4j.Slf4j;
import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.context.event.EventListener;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.util.Predicates;
import org.springframework.stereotype.Component;
@ -34,7 +32,6 @@ import reactor.util.retry.Retry;
import run.halo.app.extension.exception.ExtensionNotFoundException;
import run.halo.app.extension.index.DefaultExtensionIterator;
import run.halo.app.extension.index.ExtensionIterator;
import run.halo.app.extension.index.ExtensionPaginatedLister;
import run.halo.app.extension.index.IndexedQueryEngine;
import run.halo.app.extension.index.IndexerFactory;
import run.halo.app.extension.store.ReactiveExtensionStoreClient;
@ -341,18 +338,13 @@ public class ReactiveExtensionClientImpl implements ReactiveExtensionClient {
private ExtensionIterator<Extension> createExtensionIterator(Scheme scheme) {
var type = scheme.type();
var prefix = ExtensionStoreUtil.buildStoreNamePrefix(scheme);
var lister = new ExtensionPaginatedLister() {
@Override
@SuppressWarnings("unchecked")
public <E extends Extension> Page<E> list(Pageable pageable) {
return client.listByNamePrefix(prefix, pageable)
.map(page -> page.map(
store -> (E) converter.convertFrom(type, store))
)
.block();
}
};
return new DefaultExtensionIterator<>(lister);
return new DefaultExtensionIterator<>(pageable ->
client.listByNamePrefix(prefix, pageable)
.map(page ->
page.map(store -> (Extension) converter.convertFrom(type, store))
)
.block()
);
}
@EventListener(ContextRefreshedEvent.class)

View File

@ -17,19 +17,23 @@ import run.halo.app.extension.Extension;
*/
public class DefaultExtensionIterator<E extends Extension> implements ExtensionIterator<E> {
static final int DEFAULT_PAGE_SIZE = 500;
private final ExtensionPaginatedLister lister;
private final ExtensionPaginatedLister<E> lister;
private Pageable currentPageable;
private List<E> currentData;
private int currentIndex;
public DefaultExtensionIterator(ExtensionPaginatedLister<E> lister) {
this(PageRequest.of(0, DEFAULT_PAGE_SIZE, Sort.by("name")), lister);
}
/**
* Constructs a new DefaultExtensionIterator with the given lister.
*
* @param lister the lister to use to load data.
*/
public DefaultExtensionIterator(ExtensionPaginatedLister lister) {
public DefaultExtensionIterator(Pageable initPageable, ExtensionPaginatedLister<E> lister) {
this.lister = lister;
this.currentPageable = PageRequest.of(0, DEFAULT_PAGE_SIZE, Sort.by("name"));
this.currentPageable = initPageable;
this.currentData = loadData();
}

View File

@ -10,14 +10,14 @@ import run.halo.app.extension.Extension;
* @author guqing
* @since 2.12.0
*/
public interface ExtensionPaginatedLister {
@FunctionalInterface
public interface ExtensionPaginatedLister<E extends Extension> {
/**
* List extensions with pagination.
*
* @param pageable pageable
* @param <E> extension type
* @return page of extensions
*/
<E extends Extension> Page<E> list(Pageable pageable);
Page<E> list(Pageable pageable);
}

View File

@ -14,10 +14,10 @@ import org.apache.commons.lang3.BooleanUtils;
import org.springframework.context.SmartLifecycle;
import org.springframework.context.event.EventListener;
import org.springframework.data.domain.Sort;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
import run.halo.app.core.extension.content.Comment;
import run.halo.app.core.extension.content.Reply;
import run.halo.app.event.post.CommentUnreadReplyCountChangedEvent;
import run.halo.app.event.post.ReplyEvent;
import run.halo.app.extension.ExtensionClient;
import run.halo.app.extension.ListOptions;
@ -39,11 +39,12 @@ import run.halo.app.extension.router.selector.FieldSelector;
*/
@Slf4j
@Component
public class ReplyEventReconciler implements Reconciler<ReplyEvent>, SmartLifecycle {
public class ReplyEventReconciler
implements Reconciler<ReplyEventReconciler.CommentName>, SmartLifecycle {
private volatile boolean running = false;
private final ExtensionClient client;
private final RequestQueue<ReplyEvent> replyEventQueue;
private final RequestQueue<CommentName> replyEventQueue;
private final Controller replyEventController;
public ReplyEventReconciler(ExtensionClient client) {
@ -53,9 +54,8 @@ public class ReplyEventReconciler implements Reconciler<ReplyEvent>, SmartLifecy
}
@Override
public Result reconcile(ReplyEvent request) {
Reply requestReply = request.getReply();
String commentName = requestReply.getSpec().getCommentName();
public Result reconcile(CommentName request) {
String commentName = request.name();
client.fetch(Comment.class, commentName)
// if the comment has been deleted, then do nothing.
@ -110,6 +110,12 @@ public class ReplyEventReconciler implements Reconciler<ReplyEvent>, SmartLifecy
return new Result(false, null);
}
public record CommentName(String name) {
public static CommentName of(String name) {
return new CommentName(name);
}
}
static ListOptions listOptionsWithFieldQuery(Query query) {
var listOptions = new ListOptions();
listOptions.setFieldSelector(FieldSelector.of(query));
@ -144,13 +150,14 @@ public class ReplyEventReconciler implements Reconciler<ReplyEvent>, SmartLifecy
return this.running;
}
@Component
public class ReplyEventListener {
@EventListener(ReplyEvent.class)
public void onReplyEvent(ReplyEvent replyEvent) {
var commentName = replyEvent.getReply().getSpec().getCommentName();
replyEventQueue.addImmediately(CommentName.of(commentName));
}
@Async
@EventListener(ReplyEvent.class)
public void onReplyEvent(ReplyEvent replyEvent) {
replyEventQueue.addImmediately(replyEvent);
}
@EventListener(CommentUnreadReplyCountChangedEvent.class)
public void onUnreadReplyCountChangedEvent(CommentUnreadReplyCountChangedEvent event) {
replyEventQueue.addImmediately(CommentName.of(event.getCommentName()));
}
}

View File

@ -1,52 +0,0 @@
package run.halo.app.content.comment;
import static org.assertj.core.api.Assertions.assertThat;
import java.time.Instant;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.junit.jupiter.api.Test;
import run.halo.app.core.extension.content.Reply;
import run.halo.app.extension.Metadata;
/**
* Tests for {@link ReplyService}.
*
* @author guqing
* @since 2.0.0
*/
class ReplyServiceTest {
private final Instant now = Instant.now();
@Test
void creationTimeAscComparator() {
// creation time:
// 1. now + 5s, name: 1
// 2. now + 3s, name: 2
// 3. now + 3s, name: 3
// 5. now + 1s, name: 4
// 6. now - 1s, name: 5
// 7. null, name: 6
Reply reply1 = createReply("1", now.plusSeconds(5));
Reply reply2 = createReply("2", now.plusSeconds(3));
Reply reply3 = createReply("3", now.plusSeconds(3));
Reply reply4 = createReply("4", now.plusSeconds(1));
Reply reply5 = createReply("5", now.minusSeconds(1));
Reply reply6 = createReply("6", null);
String result = Stream.of(reply1, reply2, reply3, reply4, reply5, reply6)
.sorted(ReplyService.creationTimeAscComparator())
.map(reply -> reply.getMetadata().getName())
.collect(Collectors.joining(", "));
assertThat(result).isEqualTo("5, 4, 2, 3, 1, 6");
}
Reply createReply(String name, Instant creationTime) {
Reply reply = new Reply();
reply.setMetadata(new Metadata());
reply.getMetadata().setName(name);
reply.getMetadata().setCreationTimestamp(now);
reply.setSpec(new Reply.ReplySpec());
reply.getSpec().setCreationTime(creationTime);
return reply;
}
}

View File

@ -3,14 +3,13 @@ package run.halo.app.core.extension.reconciler;
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.ArgumentMatchers.isA;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import java.time.Instant;
import java.util.HashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import org.junit.jupiter.api.Test;
@ -19,10 +18,14 @@ import org.mockito.ArgumentCaptor;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import reactor.core.publisher.Mono;
import run.halo.app.content.comment.ReplyService;
import run.halo.app.core.extension.content.Comment;
import run.halo.app.core.extension.content.Reply;
import run.halo.app.extension.ExtensionClient;
import run.halo.app.extension.ListOptions;
import run.halo.app.extension.ListResult;
import run.halo.app.extension.Metadata;
import run.halo.app.extension.PageRequest;
import run.halo.app.extension.Ref;
import run.halo.app.extension.SchemeManager;
import run.halo.app.extension.controller.Reconciler;
@ -42,6 +45,9 @@ class CommentReconcilerTest {
@Mock
SchemeManager schemeManager;
@Mock
ReplyService replyService;
@InjectMocks
private CommentReconciler commentReconciler;
@ -57,23 +63,23 @@ class CommentReconcilerTest {
finalizers.add(CommentReconciler.FINALIZER_NAME);
comment.getMetadata().setFinalizers(finalizers);
comment.setSpec(new Comment.CommentSpec());
comment.getSpec().setSubjectRef(getRef());
comment.getSpec().setLastReadTime(now.plusSeconds(5));
comment.setStatus(new Comment.CommentStatus());
lenient().when(client.list(eq(Reply.class), any(), any()))
.thenReturn(replyList());
lenient().when(client.fetch(eq(Comment.class), eq("test")))
when(client.fetch(eq(Comment.class), eq("test")))
.thenReturn(Optional.of(comment));
lenient().when(client.list(eq(Reply.class), any(), any()))
.thenReturn(replyList());
when(replyService.removeAllByComment(eq(comment.getMetadata().getName())))
.thenReturn(Mono.empty());
when(client.listBy(eq(Comment.class), any(ListOptions.class), isA(PageRequest.class)))
.thenReturn(ListResult.emptyResult());
Reconciler.Result reconcile = commentReconciler.reconcile(new Reconciler.Request("test"));
assertThat(reconcile.reEnqueue()).isFalse();
assertThat(reconcile.retryAfter()).isNull();
verify(client, times(1)).list(eq(Reply.class), any(), any());
verify(client, times(3)).delete(any(Reply.class));
verify(replyService).removeAllByComment(eq(comment.getMetadata().getName()));
ArgumentCaptor<Comment> captor = ArgumentCaptor.forClass(Comment.class);
verify(client, times(1)).update(captor.capture());
@ -90,19 +96,11 @@ class CommentReconcilerTest {
comment.setSpec(new Comment.CommentSpec());
comment.getSpec().setApprovedTime(Instant.now());
comment.getSpec().setCreationTime(null);
when(client.fetch(eq(Comment.class), eq("fake-comment")))
.thenReturn(Optional.of(comment));
commentReconciler.compatibleCreationTime("fake-comment");
commentReconciler.compatibleCreationTime(comment);
verify(client, times(1)).fetch(eq(Comment.class), eq("fake-comment"));
ArgumentCaptor<Comment> captor = ArgumentCaptor.forClass(Comment.class);
verify(client, times(1)).update(captor.capture());
Comment updated = captor.getValue();
assertThat(updated.getSpec().getCreationTime()).isNotNull();
assertThat(updated.getSpec().getCreationTime())
.isEqualTo(updated.getSpec().getApprovedTime());
assertThat(comment.getSpec().getCreationTime())
.isEqualTo(comment.getSpec().getApprovedTime());
}
private static Ref getRef() {
@ -113,22 +111,4 @@ class CommentReconcilerTest {
ref.setName("fake-post");
return ref;
}
List<Reply> replyList() {
Reply replyA = new Reply();
replyA.setMetadata(new Metadata());
replyA.getMetadata().setName("reply-A");
replyA.getMetadata().setCreationTimestamp(now.plusSeconds(6));
Reply replyB = new Reply();
replyB.setMetadata(new Metadata());
replyB.getMetadata().setName("reply-B");
replyB.getMetadata().setCreationTimestamp(now.plusSeconds(5));
Reply replyC = new Reply();
replyC.setMetadata(new Metadata());
replyC.getMetadata().setName("reply-C");
replyC.getMetadata().setCreationTimestamp(now.plusSeconds(4));
return List.of(replyA, replyB, replyC);
}
}
}

View File

@ -28,7 +28,7 @@ import run.halo.app.extension.Extension;
class DefaultExtensionIteratorTest {
@Mock
private ExtensionPaginatedLister lister;
private ExtensionPaginatedLister<Extension> lister;
@Test
@SuppressWarnings("unchecked")