feat: suspend instance improvements (#13861)

* feat(backend): dead instance detection

* feat(backend): suspend type detection

* feat(frontend): show suspend reason on frontend

* feat(backend): resume federation automatically if the server is automatically suspended

* docs(changelog): 配信停止まわりの改善

* lint: fix lint errors

* Update packages/frontend/src/pages/instance-info.vue

* lint: fix lint error

* chore: suspendedState => suspensionState

---------

Co-authored-by: syuilo <4439005+syuilo@users.noreply.github.com>
This commit is contained in:
anatawa12
2024-05-23 15:55:47 +09:00
committed by GitHub
parent 611e303bab
commit 83a9aa4533
15 changed files with 193 additions and 17 deletions

View File

@@ -0,0 +1,16 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
export class NotRespondingSince1716345015347 {
name = 'NotRespondingSince1716345015347'
async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "instance" ADD "notRespondingSince" TIMESTAMP WITH TIME ZONE`);
}
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "instance" DROP COLUMN "notRespondingSince"`);
}
}

View File

@@ -0,0 +1,50 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
export class SuspensionStateInsteadOfIsSspended1716345771510 {
name = 'SuspensionStateInsteadOfIsSspended1716345771510'
async up(queryRunner) {
await queryRunner.query(`CREATE TYPE "public"."instance_suspensionstate_enum" AS ENUM('none', 'manuallySuspended', 'goneSuspended', 'autoSuspendedForNotResponding')`);
await queryRunner.query(`DROP INDEX "public"."IDX_34500da2e38ac393f7bb6b299c"`);
await queryRunner.query(`ALTER TABLE "instance" RENAME COLUMN "isSuspended" TO "suspensionState"`);
await queryRunner.query(`ALTER TABLE "instance" ALTER COLUMN "suspensionState" DROP DEFAULT`);
await queryRunner.query(`ALTER TABLE "instance" ALTER COLUMN "suspensionState" TYPE "public"."instance_suspensionstate_enum" USING (
CASE "suspensionState"
WHEN TRUE THEN 'manuallySuspended'::instance_suspensionstate_enum
ELSE 'none'::instance_suspensionstate_enum
END
)`);
await queryRunner.query(`ALTER TABLE "instance" ALTER COLUMN "suspensionState" SET DEFAULT 'none'`);
await queryRunner.query(`CREATE INDEX "IDX_3ede46f507c87ad698051d56a8" ON "instance" ("suspensionState") `);
}
async down(queryRunner) {
await queryRunner.query(`DROP INDEX "public"."IDX_3ede46f507c87ad698051d56a8"`);
await queryRunner.query(`ALTER TABLE "instance" ALTER COLUMN "suspensionState" DROP DEFAULT`);
await queryRunner.query(`ALTER TABLE "instance" ALTER COLUMN "suspensionState" TYPE boolean USING (
CASE "suspensionState"
WHEN 'none'::instance_suspensionstate_enum THEN FALSE
ELSE TRUE
END
)`);
await queryRunner.query(`ALTER TABLE "instance" ALTER COLUMN "suspensionState" SET DEFAULT false`);
await queryRunner.query(`ALTER TABLE "instance" RENAME COLUMN "suspensionState" TO "isSuspended"`);
await queryRunner.query(`CREATE INDEX "IDX_34500da2e38ac393f7bb6b299c" ON "instance" ("isSuspended") `);
await queryRunner.query(`DROP TYPE "public"."instance_suspensionstate_enum"`);
}
}

View File

