mirror of https://github.com/halo-dev/halo
refactor: refine statistical approach for tallying posts by category association (#5671)
parent
4d10d73ba9
commit
b4b6693732
|
@ -42,6 +42,8 @@ public class Post extends AbstractExtension {
|
|||
public static final String LAST_RELEASED_SNAPSHOT_ANNO =
|
||||
"content.halo.run/last-released-snapshot";
|
||||
public static final String LAST_ASSOCIATED_TAGS_ANNO = "content.halo.run/last-associated-tags";
|
||||
public static final String LAST_ASSOCIATED_CATEGORIES_ANNO =
|
||||
"content.halo.run/last-associated-categories";
|
||||
|
||||
public static final String STATS_ANNO = "content.halo.run/stats";
|
||||
|
||||
|
|
|
@ -0,0 +1,66 @@
|
|||
package run.halo.app.content;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import org.springframework.context.SmartLifecycle;
|
||||
import run.halo.app.extension.ExtensionClient;
|
||||
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;
|
||||
|
||||
/**
|
||||
* An abstract class for reconciling events.
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.15.0
|
||||
*/
|
||||
public abstract class AbstractEventReconciler<E> implements Reconciler<E>, SmartLifecycle {
|
||||
protected final RequestQueue<E> queue;
|
||||
|
||||
protected final Controller controller;
|
||||
|
||||
protected volatile boolean running = false;
|
||||
|
||||
protected final ExtensionClient client;
|
||||
|
||||
private final String controllerName;
|
||||
|
||||
protected AbstractEventReconciler(String controllerName, ExtensionClient client) {
|
||||
this.client = client;
|
||||
this.controllerName = controllerName;
|
||||
this.queue = new DefaultQueue<>(Instant::now);
|
||||
this.controller = this.setupWith(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Controller setupWith(ControllerBuilder builder) {
|
||||
return new DefaultController<>(
|
||||
controllerName,
|
||||
this,
|
||||
queue,
|
||||
null,
|
||||
Duration.ofMillis(100),
|
||||
Duration.ofMinutes(10)
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start() {
|
||||
controller.start();
|
||||
running = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop() {
|
||||
running = false;
|
||||
controller.dispose();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isRunning() {
|
||||
return running;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,161 @@
|
|||
package run.halo.app.content;
|
||||
|
||||
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.isNull;
|
||||
|
||||
import com.google.common.collect.Sets;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import org.apache.commons.lang3.BooleanUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.context.event.EventListener;
|
||||
import org.springframework.stereotype.Component;
|
||||
import run.halo.app.core.extension.content.Category;
|
||||
import run.halo.app.core.extension.content.Post;
|
||||
import run.halo.app.event.post.PostDeletedEvent;
|
||||
import run.halo.app.event.post.PostUpdatedEvent;
|
||||
import run.halo.app.extension.ExtensionClient;
|
||||
import run.halo.app.extension.ListOptions;
|
||||
import run.halo.app.extension.MetadataUtil;
|
||||
import run.halo.app.extension.PageRequestImpl;
|
||||
import run.halo.app.extension.index.query.Query;
|
||||
import run.halo.app.extension.router.selector.FieldSelector;
|
||||
import run.halo.app.extension.router.selector.LabelSelector;
|
||||
import run.halo.app.infra.utils.JsonUtils;
|
||||
|
||||
/**
|
||||
* A class used to update the post count of the category when the post changes.
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.15.0
|
||||
*/
|
||||
@Component
|
||||
public class CategoryPostCountUpdater
|
||||
extends AbstractEventReconciler<CategoryPostCountUpdater.PostRelatedCategories> {
|
||||
|
||||
private final CategoryPostCountService categoryPostCountService;
|
||||
|
||||
public CategoryPostCountUpdater(ExtensionClient client) {
|
||||
super(CategoryPostCountUpdater.class.getName(), client);
|
||||
this.categoryPostCountService = new CategoryPostCountService(client);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result reconcile(PostRelatedCategories request) {
|
||||
var categoryChanges = request.categoryChanges();
|
||||
|
||||
categoryPostCountService.recalculatePostCount(categoryChanges);
|
||||
|
||||
client.fetch(Post.class, request.postName()).ifPresent(post -> {
|
||||
var categories = defaultIfNull(post.getSpec().getCategories(), List.<String>of());
|
||||
var annotations = MetadataUtil.nullSafeAnnotations(post);
|
||||
var categoryAnno = JsonUtils.objectToJson(categories);
|
||||
var oldCategoryAnno = annotations.get(Post.LAST_ASSOCIATED_CATEGORIES_ANNO);
|
||||
|
||||
if (!categoryAnno.equals(oldCategoryAnno)) {
|
||||
annotations.put(Post.LAST_ASSOCIATED_CATEGORIES_ANNO, categoryAnno);
|
||||
client.update(post);
|
||||
}
|
||||
});
|
||||
return Result.doNotRetry();
|
||||
}
|
||||
|
||||
static class CategoryPostCountService {
|
||||
|
||||
private final ExtensionClient client;
|
||||
|
||||
public CategoryPostCountService(ExtensionClient client) {
|
||||
this.client = client;
|
||||
}
|
||||
|
||||
public void recalculatePostCount(Collection<String> categoryNames) {
|
||||
for (String categoryName : categoryNames) {
|
||||
recalculatePostCount(categoryName);
|
||||
}
|
||||
}
|
||||
|
||||
public void recalculatePostCount(String categoryName) {
|
||||
var totalPostCount = countTotalPosts(categoryName);
|
||||
var visiblePostCount = countVisiblePosts(categoryName);
|
||||
client.fetch(Category.class, categoryName).ifPresent(category -> {
|
||||
category.getStatusOrDefault().setPostCount(totalPostCount);
|
||||
category.getStatusOrDefault().setVisiblePostCount(visiblePostCount);
|
||||
|
||||
client.update(category);
|
||||
});
|
||||
}
|
||||
|
||||
private int countTotalPosts(String categoryName) {
|
||||
var postListOptions = new ListOptions();
|
||||
postListOptions.setFieldSelector(FieldSelector.of(
|
||||
basePostQuery(categoryName)
|
||||
));
|
||||
return (int) client.listBy(Post.class, postListOptions, PageRequestImpl.ofSize(1))
|
||||
.getTotal();
|
||||
}
|
||||
|
||||
private int countVisiblePosts(String categoryName) {
|
||||
var postListOptions = new ListOptions();
|
||||
var fieldQuery = and(basePostQuery(categoryName),
|
||||
equal("spec.visible", Post.VisibleEnum.PUBLIC.name())
|
||||
);
|
||||
var labelSelector = LabelSelector.builder()
|
||||
.eq(Post.PUBLISHED_LABEL, BooleanUtils.TRUE)
|
||||
.build();
|
||||
postListOptions.setFieldSelector(FieldSelector.of(fieldQuery));
|
||||
postListOptions.setLabelSelector(labelSelector);
|
||||
return (int) client.listBy(Post.class, postListOptions, PageRequestImpl.ofSize(1))
|
||||
.getTotal();
|
||||
}
|
||||
|
||||
private static Query basePostQuery(String categoryName) {
|
||||
return and(isNull("metadata.deletionTimestamp"),
|
||||
equal("spec.deleted", BooleanUtils.FALSE),
|
||||
equal("spec.categories", categoryName)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public record PostRelatedCategories(String postName, Collection<String> categoryChanges) {
|
||||
}
|
||||
|
||||
@EventListener(PostUpdatedEvent.class)
|
||||
public void onPostUpdated(PostUpdatedEvent event) {
|
||||
var postName = event.getName();
|
||||
var changes = calcCategoriesToUpdate(event.getName());
|
||||
queue.addImmediately(new PostRelatedCategories(postName, changes));
|
||||
}
|
||||
|
||||
@EventListener(PostDeletedEvent.class)
|
||||
public void onPostDeleted(PostDeletedEvent event) {
|
||||
var postName = event.getName();
|
||||
var categories = defaultIfNull(event.getPost().getSpec().getCategories(),
|
||||
List.<String>of());
|
||||
queue.addImmediately(new PostRelatedCategories(postName, categories));
|
||||
}
|
||||
|
||||
private Set<String> calcCategoriesToUpdate(String postName) {
|
||||
return client.fetch(Post.class, postName)
|
||||
.map(post -> {
|
||||
var annotations = MetadataUtil.nullSafeAnnotations(post);
|
||||
var oldCategories =
|
||||
Optional.ofNullable(annotations.get(Post.LAST_ASSOCIATED_CATEGORIES_ANNO))
|
||||
.filter(StringUtils::isNotBlank)
|
||||
.map(categoriesJson -> JsonUtils.jsonToObject(categoriesJson,
|
||||
String[].class))
|
||||
.orElse(new String[0]);
|
||||
|
||||
Set<String> categoriesToUpdate = Sets.newHashSet(oldCategories);
|
||||
var newCategories = post.getSpec().getCategories();
|
||||
if (newCategories != null) {
|
||||
categoriesToUpdate.addAll(newCategories);
|
||||
}
|
||||
return categoriesToUpdate;
|
||||
})
|
||||
.orElse(Set.of());
|
||||
}
|
||||
}
|
|
@ -6,13 +6,10 @@ import static run.halo.app.extension.index.query.QueryFactory.equal;
|
|||
import static run.halo.app.extension.index.query.QueryFactory.isNull;
|
||||
|
||||
import com.google.common.collect.Sets;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.context.SmartLifecycle;
|
||||
import org.springframework.context.event.EventListener;
|
||||
import org.springframework.stereotype.Component;
|
||||
import run.halo.app.core.extension.content.Post;
|
||||
|
@ -25,12 +22,6 @@ import run.halo.app.extension.ExtensionClient;
|
|||
import run.halo.app.extension.ListOptions;
|
||||
import run.halo.app.extension.MetadataUtil;
|
||||
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.router.selector.FieldSelector;
|
||||
import run.halo.app.extension.router.selector.LabelSelector;
|
||||
import run.halo.app.infra.utils.JsonUtils;
|
||||
|
@ -43,24 +34,10 @@ import run.halo.app.infra.utils.JsonUtils;
|
|||
*/
|
||||
@Component
|
||||
public class TagPostCountUpdater
|
||||
implements Reconciler<TagPostCountUpdater.PostRelatedTags>, SmartLifecycle {
|
||||
extends AbstractEventReconciler<TagPostCountUpdater.PostRelatedTags> {
|
||||
|
||||
private final RequestQueue<PostRelatedTags> tagQueue;
|
||||
|
||||
private final Controller postEventController;
|
||||
|
||||
private final ExtensionClient client;
|
||||
|
||||
private volatile boolean running = false;
|
||||
|
||||
/**
|
||||
* Construct a {@link TagPostCountUpdater} with the given {@link ExtensionClient}.
|
||||
*/
|
||||
public TagPostCountUpdater(ExtensionClient client) {
|
||||
this.client = client;
|
||||
|
||||
this.tagQueue = new DefaultQueue<>(Instant::now);
|
||||
this.postEventController = this.setupWith(null);
|
||||
super(TagPostCountUpdater.class.getName(), client);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -84,36 +61,6 @@ public class TagPostCountUpdater
|
|||
return Result.doNotRetry();
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public Controller setupWith(ControllerBuilder builder) {
|
||||
return new DefaultController<>(
|
||||
this.getClass().getName(),
|
||||
this,
|
||||
tagQueue,
|
||||
null,
|
||||
Duration.ofMillis(100),
|
||||
Duration.ofMinutes(10)
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start() {
|
||||
postEventController.start();
|
||||
running = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop() {
|
||||
running = false;
|
||||
postEventController.dispose();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isRunning() {
|
||||
return running;
|
||||
}
|
||||
|
||||
/**
|
||||
* Listen to post event to calculate post related to tag for updating.
|
||||
*/
|
||||
|
@ -122,14 +69,14 @@ public class TagPostCountUpdater
|
|||
var postName = postEvent.getName();
|
||||
if (postEvent instanceof PostUpdatedEvent) {
|
||||
var tagsToUpdate = calcTagsToUpdate(postEvent.getName());
|
||||
tagQueue.addImmediately(new PostRelatedTags(postName, tagsToUpdate));
|
||||
queue.addImmediately(new PostRelatedTags(postName, tagsToUpdate));
|
||||
return;
|
||||
}
|
||||
|
||||
if (postEvent instanceof PostDeletedEvent deletedEvent) {
|
||||
var tags = defaultIfNull(deletedEvent.getPost().getSpec().getTags(),
|
||||
List.<String>of());
|
||||
tagQueue.addImmediately(new PostRelatedTags(postName, Sets.newHashSet(tags)));
|
||||
queue.addImmediately(new PostRelatedTags(postName, Sets.newHashSet(tags)));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,40 +1,21 @@
|
|||
package run.halo.app.core.extension.reconciler;
|
||||
|
||||
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.in;
|
||||
import static run.halo.app.extension.index.query.QueryFactory.isNull;
|
||||
import static run.halo.app.extension.ExtensionUtil.addFinalizers;
|
||||
import static run.halo.app.extension.ExtensionUtil.removeFinalizers;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Deque;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
import lombok.AllArgsConstructor;
|
||||
import org.apache.commons.lang3.BooleanUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.stereotype.Component;
|
||||
import run.halo.app.content.permalinks.CategoryPermalinkPolicy;
|
||||
import run.halo.app.core.extension.content.Category;
|
||||
import run.halo.app.core.extension.content.Constant;
|
||||
import run.halo.app.core.extension.content.Post;
|
||||
import run.halo.app.extension.ExtensionClient;
|
||||
import run.halo.app.extension.ListOptions;
|
||||
import run.halo.app.extension.ExtensionUtil;
|
||||
import run.halo.app.extension.MetadataUtil;
|
||||
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.Reconciler;
|
||||
import run.halo.app.extension.index.query.Query;
|
||||
import run.halo.app.extension.router.selector.FieldSelector;
|
||||
import run.halo.app.extension.router.selector.LabelSelector;
|
||||
import run.halo.app.infra.utils.JsonUtils;
|
||||
|
||||
/**
|
||||
* Reconciler for {@link Category}.
|
||||
|
@ -51,22 +32,22 @@ public class CategoryReconciler implements Reconciler<Reconciler.Request> {
|
|||
|
||||
@Override
|
||||
public Result reconcile(Request request) {
|
||||
return client.fetch(Category.class, request.name())
|
||||
.map(category -> {
|
||||
if (category.isDeleted()) {
|
||||
cleanUpResourcesAndRemoveFinalizer(request.name());
|
||||
return new Result(false, null);
|
||||
client.fetch(Category.class, request.name())
|
||||
.ifPresent(category -> {
|
||||
if (ExtensionUtil.isDeleted(category)) {
|
||||
if (removeFinalizers(category.getMetadata(), Set.of(FINALIZER_NAME))) {
|
||||
client.update(category);
|
||||
}
|
||||
return;
|
||||
}
|
||||
addFinalizerIfNecessary(category);
|
||||
addFinalizers(category.getMetadata(), Set.of(FINALIZER_NAME));
|
||||
|
||||
reconcileMetadata(request.name());
|
||||
populatePermalinkPattern(category);
|
||||
populatePermalink(category);
|
||||
|
||||
reconcileStatusPermalink(request.name());
|
||||
|
||||
reconcileStatusPosts(request.name());
|
||||
return new Result(true, Duration.ofMinutes(1));
|
||||
})
|
||||
.orElseGet(() -> new Result(false, null));
|
||||
client.update(category);
|
||||
});
|
||||
return Result.doNotRetry();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -76,136 +57,14 @@ public class CategoryReconciler implements Reconciler<Reconciler.Request> {
|
|||
.build();
|
||||
}
|
||||
|
||||
void reconcileMetadata(String name) {
|
||||
client.fetch(Category.class, name).ifPresent(category -> {
|
||||
Map<String, String> annotations = MetadataUtil.nullSafeAnnotations(category);
|
||||
String oldPermalinkPattern = annotations.get(Constant.PERMALINK_PATTERN_ANNO);
|
||||
|
||||
String newPattern = categoryPermalinkPolicy.pattern();
|
||||
annotations.put(Constant.PERMALINK_PATTERN_ANNO, newPattern);
|
||||
|
||||
if (!StringUtils.equals(oldPermalinkPattern, newPattern)) {
|
||||
client.update(category);
|
||||
}
|
||||
});
|
||||
void populatePermalinkPattern(Category category) {
|
||||
Map<String, String> annotations = MetadataUtil.nullSafeAnnotations(category);
|
||||
String newPattern = categoryPermalinkPolicy.pattern();
|
||||
annotations.put(Constant.PERMALINK_PATTERN_ANNO, newPattern);
|
||||
}
|
||||
|
||||
private void addFinalizerIfNecessary(Category oldCategory) {
|
||||
Set<String> finalizers = oldCategory.getMetadata().getFinalizers();
|
||||
if (finalizers != null && finalizers.contains(FINALIZER_NAME)) {
|
||||
return;
|
||||
}
|
||||
client.fetch(Category.class, oldCategory.getMetadata().getName())
|
||||
.ifPresent(category -> {
|
||||
Set<String> newFinalizers = category.getMetadata().getFinalizers();
|
||||
if (newFinalizers == null) {
|
||||
newFinalizers = new HashSet<>();
|
||||
category.getMetadata().setFinalizers(newFinalizers);
|
||||
}
|
||||
newFinalizers.add(FINALIZER_NAME);
|
||||
client.update(category);
|
||||
});
|
||||
}
|
||||
|
||||
private void cleanUpResourcesAndRemoveFinalizer(String categoryName) {
|
||||
client.fetch(Category.class, categoryName).ifPresent(category -> {
|
||||
if (category.getMetadata().getFinalizers() != null) {
|
||||
category.getMetadata().getFinalizers().remove(FINALIZER_NAME);
|
||||
}
|
||||
client.update(category);
|
||||
});
|
||||
}
|
||||
|
||||
private void reconcileStatusPermalink(String name) {
|
||||
client.fetch(Category.class, name)
|
||||
.ifPresent(category -> {
|
||||
Category oldCategory = JsonUtils.deepCopy(category);
|
||||
category.getStatusOrDefault()
|
||||
.setPermalink(categoryPermalinkPolicy.permalink(category));
|
||||
|
||||
if (!oldCategory.equals(category)) {
|
||||
client.update(category);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void reconcileStatusPosts(String name) {
|
||||
client.fetch(Category.class, name).ifPresent(category -> {
|
||||
Category oldCategory = JsonUtils.deepCopy(category);
|
||||
|
||||
populatePosts(category);
|
||||
|
||||
if (!oldCategory.equals(category)) {
|
||||
client.update(category);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void populatePosts(Category category) {
|
||||
String name = category.getMetadata().getName();
|
||||
Set<String> categoryNames = listChildrenByName(name)
|
||||
.stream()
|
||||
.map(item -> item.getMetadata().getName())
|
||||
.collect(Collectors.toSet());
|
||||
|
||||
var totalPostCount = countTotalPosts(categoryNames);
|
||||
category.getStatusOrDefault().setPostCount(totalPostCount);
|
||||
|
||||
var visiblePostCount = countVisiblePosts(categoryNames);
|
||||
category.getStatusOrDefault().setVisiblePostCount(visiblePostCount);
|
||||
}
|
||||
|
||||
int countTotalPosts(Set<String> categoryNames) {
|
||||
var postListOptions = new ListOptions();
|
||||
postListOptions.setFieldSelector(FieldSelector.of(
|
||||
basePostQuery(categoryNames)
|
||||
));
|
||||
return (int) client.listBy(Post.class, postListOptions, PageRequestImpl.ofSize(1))
|
||||
.getTotal();
|
||||
}
|
||||
|
||||
int countVisiblePosts(Set<String> categoryNames) {
|
||||
var postListOptions = new ListOptions();
|
||||
var fieldQuery = and(basePostQuery(categoryNames),
|
||||
equal("spec.visible", Post.VisibleEnum.PUBLIC.name())
|
||||
);
|
||||
var labelSelector = LabelSelector.builder()
|
||||
.eq(Post.PUBLISHED_LABEL, BooleanUtils.TRUE)
|
||||
.build();
|
||||
postListOptions.setFieldSelector(FieldSelector.of(fieldQuery));
|
||||
postListOptions.setLabelSelector(labelSelector);
|
||||
return (int) client.listBy(Post.class, postListOptions, PageRequestImpl.ofSize(1))
|
||||
.getTotal();
|
||||
}
|
||||
|
||||
private static Query basePostQuery(Set<String> categoryNames) {
|
||||
return and(isNull("metadata.deletionTimestamp"),
|
||||
equal("spec.deleted", BooleanUtils.FALSE),
|
||||
in("spec.categories", categoryNames)
|
||||
);
|
||||
}
|
||||
|
||||
private List<Category> listChildrenByName(String name) {
|
||||
var categories = client.listAll(Category.class, new ListOptions(), Sort.unsorted());
|
||||
Map<String, Category> nameIdentityMap = categories.stream()
|
||||
.collect(Collectors.toMap(category -> category.getMetadata().getName(),
|
||||
Function.identity()));
|
||||
final List<Category> children = new ArrayList<>();
|
||||
|
||||
Deque<String> deque = new ArrayDeque<>();
|
||||
deque.add(name);
|
||||
while (!deque.isEmpty()) {
|
||||
String itemName = deque.poll();
|
||||
Category category = nameIdentityMap.get(itemName);
|
||||
if (category == null) {
|
||||
continue;
|
||||
}
|
||||
children.add(category);
|
||||
List<String> childrenNames = category.getSpec().getChildren();
|
||||
if (childrenNames != null) {
|
||||
deque.addAll(childrenNames);
|
||||
}
|
||||
}
|
||||
return children;
|
||||
void populatePermalink(Category category) {
|
||||
category.getStatusOrDefault()
|
||||
.setPermalink(categoryPermalinkPolicy.permalink(category));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,227 @@
|
|||
package run.halo.app.content;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.boot.test.mock.mockito.SpyBean;
|
||||
import org.springframework.test.annotation.DirtiesContext;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.test.StepVerifier;
|
||||
import run.halo.app.core.extension.content.Category;
|
||||
import run.halo.app.core.extension.content.Post;
|
||||
import run.halo.app.extension.Extension;
|
||||
import run.halo.app.extension.ExtensionClient;
|
||||
import run.halo.app.extension.ExtensionStoreUtil;
|
||||
import run.halo.app.extension.Metadata;
|
||||
import run.halo.app.extension.ReactiveExtensionClient;
|
||||
import run.halo.app.extension.SchemeManager;
|
||||
import run.halo.app.extension.index.IndexerFactory;
|
||||
import run.halo.app.extension.store.ReactiveExtensionStoreClient;
|
||||
|
||||
/**
|
||||
* Tests for {@link CategoryPostCountUpdater}.
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.15.0
|
||||
*/
|
||||
class CategoryPostCountUpdaterTest {
|
||||
|
||||
@Nested
|
||||
@DirtiesContext
|
||||
@SpringBootTest
|
||||
class CategoryPostCountServiceIntegrationTest {
|
||||
private final List<Post> storedPosts = posts();
|
||||
private final List<Category> storedCategories = categories();
|
||||
|
||||
@Autowired
|
||||
private SchemeManager schemeManager;
|
||||
|
||||
@SpyBean
|
||||
private ExtensionClient client;
|
||||
|
||||
@Autowired
|
||||
private ReactiveExtensionClient reactiveClient;
|
||||
|
||||
@Autowired
|
||||
private ReactiveExtensionStoreClient storeClient;
|
||||
|
||||
@Autowired
|
||||
private IndexerFactory indexerFactory;
|
||||
|
||||
private CategoryPostCountUpdater.CategoryPostCountService categoryPostCountService;
|
||||
|
||||
Mono<Extension> deleteImmediately(Extension extension) {
|
||||
var name = extension.getMetadata().getName();
|
||||
var scheme = schemeManager.get(extension.getClass());
|
||||
// un-index
|
||||
var indexer = indexerFactory.getIndexer(extension.groupVersionKind());
|
||||
indexer.unIndexRecord(extension.getMetadata().getName());
|
||||
|
||||
// delete from db
|
||||
var storeName = ExtensionStoreUtil.buildStoreName(scheme, name);
|
||||
return storeClient.delete(storeName, extension.getMetadata().getVersion())
|
||||
.thenReturn(extension);
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
categoryPostCountService =
|
||||
new CategoryPostCountUpdater.CategoryPostCountService(client);
|
||||
Flux.fromIterable(storedPosts)
|
||||
.flatMap(post -> reactiveClient.create(post))
|
||||
.as(StepVerifier::create)
|
||||
.expectNextCount(storedPosts.size())
|
||||
.verifyComplete();
|
||||
|
||||
Flux.fromIterable(storedCategories)
|
||||
.flatMap(category -> reactiveClient.create(category))
|
||||
.as(StepVerifier::create)
|
||||
.expectNextCount(storedCategories.size())
|
||||
.verifyComplete();
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void tearDown() {
|
||||
Flux.fromIterable(storedPosts)
|
||||
.flatMap(this::deleteImmediately)
|
||||
.as(StepVerifier::create)
|
||||
.expectNextCount(storedPosts.size())
|
||||
.verifyComplete();
|
||||
|
||||
Flux.fromIterable(storedCategories)
|
||||
.flatMap(this::deleteImmediately)
|
||||
.as(StepVerifier::create)
|
||||
.expectNextCount(storedCategories.size())
|
||||
.verifyComplete();
|
||||
}
|
||||
|
||||
@Test
|
||||
void reconcileStatusPostForCategoryA() {
|
||||
categoryPostCountService.recalculatePostCount(Set.of("category-A"));
|
||||
|
||||
ArgumentCaptor<Category> captor = ArgumentCaptor.forClass(Category.class);
|
||||
verify(client).update(captor.capture());
|
||||
var value = captor.getValue();
|
||||
assertThat(value.getStatusOrDefault().getPostCount()).isEqualTo(1);
|
||||
assertThat(value.getStatusOrDefault().getVisiblePostCount()).isEqualTo(0);
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
void reconcileStatusPostForCategoryB() {
|
||||
categoryPostCountService.recalculatePostCount(Set.of("category-B"));
|
||||
|
||||
ArgumentCaptor<Category> captor = ArgumentCaptor.forClass(Category.class);
|
||||
verify(client).update(captor.capture());
|
||||
var category = captor.getValue();
|
||||
assertThat(category.getStatusOrDefault().getPostCount()).isEqualTo(1);
|
||||
assertThat(category.getStatusOrDefault().getVisiblePostCount()).isEqualTo(0);
|
||||
}
|
||||
|
||||
@Test
|
||||
void reconcileStatusPostForCategoryC() {
|
||||
categoryPostCountService.recalculatePostCount(Set.of("category-C"));
|
||||
|
||||
ArgumentCaptor<Category> captor = ArgumentCaptor.forClass(Category.class);
|
||||
verify(client).update(captor.capture());
|
||||
var value = captor.getValue();
|
||||
assertThat(value.getStatusOrDefault().getPostCount()).isEqualTo(2);
|
||||
assertThat(value.getStatusOrDefault().getVisiblePostCount()).isEqualTo(0);
|
||||
}
|
||||
|
||||
@Test
|
||||
void reconcileStatusPostForCategoryD() {
|
||||
categoryPostCountService.recalculatePostCount(Set.of("category-D"));
|
||||
|
||||
ArgumentCaptor<Category> captor = ArgumentCaptor.forClass(Category.class);
|
||||
verify(client).update(captor.capture());
|
||||
var value = captor.getValue();
|
||||
assertThat(value.getStatusOrDefault().postCount).isEqualTo(1);
|
||||
assertThat(value.getStatusOrDefault().visiblePostCount).isEqualTo(0);
|
||||
}
|
||||
|
||||
private List<Category> categories() {
|
||||
/*
|
||||
* |-A(post-4)
|
||||
* |-B(post-3)
|
||||
* |-|-C(post-2,post-1)
|
||||
* |-D(post-1)
|
||||
*/
|
||||
Category categoryA = category("category-A");
|
||||
categoryA.getSpec().setChildren(List.of("category-B", "category-D"));
|
||||
|
||||
Category categoryB = category("category-B");
|
||||
categoryB.getSpec().setChildren(List.of("category-C"));
|
||||
|
||||
Category categoryC = category("category-C");
|
||||
Category categoryD = category("category-D");
|
||||
return List.of(categoryA, categoryB, categoryC, categoryD);
|
||||
}
|
||||
|
||||
private Category category(String name) {
|
||||
Category category = new Category();
|
||||
Metadata metadata = new Metadata();
|
||||
metadata.setName(name);
|
||||
category.setMetadata(metadata);
|
||||
category.setSpec(new Category.CategorySpec());
|
||||
category.setStatus(new Category.CategoryStatus());
|
||||
|
||||
category.getSpec().setDisplayName("display-name");
|
||||
category.getSpec().setSlug("slug");
|
||||
category.getSpec().setPriority(0);
|
||||
return category;
|
||||
}
|
||||
|
||||
private List<Post> posts() {
|
||||
/*
|
||||
* |-A(post-4)
|
||||
* |-B(post-3)
|
||||
* |-|-C(post-2,post-1)
|
||||
* |-D(post-1)
|
||||
*/
|
||||
Post post1 = fakePost();
|
||||
post1.getMetadata().setName("post-1");
|
||||
post1.getSpec().setCategories(List.of("category-D", "category-C"));
|
||||
post1.getSpec().setVisible(Post.VisibleEnum.PUBLIC);
|
||||
|
||||
Post post2 = fakePost();
|
||||
post2.getMetadata().setName("post-2");
|
||||
post2.getSpec().setCategories(List.of("category-C"));
|
||||
post2.getSpec().setVisible(Post.VisibleEnum.PUBLIC);
|
||||
|
||||
Post post3 = fakePost();
|
||||
post3.getMetadata().setName("post-3");
|
||||
post3.getSpec().setCategories(List.of("category-B"));
|
||||
post3.getSpec().setVisible(Post.VisibleEnum.PUBLIC);
|
||||
|
||||
Post post4 = fakePost();
|
||||
post4.getMetadata().setName("post-4");
|
||||
post4.getSpec().setCategories(List.of("category-A"));
|
||||
post4.getSpec().setVisible(Post.VisibleEnum.PUBLIC);
|
||||
return List.of(post1, post2, post3, post4);
|
||||
}
|
||||
|
||||
Post fakePost() {
|
||||
var post = TestPost.postV1();
|
||||
post.getSpec().setAllowComment(true);
|
||||
post.getSpec().setDeleted(false);
|
||||
post.getSpec().setExcerpt(new Post.Excerpt());
|
||||
post.getSpec().getExcerpt().setAutoGenerate(false);
|
||||
post.getSpec().setPinned(false);
|
||||
post.getSpec().setPriority(0);
|
||||
post.getSpec().setPublish(false);
|
||||
post.getSpec().setSlug("fake-post");
|
||||
return post;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,230 +0,0 @@
|
|||
package run.halo.app.core.extension.reconciler;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.boot.test.mock.mockito.SpyBean;
|
||||
import org.springframework.test.annotation.DirtiesContext;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.test.StepVerifier;
|
||||
import run.halo.app.content.TestPost;
|
||||
import run.halo.app.core.extension.content.Category;
|
||||
import run.halo.app.core.extension.content.Post;
|
||||
import run.halo.app.extension.Extension;
|
||||
import run.halo.app.extension.ExtensionClient;
|
||||
import run.halo.app.extension.ExtensionStoreUtil;
|
||||
import run.halo.app.extension.Metadata;
|
||||
import run.halo.app.extension.ReactiveExtensionClient;
|
||||
import run.halo.app.extension.SchemeManager;
|
||||
import run.halo.app.extension.controller.Reconciler;
|
||||
import run.halo.app.extension.index.IndexerFactory;
|
||||
import run.halo.app.extension.store.ReactiveExtensionStoreClient;
|
||||
|
||||
/**
|
||||
* Tests for {@link CategoryReconciler}.
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.0.0
|
||||
*/
|
||||
@DirtiesContext
|
||||
@SpringBootTest
|
||||
class CategoryReconcilerIntegrationTest {
|
||||
private final List<Post> storedPosts = posts();
|
||||
private final List<Category> storedCategories = categories();
|
||||
|
||||
@Autowired
|
||||
private SchemeManager schemeManager;
|
||||
|
||||
@SpyBean
|
||||
private ExtensionClient client;
|
||||
|
||||
@Autowired
|
||||
private ReactiveExtensionClient reactiveClient;
|
||||
|
||||
@Autowired
|
||||
private ReactiveExtensionStoreClient storeClient;
|
||||
|
||||
@Autowired
|
||||
private IndexerFactory indexerFactory;
|
||||
|
||||
@Autowired
|
||||
private CategoryReconciler categoryReconciler;
|
||||
|
||||
Mono<Extension> deleteImmediately(Extension extension) {
|
||||
var name = extension.getMetadata().getName();
|
||||
var scheme = schemeManager.get(extension.getClass());
|
||||
// un-index
|
||||
var indexer = indexerFactory.getIndexer(extension.groupVersionKind());
|
||||
indexer.unIndexRecord(extension.getMetadata().getName());
|
||||
|
||||
// delete from db
|
||||
var storeName = ExtensionStoreUtil.buildStoreName(scheme, name);
|
||||
return storeClient.delete(storeName, extension.getMetadata().getVersion())
|
||||
.thenReturn(extension);
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
Flux.fromIterable(storedPosts)
|
||||
.flatMap(post -> reactiveClient.create(post))
|
||||
.as(StepVerifier::create)
|
||||
.expectNextCount(storedPosts.size())
|
||||
.verifyComplete();
|
||||
|
||||
Flux.fromIterable(storedCategories)
|
||||
.flatMap(category -> reactiveClient.create(category))
|
||||
.as(StepVerifier::create)
|
||||
.expectNextCount(storedCategories.size())
|
||||
.verifyComplete();
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void tearDown() {
|
||||
Flux.fromIterable(storedPosts)
|
||||
.flatMap(this::deleteImmediately)
|
||||
.as(StepVerifier::create)
|
||||
.expectNextCount(storedPosts.size())
|
||||
.verifyComplete();
|
||||
|
||||
Flux.fromIterable(storedCategories)
|
||||
.flatMap(this::deleteImmediately)
|
||||
.as(StepVerifier::create)
|
||||
.expectNextCount(storedCategories.size())
|
||||
.verifyComplete();
|
||||
}
|
||||
|
||||
@Test
|
||||
void reconcileStatusPostForCategoryA() {
|
||||
reconcileStatusPostPilling("category-A");
|
||||
|
||||
ArgumentCaptor<Category> captor = ArgumentCaptor.forClass(Category.class);
|
||||
verify(client, times(3)).update(captor.capture());
|
||||
var values = captor.getAllValues();
|
||||
assertThat(values.get(2).getStatusOrDefault().getPostCount()).isEqualTo(4);
|
||||
assertThat(
|
||||
captor.getAllValues().get(2).getStatusOrDefault().getVisiblePostCount()).isEqualTo(0);
|
||||
}
|
||||
|
||||
@Test
|
||||
void reconcileStatusPostForCategoryB() {
|
||||
reconcileStatusPostPilling("category-B");
|
||||
ArgumentCaptor<Category> captor = ArgumentCaptor.forClass(Category.class);
|
||||
verify(client, times(3)).update(captor.capture());
|
||||
Category category = captor.getAllValues().get(2);
|
||||
assertThat(category.getStatusOrDefault().getPostCount()).isEqualTo(3);
|
||||
assertThat(category.getStatusOrDefault().getVisiblePostCount()).isEqualTo(0);
|
||||
}
|
||||
|
||||
@Test
|
||||
void reconcileStatusPostForCategoryC() {
|
||||
reconcileStatusPostPilling("category-C");
|
||||
ArgumentCaptor<Category> captor = ArgumentCaptor.forClass(Category.class);
|
||||
verify(client, times(3)).update(captor.capture());
|
||||
var value = captor.getAllValues().get(2);
|
||||
assertThat(value.getStatusOrDefault().getPostCount()).isEqualTo(2);
|
||||
assertThat(value.getStatusOrDefault().getVisiblePostCount()).isEqualTo(0);
|
||||
}
|
||||
|
||||
@Test
|
||||
void reconcileStatusPostForCategoryD() {
|
||||
reconcileStatusPostPilling("category-D");
|
||||
ArgumentCaptor<Category> captor = ArgumentCaptor.forClass(Category.class);
|
||||
verify(client, times(3)).update(captor.capture());
|
||||
var value = captor.getAllValues().get(2);
|
||||
assertThat(value.getStatusOrDefault().postCount).isEqualTo(1);
|
||||
assertThat(value.getStatusOrDefault().visiblePostCount).isEqualTo(0);
|
||||
}
|
||||
|
||||
private void reconcileStatusPostPilling(String reconcileCategoryName) {
|
||||
Reconciler.Result result =
|
||||
categoryReconciler.reconcile(new Reconciler.Request(reconcileCategoryName));
|
||||
assertThat(result.reEnqueue()).isTrue();
|
||||
assertThat(result.retryAfter()).isEqualTo(Duration.ofMinutes(1));
|
||||
}
|
||||
|
||||
private List<Category> categories() {
|
||||
/*
|
||||
* |-A(post-4)
|
||||
* |-B(post-3)
|
||||
* |-|-C(post-2,post-1)
|
||||
* |-D(post-1)
|
||||
*/
|
||||
Category categoryA = category("category-A");
|
||||
categoryA.getSpec().setChildren(List.of("category-B", "category-D"));
|
||||
|
||||
Category categoryB = category("category-B");
|
||||
categoryB.getSpec().setChildren(List.of("category-C"));
|
||||
|
||||
Category categoryC = category("category-C");
|
||||
Category categoryD = category("category-D");
|
||||
return List.of(categoryA, categoryB, categoryC, categoryD);
|
||||
}
|
||||
|
||||
private Category category(String name) {
|
||||
Category category = new Category();
|
||||
Metadata metadata = new Metadata();
|
||||
metadata.setName(name);
|
||||
category.setMetadata(metadata);
|
||||
category.getMetadata().setFinalizers(Set.of(CategoryReconciler.FINALIZER_NAME));
|
||||
category.setSpec(new Category.CategorySpec());
|
||||
category.setStatus(new Category.CategoryStatus());
|
||||
|
||||
category.getSpec().setDisplayName("display-name");
|
||||
category.getSpec().setSlug("slug");
|
||||
category.getSpec().setPriority(0);
|
||||
return category;
|
||||
}
|
||||
|
||||
private List<Post> posts() {
|
||||
/*
|
||||
* |-A(post-4)
|
||||
* |-B(post-3)
|
||||
* |-|-C(post-2,post-1)
|
||||
* |-D(post-1)
|
||||
*/
|
||||
Post post1 = fakePost();
|
||||
post1.getMetadata().setName("post-1");
|
||||
post1.getSpec().setCategories(List.of("category-D", "category-C"));
|
||||
post1.getSpec().setVisible(Post.VisibleEnum.PUBLIC);
|
||||
|
||||
Post post2 = fakePost();
|
||||
post2.getMetadata().setName("post-2");
|
||||
post2.getSpec().setCategories(List.of("category-C"));
|
||||
post2.getSpec().setVisible(Post.VisibleEnum.PUBLIC);
|
||||
|
||||
Post post3 = fakePost();
|
||||
post3.getMetadata().setName("post-3");
|
||||
post3.getSpec().setCategories(List.of("category-B"));
|
||||
post3.getSpec().setVisible(Post.VisibleEnum.PUBLIC);
|
||||
|
||||
Post post4 = fakePost();
|
||||
post4.getMetadata().setName("post-4");
|
||||
post4.getSpec().setCategories(List.of("category-A"));
|
||||
post4.getSpec().setVisible(Post.VisibleEnum.PUBLIC);
|
||||
return List.of(post1, post2, post3, post4);
|
||||
}
|
||||
|
||||
Post fakePost() {
|
||||
var post = TestPost.postV1();
|
||||
post.getSpec().setAllowComment(true);
|
||||
post.getSpec().setDeleted(false);
|
||||
post.getSpec().setExcerpt(new Post.Excerpt());
|
||||
post.getSpec().getExcerpt().setAutoGenerate(false);
|
||||
post.getSpec().setPinned(false);
|
||||
post.getSpec().setPriority(0);
|
||||
post.getSpec().setPublish(false);
|
||||
post.getSpec().setSlug("fake-post");
|
||||
return post;
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue