enhance: improve federation chart

This commit is contained in:
syuilo 2022-02-10 17:45:12 +09:00
parent 7ba5512a65
commit 0afebcfd9e
25 changed files with 199 additions and 64 deletions

View file

@ -0,0 +1,31 @@
const { MigrationInterface, QueryRunner } = require("typeorm");
module.exports = class chartV151644481657998 {
name = 'chartV151644481657998'
async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "__chart__federation" DROP COLUMN "___instance_total"`);
await queryRunner.query(`ALTER TABLE "__chart__federation" DROP COLUMN "___instance_inc"`);
await queryRunner.query(`ALTER TABLE "__chart__federation" DROP COLUMN "___instance_dec"`);
await queryRunner.query(`ALTER TABLE "__chart_day__federation" DROP COLUMN "___instance_total"`);
await queryRunner.query(`ALTER TABLE "__chart_day__federation" DROP COLUMN "___instance_inc"`);
await queryRunner.query(`ALTER TABLE "__chart_day__federation" DROP COLUMN "___instance_dec"`);
await queryRunner.query(`ALTER TABLE "__chart__federation" ADD "___sub" smallint NOT NULL DEFAULT '0'`);
await queryRunner.query(`ALTER TABLE "__chart__federation" ADD "___pub" smallint NOT NULL DEFAULT '0'`);
await queryRunner.query(`ALTER TABLE "__chart_day__federation" ADD "___sub" smallint NOT NULL DEFAULT '0'`);
await queryRunner.query(`ALTER TABLE "__chart_day__federation" ADD "___pub" smallint NOT NULL DEFAULT '0'`);
}
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "__chart_day__federation" DROP COLUMN "___pub"`);
await queryRunner.query(`ALTER TABLE "__chart_day__federation" DROP COLUMN "___sub"`);
await queryRunner.query(`ALTER TABLE "__chart__federation" DROP COLUMN "___pub"`);
await queryRunner.query(`ALTER TABLE "__chart__federation" DROP COLUMN "___sub"`);
await queryRunner.query(`ALTER TABLE "__chart_day__federation" ADD "___instance_dec" smallint NOT NULL DEFAULT '0'`);
await queryRunner.query(`ALTER TABLE "__chart_day__federation" ADD "___instance_inc" smallint NOT NULL DEFAULT '0'`);
await queryRunner.query(`ALTER TABLE "__chart_day__federation" ADD "___instance_total" integer NOT NULL DEFAULT '0'`);
await queryRunner.query(`ALTER TABLE "__chart__federation" ADD "___instance_dec" smallint NOT NULL DEFAULT '0'`);
await queryRunner.query(`ALTER TABLE "__chart__federation" ADD "___instance_inc" smallint NOT NULL DEFAULT '0'`);
await queryRunner.query(`ALTER TABLE "__chart__federation" ADD "___instance_total" integer NOT NULL DEFAULT '0'`);
}
}

View file

