Skip to content

Commit

Permalink
feat: suspend instance improvements (misskey-dev#13861)
Browse files Browse the repository at this point in the history
* 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 <[email protected]>
  • Loading branch information
anatawa12 and syuilo authored May 23, 2024
1 parent 611e303 commit 83a9aa4
Show file tree
Hide file tree
Showing 15 changed files with 193 additions and 17 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@
- サスペンド済みユーザーか
- 鍵アカウントユーザーか
- 「アカウントを見つけやすくする」が有効なユーザーか
- Enhance: Goneを出さずに終了したサーバーへの配信停止を自動的に行うように
- もしそのようなサーバーからから配信が届いた場合には自動的に配信を再開します
- Enhance: 配信停止の理由を表示するように
- Fix: Play作成時に設定した公開範囲が機能していない問題を修正
- Fix: 正規化されていない状態のhashtagが連合されてきたhtmlに含まれているとhashtagが正しくhashtagに復元されない問題を修正
- Fix: みつけるのアンケート欄にてチャンネルのアンケートが含まれてしまう問題を修正
Expand Down
32 changes: 32 additions & 0 deletions locales/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4972,6 +4972,38 @@ export interface Locale extends ILocale {
* お問い合わせ
*/
"inquiry": string;
"_delivery": {
/**
* 配信状態
*/
"status": string;
/**
* 配信停止
*/
"stop": string;
/**
* 配信再開
*/
"resume": string;
"_type": {
/**
* 配信中
*/
"none": string;
/**
* 手動停止中
*/
"manuallySuspended": string;
/**
* サーバー削除のため停止中
*/
"goneSuspended": string;
/**
* サーバー応答なしのため停止中
*/
"autoSuspendedForNotResponding": string;
};
};
"_bubbleGame": {
/**
* 遊び方
Expand Down
10 changes: 10 additions & 0 deletions locales/ja-JP.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1240,6 +1240,16 @@ noDescription: "説明文はありません"
alwaysConfirmFollow: "フォローの際常に確認する"
inquiry: "お問い合わせ"

_delivery:
status: "配信状態"
stop: "配信停止"
resume: "配信再開"
_type:
none: "配信中"
manuallySuspended: "手動停止中"
goneSuspended: "サーバー削除のため停止中"
autoSuspendedForNotResponding: "サーバー応答なしのため停止中"

_bubbleGame:
howToPlay: "遊び方"
hold: "ホールド"
Expand Down
16 changes: 16 additions & 0 deletions packages/backend/migration/1716345015347-NotRespondingSince.js
Original file line number Diff line number Diff line change
@@ -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"`);
}
}
Original file line number Diff line number Diff line change
@@ -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"`);
}
}
3 changes: 2 additions & 1 deletion packages/backend/src/core/entities/InstanceEntityService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
17 changes: 13 additions & 4 deletions packages/backend/src/models/Instance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
14 changes: 12 additions & 2 deletions packages/backend/src/queue/processors/DeliverProcessorService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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);
Expand All @@ -79,6 +80,7 @@ export class DeliverProcessorService {
if (i.isNotResponding) {
this.federatedInstanceService.update(i.id, {
isNotResponding: false,
notRespondingSince: null,
});
}

Expand All @@ -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();
Expand All @@ -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`);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
2 changes: 1 addition & 1 deletion packages/backend/src/server/api/ApiServerService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ export class ApiServerService {
const instances = await this.instancesRepository.find({
select: ['host'],
where: {
isSuspended: false,
suspensionState: 'none',
},
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
14 changes: 12 additions & 2 deletions packages/frontend/src/pages/admin/federation.vue
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>

<script lang="ts" setup>
import * as Misskey from 'misskey-js';
import { computed, ref } from 'vue';
import XHeader from './_header_.vue';
import MkInput from '@/components/MkInput.vue';
Expand Down Expand Up @@ -90,8 +91,17 @@ const pagination = {
})),
};

function getStatus(instance) {
if (instance.isSuspended) return 'Suspended';
function getStatus(instance: Misskey.entities.FederationInstance) {
switch (instance.suspensionState) {
case 'manuallySuspended':
return 'Manually Suspended';
case 'goneSuspended':
return 'Automatically Suspended (Gone)';
case 'autoSuspendedForNotResponding':
return 'Automatically Suspended (Not Responding)';
case 'none':
break;
}
if (instance.isBlocked) return 'Blocked';
if (instance.isSilenced) return 'Silenced';
if (instance.isNotResponding) return 'Error';
Expand Down
29 changes: 24 additions & 5 deletions packages/frontend/src/pages/instance-info.vue
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,16 @@ SPDX-License-Identifier: AGPL-3.0-only
<FormSection v-if="iAmModerator">
<template #label>Moderation</template>
<div class="_gaps_s">
<MkSwitch v-model="suspended" :disabled="!instance" @update:modelValue="toggleSuspend">{{ i18n.ts.stopActivityDelivery }}</MkSwitch>
<MkKeyValue>
<template #key>
{{ i18n.ts._delivery.status }}
</template>
<template #value>
{{ i18n.ts._delivery._type[suspensionState] }}
</template>
</MkKeyValue>
<MkButton v-if="suspensionState === 'none'" :disabled="!instance" danger @click="stopDelivery">{{ i18n.ts._delivery.stop }}</MkButton>
<MkButton v-if="suspensionState !== 'none'" :disabled="!instance" @click="resumeDelivery">{{ i18n.ts._delivery.resume }}</MkButton>
<MkSwitch v-model="isBlocked" :disabled="!meta || !instance" @update:modelValue="toggleBlock">{{ i18n.ts.blockThisInstance }}</MkSwitch>
<MkSwitch v-model="isSilenced" :disabled="!meta || !instance" @update:modelValue="toggleSilenced">{{ i18n.ts.silenceThisInstance }}</MkSwitch>
<MkButton @click="refreshMetadata"><i class="ti ti-refresh"></i> Refresh metadata</MkButton>
Expand Down Expand Up @@ -155,7 +164,7 @@ const tab = ref('overview');
const chartSrc = ref('instance-requests');
const meta = ref<Misskey.entities.AdminMetaResponse | null>(null);
const instance = ref<Misskey.entities.FederationInstance | null>(null);
const suspended = ref(false);
const suspensionState = ref<'none' | 'manuallySuspended' | 'goneSuspended' | 'autoSuspendedForNotResponding'>('none');
const isBlocked = ref(false);
const isSilenced = ref(false);
const faviconUrl = ref<string | null>(null);
Expand Down Expand Up @@ -183,7 +192,7 @@ async function fetch(): Promise<void> {
instance.value = await misskeyApi('federation/show-instance', {
host: props.host,
});
suspended.value = instance.value?.isSuspended ?? false;
suspensionState.value = instance.value?.suspensionState ?? 'none';
isBlocked.value = instance.value?.isBlocked ?? false;
isSilenced.value = instance.value?.isSilenced ?? false;
faviconUrl.value = getProxiedImageUrlNullable(instance.value?.faviconUrl, 'preview') ?? getProxiedImageUrlNullable(instance.value?.iconUrl, 'preview');
Expand All @@ -209,11 +218,21 @@ async function toggleSilenced(): Promise<void> {
});
}

async function toggleSuspend(): Promise<void> {
async function stopDelivery(): Promise<void> {
if (!instance.value) throw new Error('No instance?');
suspensionState.value = 'manuallySuspended';
await misskeyApi('admin/federation/update-instance', {
host: instance.value.host,
isSuspended: suspended.value,
isSuspended: true,
});
}

async function resumeDelivery(): Promise<void> {
if (!instance.value) throw new Error('No instance?');
suspensionState.value = 'none';
await misskeyApi('admin/federation/update-instance', {
host: instance.value.host,
isSuspended: false,
});
}

Expand Down
2 changes: 2 additions & 0 deletions packages/misskey-js/src/autogen/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4475,6 +4475,8 @@ export type components = {
followersCount: number;
isNotResponding: boolean;
isSuspended: boolean;
/** @enum {string} */
suspensionState: 'none' | 'manuallySuspended' | 'goneSuspended' | 'autoSuspendedForNotResponding';
isBlocked: boolean;
/** @example misskey */
softwareName: string | null;
Expand Down

0 comments on commit 83a9aa4

Please sign in to comment.