Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: optimize reply queries using index mechanisms #5497

Merged
merged 4 commits into from Mar 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Expand Up @@ -56,13 +56,16 @@ public String getOwnerName() {
@ArraySchema(uniqueItems = true,
arraySchema = @Schema(name = "sort",
description = "Sort property and direction of the list result. Supported fields: "
+ "creationTimestamp,replyCount,lastReplyTime"),
+ "metadata.creationTimestamp,status.replyCount,status.lastReplyTime"),
schema = @Schema(description = "like field,asc or field,desc",
implementation = String.class,
example = "creationTimestamp,desc"))
public Sort getSort() {
var sort = SortResolver.defaultInstance.resolve(exchange);
return sort.and(Sort.by("spec.creationTime", "metadata.name").descending());
return sort.and(Sort.by("status.lastReplyTime",
"spec.creationTime",
"metadata.name"
).descending());
}

public PageRequest toPageRequest() {
Expand Down
@@ -1,26 +1,54 @@
package run.halo.app.content.comment;

import static run.halo.app.extension.index.query.QueryFactory.equal;
import static run.halo.app.extension.router.selector.SelectorUtil.labelAndFieldSelectorToListOptions;

import io.swagger.v3.oas.annotations.media.Schema;
import org.apache.commons.lang3.StringUtils;
import org.springframework.util.MultiValueMap;
import org.springframework.data.domain.Sort;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.ServerWebInputException;
import run.halo.app.core.extension.content.Reply;
import run.halo.app.extension.router.IListRequest;
import run.halo.app.extension.ListOptions;
import run.halo.app.extension.PageRequest;
import run.halo.app.extension.PageRequestImpl;
import run.halo.app.extension.router.SortableRequest;

/**
* Query criteria for {@link Reply} list.
*
* @author guqing
* @since 2.0.0
*/
public class ReplyQuery extends IListRequest.QueryListRequest {
public class ReplyQuery extends SortableRequest {

public ReplyQuery(MultiValueMap<String, String> queryParams) {
super(queryParams);
public ReplyQuery(ServerWebExchange exchange) {
super(exchange);
}

@Schema(description = "Replies filtered by commentName.")
public String getCommentName() {
String commentName = queryParams.getFirst("commentName");
return StringUtils.isBlank(commentName) ? null : commentName;
if (StringUtils.isBlank(commentName)) {
throw new ServerWebInputException("The required parameter 'commentName' is missing.");
}
return commentName;
}

/**
* Build list options from query criteria.
*/
public ListOptions toListOptions() {
var listOptions =
labelAndFieldSelectorToListOptions(getLabelSelector(), getFieldSelector());
var newFieldSelector = listOptions.getFieldSelector()
.andQuery(equal("spec.commentName", getCommentName()));
listOptions.setFieldSelector(newFieldSelector);
return listOptions;
}

public PageRequest toPageRequest() {
var sort = getSort().and(Sort.by("spec.creationTime").ascending());
return PageRequestImpl.of(getPage(), getSize(), sort);
}
}
Expand Up @@ -79,9 +79,7 @@ public Mono<Reply> create(String commentName, Reply reply) {

@Override
public Mono<ListResult<ListedReply>> list(ReplyQuery query) {
return client.list(Reply.class, getReplyPredicate(query),
ReplyService.creationTimeAscComparator(),
query.getPage(), query.getSize())
return client.listBy(Reply.class, query.toListOptions(), query.toPageRequest())
.flatMap(list -> Flux.fromStream(list.get()
.map(this::toListedReply))
.concatMap(Function.identity())
Expand Down
Expand Up @@ -48,7 +48,7 @@ public RouterFunction<ServerResponse> endpoint() {
}

Mono<ServerResponse> listReplies(ServerRequest request) {
ReplyQuery replyQuery = new ReplyQuery(request.queryParams());
ReplyQuery replyQuery = new ReplyQuery(request.exchange());
return replyService.list(replyQuery)
.flatMap(listedReplies -> ServerResponse.ok().bodyValue(listedReplies));
}
Expand Down
@@ -1,5 +1,6 @@
package run.halo.app.core.extension.reconciler;

import static org.apache.commons.lang3.ObjectUtils.defaultIfNull;
import static run.halo.app.extension.ExtensionUtil.addFinalizers;

import java.util.Set;
Expand Down Expand Up @@ -45,6 +46,15 @@ public Result reconcile(Request request) {
eventPublisher.publishEvent(new ReplyCreatedEvent(this, reply));
}

if (reply.getSpec().getCreationTime() == null) {
reply.getSpec().setCreationTime(
defaultIfNull(reply.getSpec().getApprovedTime(),
reply.getMetadata().getCreationTimestamp()
)
);
}
client.update(reply);

replyNotificationSubscriptionHelper.subscribeNewReplyReasonForReply(reply);

eventPublisher.publishEvent(new ReplyChangedEvent(this, reply));
Expand Down
Expand Up @@ -223,7 +223,8 @@ public void onApplicationEvent(@NonNull ApplicationContextInitializedEvent event
indexSpecs.add(new IndexSpec()
.setName("spec.creationTime")
.setIndexFunc(simpleAttribute(Comment.class,
comment -> comment.getSpec().getCreationTime().toString())
comment -> defaultIfNull(comment.getSpec().getCreationTime(),
comment.getMetadata().getCreationTimestamp()).toString())
));
indexSpecs.add(new IndexSpec()
.setName("spec.approved")
Expand Down Expand Up @@ -282,7 +283,35 @@ public void onApplicationEvent(@NonNull ApplicationContextInitializedEvent event
return defaultIfNull(replyCount, 0).toString();
})));
});
schemeManager.register(Reply.class);
schemeManager.register(Reply.class, indexSpecs -> {
indexSpecs.add(new IndexSpec()
.setName("spec.creationTime")
.setIndexFunc(simpleAttribute(Reply.class,
reply -> defaultIfNull(reply.getSpec().getCreationTime(),
reply.getMetadata().getCreationTimestamp()).toString())
));
indexSpecs.add(new IndexSpec()
.setName("spec.commentName")
.setIndexFunc(simpleAttribute(Reply.class,
reply -> reply.getSpec().getCommentName())
));
indexSpecs.add(new IndexSpec()
.setName("spec.hidden")
.setIndexFunc(simpleAttribute(Reply.class,
reply -> toStringTrueFalse(isTrue(reply.getSpec().getHidden())))
));
indexSpecs.add(new IndexSpec()
.setName("spec.approved")
.setIndexFunc(simpleAttribute(Reply.class,
reply -> toStringTrueFalse(isTrue(reply.getSpec().getApproved())))
));
indexSpecs.add(new IndexSpec()
.setName("spec.owner")
.setIndexFunc(simpleAttribute(Reply.class, reply -> {
var owner = reply.getSpec().getOwner();
return Comment.CommentOwner.ownerIdentity(owner.getKind(), owner.getName());
})));
});
schemeManager.register(SinglePage.class);
// storage.halo.run
schemeManager.register(Group.class);
Expand Down
@@ -1,28 +1,35 @@
package run.halo.app.metrics;

import static org.apache.commons.lang3.BooleanUtils.isFalse;
import static org.apache.commons.lang3.BooleanUtils.isTrue;
import static org.apache.commons.lang3.ObjectUtils.defaultIfNull;
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.greaterThan;
import static run.halo.app.extension.index.query.QueryFactory.isNull;

import java.time.Duration;
import java.time.Instant;
import java.util.List;
import java.util.Optional;
import lombok.extern.slf4j.Slf4j;
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.content.comment.ReplyService;
import run.halo.app.core.extension.content.Comment;
import run.halo.app.core.extension.content.Reply;
import run.halo.app.event.post.ReplyEvent;
import run.halo.app.extension.ExtensionClient;
import run.halo.app.extension.ListOptions;
import run.halo.app.extension.PageRequestImpl;
import run.halo.app.extension.controller.Controller;
import run.halo.app.extension.controller.ControllerBuilder;
import run.halo.app.extension.controller.DefaultController;
import run.halo.app.extension.controller.DefaultQueue;
import run.halo.app.extension.controller.Reconciler;
import run.halo.app.extension.controller.RequestQueue;
import run.halo.app.extension.index.query.Query;
import run.halo.app.extension.router.selector.FieldSelector;

/**
* Update the comment status after receiving the reply event.
Expand Down Expand Up @@ -54,42 +61,61 @@ public Result reconcile(ReplyEvent request) {
// if the comment has been deleted, then do nothing.
.filter(comment -> comment.getMetadata().getDeletionTimestamp() == null)
.ifPresent(comment -> {

// order by reply creation time desc to get first as last reply time
List<Reply> replies = client.list(Reply.class,
record -> commentName.equals(record.getSpec().getCommentName())
&& record.getMetadata().getDeletionTimestamp() == null,
ReplyService.creationTimeAscComparator().reversed());

Comment.CommentStatus status = comment.getStatusOrDefault();
var baseQuery = and(
equal("spec.commentName", commentName),
isNull("metadata.deletionTimestamp")
);
var pageRequest = PageRequestImpl.ofSize(1).withSort(
Sort.by("spec.creationTime", "metadata.name").descending()
);
final Comment.CommentStatus status = comment.getStatusOrDefault();

var replyPageResult =
client.listBy(Reply.class, listOptionsWithFieldQuery(baseQuery), pageRequest);
// total reply count
status.setReplyCount(replies.size());

long visibleReplyCount = replies.stream()
.filter(reply -> isTrue(reply.getSpec().getApproved())
&& isFalse(reply.getSpec().getHidden())
)
.count();
status.setVisibleReplyCount((int) visibleReplyCount);
status.setReplyCount((int) replyPageResult.getTotal());

// calculate last reply time
Instant lastReplyTime = replies.stream()
// calculate last reply time from total replies(top 1)
Instant lastReplyTime = replyPageResult.get()
.map(reply -> reply.getSpec().getCreationTime())
.findFirst()
.map(reply -> defaultIfNull(reply.getSpec().getCreationTime(),
reply.getMetadata().getCreationTimestamp())
)
.orElse(null);
status.setLastReplyTime(lastReplyTime);

Instant lastReadTime = comment.getSpec().getLastReadTime();
status.setUnreadReplyCount(Comment.getUnreadReplyCount(replies, lastReadTime));
// calculate visible reply count(only approved and not hidden)
var visibleReplyPageResult =
client.listBy(Reply.class, listOptionsWithFieldQuery(and(
baseQuery,
equal("spec.approved", BooleanUtils.TRUE),
equal("spec.hidden", BooleanUtils.FALSE)
)), pageRequest);
status.setVisibleReplyCount((int) visibleReplyPageResult.getTotal());

// calculate unread reply count(after last read time)
var unReadQuery = Optional.ofNullable(comment.getSpec().getLastReadTime())
.map(lastReadTime -> and(
baseQuery,
greaterThan("spec.creationTime", lastReadTime.toString())
))
.orElse(baseQuery);
var unReadPageResult =
client.listBy(Reply.class, listOptionsWithFieldQuery(unReadQuery), pageRequest);
status.setUnreadReplyCount((int) unReadPageResult.getTotal());

status.setHasNewReply(defaultIfNull(status.getUnreadReplyCount(), 0) > 0);

client.update(comment);
});
return new Result(false, null);
}

static ListOptions listOptionsWithFieldQuery(Query query) {
var listOptions = new ListOptions();
listOptions.setFieldSelector(FieldSelector.of(query));
return listOptions;
}

@Override
public Controller setupWith(ControllerBuilder builder) {
return new DefaultController<>(
Expand Down
@@ -1,9 +1,7 @@
package run.halo.app.theme.finders;

import java.util.Comparator;
import org.springframework.lang.Nullable;
import reactor.core.publisher.Mono;
import run.halo.app.core.extension.content.Reply;
import run.halo.app.extension.ListResult;
import run.halo.app.extension.PageRequest;
import run.halo.app.extension.Ref;
Expand All @@ -26,6 +24,5 @@ Mono<ListResult<CommentVo>> list(Ref ref, @Nullable Integer page,
Mono<ListResult<ReplyVo>> listReply(String commentName, @Nullable Integer page,
@Nullable Integer size);

Mono<ListResult<ReplyVo>> listReply(String commentName, @Nullable Integer page,
@Nullable Integer size, @Nullable Comparator<Reply> comparator);
Mono<ListResult<ReplyVo>> listReply(String commentName, PageRequest pageRequest);
}