feat: 管理画面のジョブキューページにresume/pauseボタンを用意 (#17436)

* feat: 管理画面のジョブキューページにresume/pauseボタンを用意

* fix review
This commit is contained in:
おさむのひと 2026-05-22 16:20:53 +09:00 committed by GitHub
commit 9f2e806c20
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 303 additions and 3 deletions

View file

@ -1,7 +1,7 @@
## Unreleased
### General
-
- Feat: ジョブキュー管理画面からキューの一時停止/再開ができるように
### Client
-

View file

@ -769,6 +769,18 @@ export class QueueService {
await queue.promoteJobs();
}
@bindThis
public async queuePause(queueType: typeof QUEUE_TYPES[number]) {
const queue = this.getQueue(queueType);
await queue.pause();
}
@bindThis
public async queueResume(queueType: typeof QUEUE_TYPES[number]) {
const queue = this.getQueue(queueType);
await queue.resume();
}
@bindThis
public async queueRetryJob(queueType: typeof QUEUE_TYPES[number], jobId: string) {
const queue = this.getQueue(queueType);

View file

@ -72,6 +72,8 @@ export * as 'admin/queue/remove-job' from './endpoints/admin/queue/remove-job.js
export * as 'admin/queue/show-job' from './endpoints/admin/queue/show-job.js';
export * as 'admin/queue/show-job-logs' from './endpoints/admin/queue/show-job-logs.js';
export * as 'admin/queue/promote-jobs' from './endpoints/admin/queue/promote-jobs.js';
export * as 'admin/queue/pause' from './endpoints/admin/queue/pause.js';
export * as 'admin/queue/resume' from './endpoints/admin/queue/resume.js';
export * as 'admin/queue/jobs' from './endpoints/admin/queue/jobs.js';
export * as 'admin/queue/stats' from './endpoints/admin/queue/stats.js';
export * as 'admin/queue/queues' from './endpoints/admin/queue/queues.js';

View file

@ -0,0 +1,39 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { ModerationLogService } from '@/core/ModerationLogService.js';
import { QUEUE_TYPES, QueueService } from '@/core/QueueService.js';
export const meta = {
tags: ['admin'],
requireCredential: true,
requireModerator: true,
kind: 'write:admin:queue',
} as const;
export const paramDef = {
type: 'object',
properties: {
queue: { type: 'string', enum: QUEUE_TYPES },
},
required: ['queue'],
} as const;
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
private moderationLogService: ModerationLogService,
private queueService: QueueService,
) {
super(meta, paramDef, async (ps, me) => {
await this.queueService.queuePause(ps.queue);
this.moderationLogService.log(me, 'pauseQueue');
});
}
}

View file

@ -0,0 +1,39 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { ModerationLogService } from '@/core/ModerationLogService.js';
import { QUEUE_TYPES, QueueService } from '@/core/QueueService.js';
export const meta = {
tags: ['admin'],
requireCredential: true,
requireModerator: true,
kind: 'write:admin:queue',
} as const;
export const paramDef = {
type: 'object',
properties: {
queue: { type: 'string', enum: QUEUE_TYPES },
},
required: ['queue'],
} as const;
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
private moderationLogService: ModerationLogService,
private queueService: QueueService,
) {
super(meta, paramDef, async (ps, me) => {
await this.queueService.queueResume(ps.queue);
this.moderationLogService.log(me, 'resumeQueue');
});
}
}

View file

@ -94,6 +94,8 @@ export const moderationLogTypes = [
'deleteRole',
'clearQueue',
'promoteQueue',
'pauseQueue',
'resumeQueue',
'deleteDriveFile',
'deleteNote',
'createGlobalAnnouncement',
@ -199,6 +201,8 @@ export type ModerationLogPayloads = {
};
clearQueue: Record<string, never>;
promoteQueue: Record<string, never>;
pauseQueue: Record<string, never>;
resumeQueue: Record<string, never>;
deleteDriveFile: {
fileId: string;
fileUserId: string | null;

View file

@ -38,8 +38,8 @@ SPDX-License-Identifier: AGPL-3.0-only
<div class="_buttons">
<MkButton rounded @click="promoteAllJobs"><i class="ti ti-player-track-next"></i> Promote all jobs</MkButton>
<!-- <MkButton rounded @click="createJob"><i class="ti ti-plus"></i> Add job</MkButton> -->
<!-- <MkButton v-if="queueInfo.isPaused" rounded @click="resumeQueue"><i class="ti ti-player-play"></i> Resume queue</MkButton> -->
<!-- <MkButton v-else rounded danger @click="pauseQueue"><i class="ti ti-player-pause"></i> Pause queue</MkButton> -->
<MkButton v-if="queueInfo.isPaused" rounded @click="resumeQueue"><i class="ti ti-player-play"></i> Resume queue</MkButton>
<MkButton v-else rounded danger @click="pauseQueue"><i class="ti ti-player-pause"></i> Pause queue</MkButton>
<MkButton rounded danger @click="clearQueue"><i class="ti ti-trash"></i> Empty queue</MkButton>
</div>
</template>
@ -292,6 +292,30 @@ async function promoteAllJobs() {
fetchJobs();
}
async function pauseQueue() {
if (tab.value === '-') return;
const { canceled } = await os.confirm({
type: 'warning',
title: i18n.ts.areYouSure,
});
if (canceled) return;
await os.apiWithDialog('admin/queue/pause', { queue: tab.value });
fetchCurrentQueue();
fetchJobs();
}
async function resumeQueue() {
if (tab.value === '-') return;
await os.apiWithDialog('admin/queue/resume', { queue: tab.value });
fetchCurrentQueue();
fetchJobs();
}
async function removeJobs() {
if (tab.value === '-' || jobState.value === 'latest') return;

View file

@ -280,6 +280,9 @@ type AdminQueueJobsRequest = operations['admin___queue___jobs']['requestBody']['
// @public (undocumented)
type AdminQueueJobsResponse = operations['admin___queue___jobs']['responses']['200']['content']['application/json'];
// @public (undocumented)
type AdminQueuePauseRequest = operations['admin___queue___pause']['requestBody']['content']['application/json'];
// @public (undocumented)
type AdminQueuePromoteJobsRequest = operations['admin___queue___promote-jobs']['requestBody']['content']['application/json'];
@ -295,6 +298,9 @@ type AdminQueueQueueStatsResponse = operations['admin___queue___queue-stats']['r
// @public (undocumented)
type AdminQueueRemoveJobRequest = operations['admin___queue___remove-job']['requestBody']['content']['application/json'];
// @public (undocumented)
type AdminQueueResumeRequest = operations['admin___queue___resume']['requestBody']['content']['application/json'];
// @public (undocumented)
type AdminQueueRetryJobRequest = operations['admin___queue___retry-job']['requestBody']['content']['application/json'];
@ -1604,11 +1610,13 @@ declare namespace entities {
AdminQueueInboxDelayedResponse,
AdminQueueJobsRequest,
AdminQueueJobsResponse,
AdminQueuePauseRequest,
AdminQueuePromoteJobsRequest,
AdminQueueQueueStatsRequest,
AdminQueueQueueStatsResponse,
AdminQueueQueuesResponse,
AdminQueueRemoveJobRequest,
AdminQueueResumeRequest,
AdminQueueRetryJobRequest,
AdminQueueShowJobRequest,
AdminQueueShowJobResponse,

View file

@ -647,6 +647,17 @@ declare module '../api.js' {
credential?: string | null,
): Promise<SwitchCaseResponseType<E, P>>;
/**
* No description provided.
*
* **Credential required**: *Yes* / **Permission**: *write:admin:queue*
*/
request<E extends 'admin/queue/pause', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
): Promise<SwitchCaseResponseType<E, P>>;
/**
* No description provided.
*
@ -691,6 +702,17 @@ declare module '../api.js' {
credential?: string | null,
): Promise<SwitchCaseResponseType<E, P>>;
/**
* No description provided.
*
* **Credential required**: *Yes* / **Permission**: *write:admin:queue*
*/
request<E extends 'admin/queue/resume', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
): Promise<SwitchCaseResponseType<E, P>>;
/**
* No description provided.
*

View file

@ -80,11 +80,13 @@ import type {
AdminQueueInboxDelayedResponse,
AdminQueueJobsRequest,
AdminQueueJobsResponse,
AdminQueuePauseRequest,
AdminQueuePromoteJobsRequest,
AdminQueueQueueStatsRequest,
AdminQueueQueueStatsResponse,
AdminQueueQueuesResponse,
AdminQueueRemoveJobRequest,
AdminQueueResumeRequest,
AdminQueueRetryJobRequest,
AdminQueueShowJobRequest,
AdminQueueShowJobResponse,
@ -722,10 +724,12 @@ export type Endpoints = {
'admin/queue/deliver-delayed': { req: EmptyRequest; res: AdminQueueDeliverDelayedResponse };
'admin/queue/inbox-delayed': { req: EmptyRequest; res: AdminQueueInboxDelayedResponse };
'admin/queue/jobs': { req: AdminQueueJobsRequest; res: AdminQueueJobsResponse };
'admin/queue/pause': { req: AdminQueuePauseRequest; res: EmptyResponse };
'admin/queue/promote-jobs': { req: AdminQueuePromoteJobsRequest; res: EmptyResponse };
'admin/queue/queue-stats': { req: AdminQueueQueueStatsRequest; res: AdminQueueQueueStatsResponse };
'admin/queue/queues': { req: EmptyRequest; res: AdminQueueQueuesResponse };
'admin/queue/remove-job': { req: AdminQueueRemoveJobRequest; res: EmptyResponse };
'admin/queue/resume': { req: AdminQueueResumeRequest; res: EmptyResponse };
'admin/queue/retry-job': { req: AdminQueueRetryJobRequest; res: EmptyResponse };
'admin/queue/show-job': { req: AdminQueueShowJobRequest; res: AdminQueueShowJobResponse };
'admin/queue/show-job-logs': { req: AdminQueueShowJobLogsRequest; res: AdminQueueShowJobLogsResponse };

View file

@ -83,11 +83,13 @@ export type AdminQueueDeliverDelayedResponse = operations['admin___queue___deliv
export type AdminQueueInboxDelayedResponse = operations['admin___queue___inbox-delayed']['responses']['200']['content']['application/json'];
export type AdminQueueJobsRequest = operations['admin___queue___jobs']['requestBody']['content']['application/json'];
export type AdminQueueJobsResponse = operations['admin___queue___jobs']['responses']['200']['content']['application/json'];
export type AdminQueuePauseRequest = operations['admin___queue___pause']['requestBody']['content']['application/json'];
export type AdminQueuePromoteJobsRequest = operations['admin___queue___promote-jobs']['requestBody']['content']['application/json'];
export type AdminQueueQueueStatsRequest = operations['admin___queue___queue-stats']['requestBody']['content']['application/json'];
export type AdminQueueQueueStatsResponse = operations['admin___queue___queue-stats']['responses']['200']['content']['application/json'];
export type AdminQueueQueuesResponse = operations['admin___queue___queues']['responses']['200']['content']['application/json'];
export type AdminQueueRemoveJobRequest = operations['admin___queue___remove-job']['requestBody']['content']['application/json'];
export type AdminQueueResumeRequest = operations['admin___queue___resume']['requestBody']['content']['application/json'];
export type AdminQueueRetryJobRequest = operations['admin___queue___retry-job']['requestBody']['content']['application/json'];
export type AdminQueueShowJobRequest = operations['admin___queue___show-job']['requestBody']['content']['application/json'];
export type AdminQueueShowJobResponse = operations['admin___queue___show-job']['responses']['200']['content']['application/json'];

View file

@ -530,6 +530,15 @@ export type paths = {
*/
post: operations['admin___queue___jobs'];
};
'/admin/queue/pause': {
/**
* admin/queue/pause
* @description No description provided.
*
* **Credential required**: *Yes* / **Permission**: *write:admin:queue*
*/
post: operations['admin___queue___pause'];
};
'/admin/queue/promote-jobs': {
/**
* admin/queue/promote-jobs
@ -566,6 +575,15 @@ export type paths = {
*/
post: operations['admin___queue___remove-job'];
};
'/admin/queue/resume': {
/**
* admin/queue/resume
* @description No description provided.
*
* **Credential required**: *Yes* / **Permission**: *write:admin:queue*
*/
post: operations['admin___queue___resume'];
};
'/admin/queue/retry-job': {
/**
* admin/queue/retry-job
@ -9903,6 +9921,69 @@ export interface operations {
};
};
};
admin___queue___pause: {
requestBody: {
content: {
'application/json': {
/** @enum {string} */
queue: 'system' | 'endedPollNotification' | 'postScheduledNote' | 'deliver' | 'inbox' | 'db' | 'relationship' | 'objectStorage' | 'userWebhookDeliver' | 'systemWebhookDeliver';
};
};
};
responses: {
/** @description OK (without any results) */
204: {
headers: {
[name: string]: unknown;
};
};
/** @description Client error */
400: {
headers: {
[name: string]: unknown;
};
content: {
'application/json': components['schemas']['Error'];
};
};
/** @description Authentication error */
401: {
headers: {
[name: string]: unknown;
};
content: {
'application/json': components['schemas']['Error'];
};
};
/** @description Forbidden error */
403: {
headers: {
[name: string]: unknown;
};
content: {
'application/json': components['schemas']['Error'];
};
};
/** @description I'm Ai */
418: {
headers: {
[name: string]: unknown;
};
content: {
'application/json': components['schemas']['Error'];
};
};
/** @description Internal server error */
500: {
headers: {
[name: string]: unknown;
};
content: {
'application/json': components['schemas']['Error'];
};
};
};
};
'admin___queue___promote-jobs': {
requestBody: {
content: {
@ -10197,6 +10278,69 @@ export interface operations {
};
};
};
admin___queue___resume: {
requestBody: {
content: {
'application/json': {
/** @enum {string} */
queue: 'system' | 'endedPollNotification' | 'postScheduledNote' | 'deliver' | 'inbox' | 'db' | 'relationship' | 'objectStorage' | 'userWebhookDeliver' | 'systemWebhookDeliver';
};
};
};
responses: {
/** @description OK (without any results) */
204: {
headers: {
[name: string]: unknown;
};
};
/** @description Client error */
400: {
headers: {
[name: string]: unknown;
};
content: {
'application/json': components['schemas']['Error'];
};
};
/** @description Authentication error */
401: {
headers: {
[name: string]: unknown;
};
content: {
'application/json': components['schemas']['Error'];
};
};
/** @description Forbidden error */
403: {
headers: {
[name: string]: unknown;
};
content: {
'application/json': components['schemas']['Error'];
};
};
/** @description I'm Ai */
418: {
headers: {
[name: string]: unknown;
};
content: {
'application/json': components['schemas']['Error'];
};
};
/** @description Internal server error */
500: {
headers: {
[name: string]: unknown;
};
content: {
'application/json': components['schemas']['Error'];
};
};
};
};
'admin___queue___retry-job': {
requestBody: {
content: {