@@ -39,7 +39,8 @@ export class InstanceEntityService {
followingCount: instance.followingCount,
followersCount: instance.followersCount,
isNotResponding: instance.isNotResponding,
isSuspended: instance.isSuspended,
isSuspended: instance.suspensionState !== 'none',
suspensionState: instance.suspensionState,
isBlocked: this.utilityService.isBlockedHost(meta.blockedHosts, instance.host),
softwareName: instance.softwareName,
softwareVersion: instance.softwareVersion,

View File

@@ -81,13 +81,22 @@ export class MiInstance {
public isNotResponding: boolean;
/**
* このインスタンスへの配信を停止するか
* このインスタンスと不通になった日時
*/
@Column('timestamp with time zone', {
nullable: true,
})
public notRespondingSince: Date | null;
/**
* このインスタンスへの配信状態
*/
@Index()
@Column('boolean', {
default: false,
@Column('enum', {
default: 'none',
enum: ['none', 'manuallySuspended', 'goneSuspended', 'autoSuspendedForNotResponding'],
})
public isSuspended: boolean;
public suspensionState: 'none' | 'manuallySuspended' | 'goneSuspended' | 'autoSuspendedForNotResponding';
@Column('varchar', {
length: 64, nullable: true,

View File

@@ -45,6 +45,11 @@ export const packedFederationInstanceSchema = {
type: 'boolean',
optional: false, nullable: false,
},
suspensionState: {
type: 'string',
nullable: false, optional: false,
enum: ['none', 'manuallySuspended', 'goneSuspended', 'autoSuspendedForNotResponding'],
},
isBlocked: {
type: 'boolean',
optional: false, nullable: false,

View File

@@ -5,6 +5,7 @@
import { Inject, Injectable } from '@nestjs/common';
import * as Bull from 'bullmq';
import { Not } from 'typeorm';
import { DI } from '@/di-symbols.js';
import type { InstancesRepository } from '@/models/_.js';
import type Logger from '@/logger.js';
@@ -62,7 +63,7 @@ export class DeliverProcessorService {
if (suspendedHosts == null) {
suspendedHosts = await this.instancesRepository.find({
where: {
isSuspended: true,
suspensionState: Not('none'),
},
});
this.suspendedHostsCache.set(suspendedHosts);
@@ -79,6 +80,7 @@ export class DeliverProcessorService {
if (i.isNotResponding) {
this.federatedInstanceService.update(i.id, {
isNotResponding: false,
notRespondingSince: null,
});
}
@@ -98,7 +100,15 @@ export class DeliverProcessorService {
if (!i.isNotResponding) {
this.federatedInstanceService.update(i.id, {
isNotResponding: true,
notRespondingSince: new Date(),
});
} else if (i.notRespondingSince) {
// 1週間以上不通ならサスペンド
if (i.suspensionState === 'none' && i.notRespondingSince.getTime() <= Date.now() - 1000 * 60 * 60 * 24 * 7) {
this.federatedInstanceService.update(i.id, {
suspensionState: 'autoSuspendedForNotResponding',
});
}
}
this.apRequestChart.deliverFail();
@@ -116,7 +126,7 @@ export class DeliverProcessorService {
if (job.data.isSharedInbox && res.statusCode === 410) {
this.federatedInstanceService.fetch(host).then(i => {
this.federatedInstanceService.update(i.id, {
isSuspended: true,
suspensionState: 'goneSuspended',
});
});
throw new Bull.UnrecoverableError(`${host} is gone`);

View File

@@ -188,6 +188,8 @@ export class InboxProcessorService {
this.federatedInstanceService.update(i.id, {
latestRequestReceivedAt: new Date(),
isNotResponding: false,
// もしサーバーが死んでるために配信が止まっていた場合には自動的に復活させてあげる
suspensionState: i.suspensionState === 'autoSuspendedForNotResponding' ? 'none' : undefined,
});
this.fetchInstanceMetadataService.fetchInstanceMetadata(i);

View File

@@ -137,7 +137,7 @@ export class ApiServerService {
const instances = await this.instancesRepository.find({
select: ['host'],
where: {
isSuspended: false,
suspensionState: 'none',
},
});

View File

@@ -46,12 +46,19 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
throw new Error('instance not found');
}
const isSuspendedBefore = instance.suspensionState !== 'none';
let suspensionState: undefined | 'manuallySuspended' | 'none';
if (ps.isSuspended != null && isSuspendedBefore !== ps.isSuspended) {
suspensionState = ps.isSuspended ? 'manuallySuspended' : 'none';
}
await this.federatedInstanceService.update(instance.id, {
isSuspended: ps.isSuspended,
suspensionState,
moderationNote: ps.moderationNote,
});
if (ps.isSuspended != null && instance.isSuspended !== ps.isSuspended) {
if (ps.isSuspended != null && isSuspendedBefore !== ps.isSuspended) {
if (ps.isSuspended) {
this.moderationLogService.log(me, 'suspendRemoteInstance', {
id: instance.id,