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 the update method for the number of tag associated with posts #5422

Merged
merged 2 commits into from Mar 12, 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 @@ -41,7 +41,8 @@ public class Post extends AbstractExtension {
public static final String CATEGORIES_ANNO = "content.halo.run/categories";
public static final String LAST_RELEASED_SNAPSHOT_ANNO =
"content.halo.run/last-released-snapshot";
public static final String TAGS_ANNO = "content.halo.run/tags";
public static final String LAST_ASSOCIATED_TAGS_ANNO = "content.halo.run/last-associated-tags";

public static final String DELETED_LABEL = "content.halo.run/deleted";
public static final String PUBLISHED_LABEL = "content.halo.run/published";
public static final String OWNER_LABEL = "content.halo.run/owner";
Expand Down
Expand Up @@ -27,6 +27,8 @@ public class Tag extends AbstractExtension {

public static final GroupVersionKind GVK = GroupVersionKind.fromExtension(Tag.class);

public static final String REQUIRE_SYNC_ON_STARTUP_INDEX_NAME = "requireSyncOnStartup";

@Schema(requiredMode = REQUIRED)
private TagSpec spec;

Expand Down Expand Up @@ -77,5 +79,7 @@ public static class TagStatus {
public Integer visiblePostCount;

public Integer postCount;

private Long observedVersion;
}
}
@@ -0,0 +1,186 @@
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.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;
import run.halo.app.core.extension.content.Tag;
import run.halo.app.core.extension.content.Tag.TagStatus;
import run.halo.app.event.post.PostDeletedEvent;
import run.halo.app.event.post.PostEvent;
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.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;

/**
* Update {@link TagStatus#postCount} when post related to tag is updated.
*
* @author guqing
* @since 2.13.0
*/
@Component
public class TagPostCountUpdater
implements Reconciler<TagPostCountUpdater.PostRelatedTags>, SmartLifecycle {

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);
}

@Override
public Result reconcile(PostRelatedTags postRelatedTags) {
for (var tag : postRelatedTags.tags()) {
updateTagRelatedPostCount(tag);
}

// Update last associated tags when handled
client.fetch(Post.class, postRelatedTags.postName()).ifPresent(post -> {
var tags = defaultIfNull(post.getSpec().getTags(), List.<String>of());
var annotations = MetadataUtil.nullSafeAnnotations(post);
var tagAnno = JsonUtils.objectToJson(tags);
var oldTagAnno = annotations.get(Post.LAST_ASSOCIATED_TAGS_ANNO);

if (!tagAnno.equals(oldTagAnno)) {
annotations.put(Post.LAST_ASSOCIATED_TAGS_ANNO, tagAnno);
client.update(post);
}
});
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.
*/
@EventListener(PostEvent.class)
public void onPostUpdated(PostEvent postEvent) {
var postName = postEvent.getName();
if (postEvent instanceof PostUpdatedEvent) {
var tagsToUpdate = calcTagsToUpdate(postEvent.getName());
tagQueue.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)));
}
}

private Set<String> calcTagsToUpdate(String postName) {
var post = client.fetch(Post.class, postName).orElseThrow();
var annotations = MetadataUtil.nullSafeAnnotations(post);
var oldTags = Optional.ofNullable(annotations.get(Post.LAST_ASSOCIATED_TAGS_ANNO))
.filter(StringUtils::isNotBlank)
.map(tagsJson -> JsonUtils.jsonToObject(tagsJson, String[].class))
.orElse(new String[0]);

var tagsToUpdate = Sets.newHashSet(oldTags);
var newTags = post.getSpec().getTags();
if (newTags != null) {
tagsToUpdate.addAll(newTags);
}
return tagsToUpdate;
}

public record PostRelatedTags(String postName, Set<String> tags) {
}

private void updateTagRelatedPostCount(String tagName) {
client.fetch(Tag.class, tagName).ifPresent(tag -> {
var commonFieldQuery = and(
equal("spec.tags", tag.getMetadata().getName()),
isNull("metadata.deletionTimestamp")
);
// Update post count
var allPostOptions = new ListOptions();
allPostOptions.setFieldSelector(FieldSelector.of(commonFieldQuery));
var result = client.listBy(Post.class, allPostOptions, PageRequestImpl.ofSize(1));
tag.getStatusOrDefault().setPostCount((int) result.getTotal());

// Update visible post count
var publicPostOptions = new ListOptions();
publicPostOptions.setLabelSelector(LabelSelector.builder()
.eq(Post.PUBLISHED_LABEL, "true")
.build());
publicPostOptions.setFieldSelector(FieldSelector.of(
and(
commonFieldQuery,
equal("spec.deleted", "false"),
equal("spec.visible", Post.VisibleEnum.PUBLIC.name())
)
));
var publicPosts =
client.listBy(Post.class, publicPostOptions, PageRequestImpl.ofSize(1));
tag.getStatusOrDefault().setVisiblePostCount((int) publicPosts.getTotal());

client.update(tag);
});
}
}
Expand Up @@ -33,6 +33,7 @@
import run.halo.app.core.extension.content.Post.VisibleEnum;
import run.halo.app.core.extension.content.Snapshot;
import run.halo.app.core.extension.notification.Subscription;
import run.halo.app.event.post.PostDeletedEvent;
import run.halo.app.event.post.PostPublishedEvent;
import run.halo.app.event.post.PostUnpublishedEvent;
import run.halo.app.event.post.PostUpdatedEvent;
Expand Down Expand Up @@ -86,6 +87,7 @@ public Result reconcile(Request request) {
if (ExtensionOperator.isDeleted(post)) {
removeFinalizers(post.getMetadata(), Set.of(FINALIZER_NAME));
unPublishPost(post, events);
events.add(new PostDeletedEvent(this, post));
cleanUpResources(post);
// update post to be able to be collected by gc collector.
client.update(post);
Expand Down Expand Up @@ -126,7 +128,7 @@ public Result reconcile(Request request) {

// calculate the sha256sum
var configSha256sum = Hashing.sha256().hashString(post.getSpec().toString(), UTF_8)
.toString();
.toString();

var oldConfigChecksum = annotations.get(Constant.CHECKSUM_CONFIG_ANNO);
if (!Objects.equals(oldConfigChecksum, configSha256sum)) {
Expand Down