refactor: dont store ap pids in posts:pid

new zset postsRemote:pid
in topic purge only make 1 db call to reduce counters
This commit is contained in:
Barış Soner Uşaklı
2026-02-24 17:36:17 -05:00
parent 1d48a6c06c
commit de1f04d9d8
9 changed files with 108 additions and 15 deletions

View File

@@ -6,6 +6,8 @@
"new-users": "New Users",
"posts": "Posts",
"topics": "Topics",
"remote-posts": "Remote Posts",
"remote-topics": "Remote Topics",
"page-views-seven": "Last 7 Days",
"page-views-thirty": "Last 30 Days",
"page-views-last-day": "Last 24 hours",

View File

@@ -130,13 +130,15 @@ async function getStats() {
return cachedStats;
}
let results = await Promise.all([
let results = (await Promise.all([
getStatsFromAnalytics('uniquevisitors', ''),
getStatsFromAnalytics('logins', 'loginCount'),
getStatsForSet('users:joindate', 'userCount'),
getStatsForSet('posts:pid', 'postCount'),
getStatsForSet('topics:tid', 'topicCount'),
]);
meta.config.activitypubEnabled ? getStatsForSet('postsRemote:pid', '') : null,
meta.config.activitypubEnabled ? getStatsForSet('topicsRemote:tid', '') : null,
])).filter(Boolean);
results[0].name = '[[admin/dashboard:unique-visitors]]';
@@ -151,6 +153,13 @@ async function getStats() {
results[4].name = '[[admin/dashboard:topics]]';
results[4].href = `${nconf.get('relative_path')}/admin/dashboard/topics`;
if (results[5]) {
results[5].name = '[[admin/dashboard:remote-posts]]';
}
if (results[6]) {
results[6].name = '[[admin/dashboard:remote-topics]]';
}
({ results } = await plugins.hooks.fire('filter:admin.getStats', {
results,
helpers: { getStatsForSet, getStatsFromAnalytics },

View File

@@ -59,10 +59,10 @@ module.exports = function (Posts) {
const topicData = await topics.getTopicFields(tid, ['cid', 'pinned']);
postData.cid = topicData.cid;
const isRemote = !utils.isNumber(pid);
await Promise.all([
db.sortedSetAdd('posts:pid', timestamp, postData.pid),
utils.isNumber(pid) ? db.incrObjectField('global', 'postCount') : null,
db.sortedSetAdd(!isRemote ? 'posts:pid' : 'postsRemote:pid', timestamp, postData.pid),
!isRemote ? db.incrObjectField('global', 'postCount') : null,
user.onNewPostMade(postData),
topics.onNewPostMade(postData),
categories.onNewPostMade(topicData.cid, topicData.pinned, postData),

View File

@@ -79,7 +79,9 @@ module.exports = function (Posts) {
deleteFromGroups(pids),
deleteDiffs(pids),
deleteFromUploads(pids),
db.sortedSetsRemove(['posts:pid', 'posts:votes', 'posts:flagged'], pids),
db.sortedSetsRemove([
'posts:pid', 'posts:votes', 'posts:flagged', 'postsRemote:pid',
], pids),
Posts.attachments.empty(pids),
activitypub.notes.delete(pids),
db.deleteAll(pids.map(pid => `pid:${pid}:editors`)),

View File

@@ -260,7 +260,9 @@ module.exports = function (Posts) {
}
await Promise.all([
updateTopicVoteCount(postData),
db.sortedSetAdd('posts:votes', postData.votes, postData.pid),
utils.isNumber(postData.pid) ?
db.sortedSetAdd('posts:votes', postData.votes, postData.pid) :
null,
Posts.setPostFields(postData.pid, {
upvotes: postData.upvotes,
downvotes: postData.downvotes,

View File

@@ -138,15 +138,15 @@ module.exports = function (Topics) {
async function reduceCounters(tid) {
const incr = -1;
const { cid, postcount } = await Topics.getTopicFields(tid, ['cid', 'postcount']);
const postCountChange = incr * postcount;
const categoryKey = `${utils.isNumber(cid) ? 'category' : 'categoryRemote'}:${cid}`;
const bulkIncr = [
[categoryKey, { post_count: postCountChange, topic_count: incr }],
];
if (utils.isNumber(tid)) {
await db.incrObjectFieldBy('global', 'topicCount', incr);
bulkIncr.push(['global', { postCount: postCountChange, topicCount: incr }]);
}
const topicData = await Topics.getTopicFields(tid, ['cid', 'postcount']);
const postCountChange = incr * topicData.postcount;
await Promise.all([
db.incrObjectFieldBy('global', 'postCount', postCountChange),
db.incrObjectFieldBy(`${utils.isNumber(topicData.cid) ? 'category' : 'categoryRemote'}:${topicData.cid}`, 'post_count', postCountChange),
db.incrObjectFieldBy(`${utils.isNumber(topicData.cid) ? 'category' : 'categoryRemote'}:${topicData.cid}`, 'topic_count', incr),
]);
await db.incrObjectFieldByBulk(bulkIncr);
}
};

View File

@@ -0,0 +1,37 @@
'use strict';
const db = require('../../database');
const batch = require('../../batch');
const utils = require('../../utils');
module.exports = {
name: 'Move ap pids from posts:pid to postsRemote:pid',
timestamp: Date.UTC(2026, 1, 24),
method: async function () {
const { progress } = this;
progress.total = await db.sortedSetCard('posts:pid');
const removePosts = [];
await batch.processSortedSet('posts:pid', async (postData) => {
const apPosts = postData.filter(post => !utils.isNumber(post.value));
removePosts.push(...apPosts.map(post => post.value));
await db.sortedSetAdd(
'postsRemote:pid',
apPosts.map(p => p.score),
apPosts.map(p => p.value)
);
progress.incr(postData.length);
}, {
batch: 500,
withScores: true,
});
await batch.processArray(removePosts, async (pids) => {
await db.sortedSetsRemove(['posts:pid', 'posts:votes'], pids);
}, {
batch: 500,
});
const postCount = await db.sortedSetCard('posts:pid');
await db.setObjectField('global', 'postCount', postCount);
},
};

View File

@@ -30,5 +30,7 @@ module.exports = {
}, {
batch: 500,
});
const topicCount = await db.sortedSetCard('topics:tid');
await db.setObjectField('global', 'topicCount', topicCount);
},
};

View File

@@ -782,6 +782,45 @@ describe('Topic\'s', () => {
assert.strictEqual(false, isMember);
});
it('should update global & category topic/post counters when topic is purged', async () => {
const category = await categories.create({
name: 'Category for purge count test',
});
const { topicCount, postCount } = await db.getObject('global');
const cid = category.cid;
const topic1 = await topics.post({
uid: adminUid,
title: 'topic for purge count test',
content: 'topic content',
cid,
});
await topics.post({
uid: adminUid,
title: 'topic for purge count test',
content: 'topic content',
cid,
});
const tid1 = topic1.topicData.tid;
await topics.reply({ uid: adminUid, content: 'reply 1', tid: tid1 });
await topics.reply({ uid: adminUid, content: 'reply 2', tid: tid1 });
await topics.reply({ uid: adminUid, content: 'reply 3', tid: tid1 });
let categoryData = await categories.getCategoriesFields([cid], ['topic_count', 'post_count']);
assert.strictEqual(categoryData[0].topic_count, 2);
assert.strictEqual(categoryData[0].post_count, 5);
await apiTopics.purge({ uid: adminUid }, { tids: [tid1], cid: categoryObj.cid });
categoryData = await categories.getCategoriesFields([cid], ['topic_count', 'post_count']);
assert.strictEqual(categoryData[0].topic_count, 1);
assert.strictEqual(categoryData[0].post_count, 1);
const afterPurge = await db.getObject('global');
assert.strictEqual(parseInt(afterPurge.topicCount, 10), parseInt(topicCount, 10) + 1);
assert.strictEqual(parseInt(afterPurge.postCount, 10), parseInt(postCount, 10) + 1);
assert(false);
});
it('should not allow user to restore their topic if it was deleted by an admin', async () => {
const result = await topics.post({
uid: fooUid,