@ -258,6 +258,11 @@ export default function() {
processDb(dbQueue); processDb(dbQueue);
processObjectStorage(objectStorageQueue); processObjectStorage(objectStorageQueue);
systemQueue.add('tickCharts', {
}, {
repeat: { cron: '55 * * * *' },
});
systemQueue.add('resyncCharts', { systemQueue.add('resyncCharts', {
}, { }, {
repeat: { cron: '0 0 * * *' }, repeat: { cron: '0 0 * * *' },

View file

@ -1,8 +1,10 @@
import * as Bull from 'bull'; import * as Bull from 'bull';
import { tickCharts } from './tick-charts';
import { resyncCharts } from './resync-charts'; import { resyncCharts } from './resync-charts';
import { cleanCharts } from './clean-charts'; import { cleanCharts } from './clean-charts';
const jobs = { const jobs = {
tickCharts,
resyncCharts, resyncCharts,
cleanCharts, cleanCharts,
} as Record<string, Bull.ProcessCallbackFunction<Record<string, unknown>> | Bull.ProcessPromiseFunction<Record<string, unknown>>>; } as Record<string, Bull.ProcessCallbackFunction<Record<string, unknown>> | Bull.ProcessPromiseFunction<Record<string, unknown>>>;

View file

@ -0,0 +1,28 @@
import * as Bull from 'bull';
import { queueLogger } from '../../logger';
import { activeUsersChart, driveChart, federationChart, hashtagChart, instanceChart, notesChart, perUserDriveChart, perUserFollowingChart, perUserNotesChart, perUserReactionsChart, usersChart, apRequestChart } from '@/services/chart/index';
const logger = queueLogger.createSubLogger('tick-charts');
export async function tickCharts(job: Bull.Job<Record<string, unknown>>, done: any): Promise<void> {
logger.info(`Tick charts...`);
await Promise.all([
federationChart.tick(false),
notesChart.tick(false),
usersChart.tick(false),
activeUsersChart.tick(false),
instanceChart.tick(false),
perUserNotesChart.tick(false),
driveChart.tick(false),
perUserReactionsChart.tick(false),
hashtagChart.tick(false),
perUserFollowingChart.tick(false),
perUserDriveChart.tick(false),
apRequestChart.tick(false),
]);
logger.succ(`All charts successfully ticked.`);
done();
}

View file

@ -1,22 +0,0 @@
import define from '../../define';
import { driveChart, notesChart, usersChart } from '@/services/chart/index';
import { insertModerationLog } from '@/services/insert-moderation-log';
export const meta = {
tags: ['admin'],
requireCredential: true,
requireModerator: true,
} as const;
// eslint-disable-next-line import/no-default-export
export default define(meta, async (ps, me) => {
insertModerationLog(me, 'chartResync');
driveChart.resync();
notesChart.resync();
usersChart.resync();
// TODO: ユーザーごとのチャートもキューに入れて更新する
// TODO: インスタンスごとのチャートもキューに入れて更新する
});

View file

@ -18,7 +18,12 @@ export default class ActiveUsersChart extends Chart<typeof schema> {
} }
@autobind @autobind
protected async queryCurrentState(): Promise<Partial<KVs<typeof schema>>> { protected async tickMajor(): Promise<Partial<KVs<typeof schema>>> {
return {};
}
@autobind
protected async tickMinor(): Promise<Partial<KVs<typeof schema>>> {
return {}; return {};
} }

View file

@ -12,7 +12,12 @@ export default class ApRequestChart extends Chart<typeof schema> {
} }
@autobind @autobind
protected async queryCurrentState(): Promise<Partial<KVs<typeof schema>>> { protected async tickMajor(): Promise<Partial<KVs<typeof schema>>> {
return {};
}
@autobind
protected async tickMinor(): Promise<Partial<KVs<typeof schema>>> {
return {}; return {};
} }

View file

@ -15,7 +15,12 @@ export default class DriveChart extends Chart<typeof schema> {
} }
@autobind @autobind
protected async queryCurrentState(): Promise<Partial<KVs<typeof schema>>> { protected async tickMajor(): Promise<Partial<KVs<typeof schema>>> {
return {};
}
@autobind
protected async tickMinor(): Promise<Partial<KVs<typeof schema>>> {
return {}; return {};
} }

View file

@ -3,12 +3,11 @@ import Chart from '../../core';
export const name = 'federation'; export const name = 'federation';
export const schema = { export const schema = {
'instance.total': { accumulate: true },
'instance.inc': { range: 'small' },
'instance.dec': { range: 'small' },
'deliveredInstances': { uniqueIncrement: true, range: 'small' }, 'deliveredInstances': { uniqueIncrement: true, range: 'small' },
'inboxInstances': { uniqueIncrement: true, range: 'small' }, 'inboxInstances': { uniqueIncrement: true, range: 'small' },
'stalled': { uniqueIncrement: true, range: 'small' }, 'stalled': { uniqueIncrement: true, range: 'small' },
'sub': { accumulate: true, range: 'small' },
'pub': { accumulate: true, range: 'small' },
} as const; } as const;
export const entity = Chart.schemaToEntity(name, schema); export const entity = Chart.schemaToEntity(name, schema);

View file

@ -1,6 +1,6 @@
import autobind from 'autobind-decorator'; import autobind from 'autobind-decorator';
import Chart, { KVs } from '../core'; import Chart, { KVs } from '../core';
import { Instances } from '@/models/index'; import { Followings } from '@/models/index';
import { name, schema } from './entities/federation'; import { name, schema } from './entities/federation';
/** /**
@ -13,23 +13,30 @@ export default class FederationChart extends Chart<typeof schema> {
} }
@autobind @autobind
protected async queryCurrentState(): Promise<Partial<KVs<typeof schema>>> { protected async tickMajor(): Promise<Partial<KVs<typeof schema>>> {
const [total] = await Promise.all([
Instances.count({}),
]);
return { return {
'instance.total': total,
}; };
} }
@autobind @autobind
public async update(isAdditional: boolean): Promise<void> { protected async tickMinor(): Promise<Partial<KVs<typeof schema>>> {
await this.commit({ const [sub, pub] = await Promise.all([
'instance.total': isAdditional ? 1 : -1, Followings.createQueryBuilder('following')
'instance.inc': isAdditional ? 1 : 0, .select('COUNT(DISTINCT following.followeeHost)')
'instance.dec': isAdditional ? 0 : 1, .where('following.followeeHost IS NOT NULL')
}); .getRawOne()
.then(x => parseInt(x.count, 10)),
Followings.createQueryBuilder('following')
.select('COUNT(DISTINCT following.followerHost)')
.where('following.followerHost IS NOT NULL')
.getRawOne()
.then(x => parseInt(x.count, 10)),
]);
return {
'sub': sub,
'pub': pub,
};
} }
@autobind @autobind

View file

@ -14,7 +14,12 @@ export default class HashtagChart extends Chart<typeof schema> {
} }
@autobind @autobind
protected async queryCurrentState(): Promise<Partial<KVs<typeof schema>>> { protected async tickMajor(): Promise<Partial<KVs<typeof schema>>> {
return {};
}
@autobind
protected async tickMinor(): Promise<Partial<KVs<typeof schema>>> {
return {}; return {};
} }

View file

@ -16,7 +16,7 @@ export default class InstanceChart extends Chart<typeof schema> {
} }
@autobind @autobind
protected async queryCurrentState(group: string): Promise<Partial<KVs<typeof schema>>> { protected async tickMajor(group: string): Promise<Partial<KVs<typeof schema>>> {
const [ const [
notesCount, notesCount,
usersCount, usersCount,
@ -42,6 +42,11 @@ export default class InstanceChart extends Chart<typeof schema> {
}; };
} }
@autobind
protected async tickMinor(): Promise<Partial<KVs<typeof schema>>> {
return {};
}
@autobind @autobind
public async requestReceived(host: string): Promise<void> { public async requestReceived(host: string): Promise<void> {
await this.commit({ await this.commit({

View file

@ -15,7 +15,7 @@ export default class NotesChart extends Chart<typeof schema> {
} }
@autobind @autobind
protected async queryCurrentState(): Promise<Partial<KVs<typeof schema>>> { protected async tickMajor(): Promise<Partial<KVs<typeof schema>>> {
const [localCount, remoteCount] = await Promise.all([ const [localCount, remoteCount] = await Promise.all([
Notes.count({ userHost: null }), Notes.count({ userHost: null }),
Notes.count({ userHost: Not(IsNull()) }), Notes.count({ userHost: Not(IsNull()) }),
@ -27,6 +27,11 @@ export default class NotesChart extends Chart<typeof schema> {
}; };
} }
@autobind
protected async tickMinor(): Promise<Partial<KVs<typeof schema>>> {
return {};
}
@autobind @autobind
public async update(note: Note, isAdditional: boolean): Promise<void> { public async update(note: Note, isAdditional: boolean): Promise<void> {
const prefix = note.userHost === null ? 'local' : 'remote'; const prefix = note.userHost === null ? 'local' : 'remote';

View file

@ -14,7 +14,7 @@ export default class PerUserDriveChart extends Chart<typeof schema> {
} }
@autobind @autobind
protected async queryCurrentState(group: string): Promise<Partial<KVs<typeof schema>>> { protected async tickMajor(group: string): Promise<Partial<KVs<typeof schema>>> {
const [count, size] = await Promise.all([ const [count, size] = await Promise.all([
DriveFiles.count({ userId: group }), DriveFiles.count({ userId: group }),
DriveFiles.calcDriveUsageOf(group), DriveFiles.calcDriveUsageOf(group),
@ -26,6 +26,11 @@ export default class PerUserDriveChart extends Chart<typeof schema> {
}; };
} }
@autobind
protected async tickMinor(): Promise<Partial<KVs<typeof schema>>> {
return {};
}
@autobind @autobind
public async update(file: DriveFile, isAdditional: boolean): Promise<void> { public async update(file: DriveFile, isAdditional: boolean): Promise<void> {
const fileSizeKb = file.size / 1000; const fileSizeKb = file.size / 1000;

View file

@ -15,7 +15,7 @@ export default class PerUserFollowingChart extends Chart<typeof schema> {
} }
@autobind @autobind
protected async queryCurrentState(group: string): Promise<Partial<KVs<typeof schema>>> { protected async tickMajor(group: string): Promise<Partial<KVs<typeof schema>>> {
const [ const [
localFollowingsCount, localFollowingsCount,
localFollowersCount, localFollowersCount,
@ -36,6 +36,11 @@ export default class PerUserFollowingChart extends Chart<typeof schema> {
}; };
} }
@autobind
protected async tickMinor(): Promise<Partial<KVs<typeof schema>>> {
return {};
}
@autobind @autobind
public async update(follower: { id: User['id']; host: User['host']; }, followee: { id: User['id']; host: User['host']; }, isFollow: boolean): Promise<void> { public async update(follower: { id: User['id']; host: User['host']; }, followee: { id: User['id']; host: User['host']; }, isFollow: boolean): Promise<void> {
const prefixFollower = Users.isLocalUser(follower) ? 'local' : 'remote'; const prefixFollower = Users.isLocalUser(follower) ? 'local' : 'remote';

View file

@ -15,7 +15,7 @@ export default class PerUserNotesChart extends Chart<typeof schema> {
} }
@autobind @autobind
protected async queryCurrentState(group: string): Promise<Partial<KVs<typeof schema>>> { protected async tickMajor(group: string): Promise<Partial<KVs<typeof schema>>> {
const [count] = await Promise.all([ const [count] = await Promise.all([
Notes.count({ userId: group }), Notes.count({ userId: group }),
]); ]);
@ -25,6 +25,11 @@ export default class PerUserNotesChart extends Chart<typeof schema> {
}; };
} }
@autobind
protected async tickMinor(): Promise<Partial<KVs<typeof schema>>> {
return {};
}
@autobind @autobind
public async update(user: { id: User['id'] }, note: Note, isAdditional: boolean): Promise<void> { public async update(user: { id: User['id'] }, note: Note, isAdditional: boolean): Promise<void> {
await this.commit({ await this.commit({

View file

@ -15,7 +15,12 @@ export default class PerUserReactionsChart extends Chart<typeof schema> {
} }
@autobind @autobind
protected async queryCurrentState(group: string): Promise<Partial<KVs<typeof schema>>> { protected async tickMajor(group: string): Promise<Partial<KVs<typeof schema>>> {
return {};
}
@autobind
protected async tickMinor(): Promise<Partial<KVs<typeof schema>>> {
return {}; return {};
} }

View file

@ -14,12 +14,17 @@ export default class TestGroupedChart extends Chart<typeof schema> {
} }
@autobind @autobind
protected async queryCurrentState(group: string): Promise<Partial<KVs<typeof schema>>> { protected async tickMajor(group: string): Promise<Partial<KVs<typeof schema>>> {
return { return {
'foo.total': this.total[group], 'foo.total': this.total[group],
}; };
} }
@autobind
protected async tickMinor(): Promise<Partial<KVs<typeof schema>>> {
return {};
}
@autobind @autobind
public async increment(group: string): Promise<void> { public async increment(group: string): Promise<void> {
if (this.total[group] == null) this.total[group] = 0; if (this.total[group] == null) this.total[group] = 0;

View file

@ -12,7 +12,12 @@ export default class TestIntersectionChart extends Chart<typeof schema> {
} }
@autobind @autobind
protected async queryCurrentState(): Promise<Partial<KVs<typeof schema>>> { protected async tickMajor(): Promise<Partial<KVs<typeof schema>>> {
return {};
}
@autobind
protected async tickMinor(): Promise<Partial<KVs<typeof schema>>> {
return {}; return {};
} }

View file

@ -12,7 +12,12 @@ export default class TestUniqueChart extends Chart<typeof schema> {
} }
@autobind @autobind
protected async queryCurrentState(): Promise<Partial<KVs<typeof schema>>> { protected async tickMajor(): Promise<Partial<KVs<typeof schema>>> {
return {};
}
@autobind
protected async tickMinor(): Promise<Partial<KVs<typeof schema>>> {
return {}; return {};
} }

View file

@ -14,12 +14,17 @@ export default class TestChart extends Chart<typeof schema> {
} }
@autobind @autobind
protected async queryCurrentState(): Promise<Partial<KVs<typeof schema>>> { protected async tickMajor(): Promise<Partial<KVs<typeof schema>>> {
return { return {
'foo.total': this.total, 'foo.total': this.total,
}; };
} }
@autobind
protected async tickMinor(): Promise<Partial<KVs<typeof schema>>> {
return {};
}
@autobind @autobind
public async increment(): Promise<void> { public async increment(): Promise<void> {
this.total++; this.total++;

View file

@ -15,7 +15,7 @@ export default class UsersChart extends Chart<typeof schema> {
} }
@autobind @autobind
protected async queryCurrentState(): Promise<Partial<KVs<typeof schema>>> { protected async tickMajor(): Promise<Partial<KVs<typeof schema>>> {
const [localCount, remoteCount] = await Promise.all([ const [localCount, remoteCount] = await Promise.all([
Users.count({ host: null }), Users.count({ host: null }),
Users.count({ host: Not(IsNull()) }), Users.count({ host: Not(IsNull()) }),
@ -27,6 +27,11 @@ export default class UsersChart extends Chart<typeof schema> {
}; };
} }
@autobind
protected async tickMinor(): Promise<Partial<KVs<typeof schema>>> {
return {};
}
@autobind @autobind
public async update(user: { id: User['id'], host: User['host'] }, isAdditional: boolean): Promise<void> { public async update(user: { id: User['id'], host: User['host'] }, isAdditional: boolean): Promise<void> {
const prefix = Users.isLocalUser(user) ? 'local' : 'remote'; const prefix = Users.isLocalUser(user) ? 'local' : 'remote';

View file

@ -81,7 +81,15 @@ export default abstract class Chart<T extends Schema> {
protected repositoryForHour: Repository<RawRecord<T>>; protected repositoryForHour: Repository<RawRecord<T>>;
protected repositoryForDay: Repository<RawRecord<T>>; protected repositoryForDay: Repository<RawRecord<T>>;
protected abstract queryCurrentState(group: string | null): Promise<Partial<KVs<T>>>; /**
* 1(CASCADE削除などアプリケーション側で感知できない変動によるズレの修正用)
*/
protected abstract tickMajor(group: string | null): Promise<Partial<KVs<T>>>;
/**
* 1
*/
protected abstract tickMinor(group: string | null): Promise<Partial<KVs<T>>>;
@autobind @autobind
private static convertSchemaToColumnDefinitions(schema: Schema): Record<string, { type: string; array?: boolean; default?: any; }> { private static convertSchemaToColumnDefinitions(schema: Schema): Record<string, { type: string; array?: boolean; default?: any; }> {
@ -445,8 +453,8 @@ export default abstract class Chart<T extends Schema> {
} }
@autobind @autobind
public async resync(group: string | null = null): Promise<void> { public async tick(major: boolean, group: string | null = null): Promise<void> {
const data = await this.queryCurrentState(group); const data = major ? await this.tickMajor(group) : await this.tickMinor(group);
const columns = {} as Record<string, number>; const columns = {} as Record<string, number>;
for (const [k, v] of Object.entries(data)) { for (const [k, v] of Object.entries(data)) {
@ -480,6 +488,11 @@ export default abstract class Chart<T extends Schema> {
update(logHour, logDay)); update(logHour, logDay));
} }
@autobind
public resync(group: string | null = null): Promise<void> {
return this.tick(true, group);
}
@autobind @autobind
public async clean(): Promise<void> { public async clean(): Promise<void> {
const current = dateUTC(Chart.getCurrentDate()); const current = dateUTC(Chart.getCurrentDate());

View file

@ -1,6 +1,5 @@
import { Instance } from '@/models/entities/instance'; import { Instance } from '@/models/entities/instance';
import { Instances } from '@/models/index'; import { Instances } from '@/models/index';
import { federationChart } from '@/services/chart/index';
import { genId } from '@/misc/gen-id'; import { genId } from '@/misc/gen-id';
import { toPuny } from '@/misc/convert-host'; import { toPuny } from '@/misc/convert-host';
import { Cache } from '@/misc/cache'; import { Cache } from '@/misc/cache';
@ -23,8 +22,6 @@ export async function registerOrFetchInstanceDoc(host: string): Promise<Instance
lastCommunicatedAt: new Date(), lastCommunicatedAt: new Date(),
}).then(x => Instances.findOneOrFail(x.identifiers[0])); }).then(x => Instances.findOneOrFail(x.identifiers[0]));
federationChart.update(true);
cache.set(host, i); cache.set(host, i);
return i; return i;
} else { } else {

View file

@ -370,14 +370,14 @@ export default defineComponent({
const raw = await os.api('charts/federation', { limit: props.limit, span: props.span }); const raw = await os.api('charts/federation', { limit: props.limit, span: props.span });
return { return {
series: [{ series: [{
name: 'Total', name: 'Sub',
type: 'area', type: 'area',
data: format(raw.instance.total), data: format(raw.sub),
color: '#888888', color: colors.orange,
}, { }, {
name: 'Inc/Dec', name: 'Pub',
type: 'area', type: 'area',
data: format(sum(raw.instance.inc, negate(raw.instance.dec))), data: format(raw.pub),
color: colors.purple, color: colors.purple,
}, { }, {
name: 'Received', name: 'Received',