diff options
| author | Kagami Sascha Rosylight <saschanaz@outlook.com> | 2023-03-03 03:13:12 +0100 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2023-03-03 11:13:12 +0900 |
| commit | 61215e50ff9e4c84787c8d99c75fd36dafbd8815 (patch) | |
| tree | 36419e8a3ec97afa0a3a0011d523d80addf8e724 /packages/backend/src | |
| parent | fix(server): チャンネルでミュートが正しく機能していない... (diff) | |
| download | sharkey-61215e50ff9e4c84787c8d99c75fd36dafbd8815.tar.gz sharkey-61215e50ff9e4c84787c8d99c75fd36dafbd8815.tar.bz2 sharkey-61215e50ff9e4c84787c8d99c75fd36dafbd8815.zip | |
test(backend): APIテストの復活 (#10163)
* Revert 1c5291f8185651c231903129ee7c1cee263f9f03
* APIテストの復活
* apiテストの移行
* moduleNameMapper修正
* simpleGetでthrowしないように
status確認しているので要らない
* longer timeout
* ローカルでは問題ないのになんで
* case sensitive
* Run Nest instance within the current process
* Skip some setIntervals
* wait for 5 seconds
* kill them all!!
* logHeapUsage: true
* detectOpenHandlesがじゃましているらしい
* maxWorkers=1?
* restore drive api tests
* workerIdleMemoryLimit: 500MB
* 1024MiB
* Wait what
Diffstat (limited to 'packages/backend/src')
| -rw-r--r-- | packages/backend/src/GlobalModule.ts | 9 | ||||
| -rw-r--r-- | packages/backend/src/boot/common.ts | 4 | ||||
| -rw-r--r-- | packages/backend/src/core/CreateNotificationService.ts | 37 | ||||
| -rw-r--r-- | packages/backend/src/core/NoteCreateService.ts | 16 | ||||
| -rw-r--r-- | packages/backend/src/core/NoteReadService.ts | 47 | ||||
| -rw-r--r-- | packages/backend/src/core/chart/ChartManagementService.ts | 8 | ||||
| -rw-r--r-- | packages/backend/src/core/chart/charts/per-user-notes.ts | 4 | ||||
| -rw-r--r-- | packages/backend/src/server/ServerService.ts | 16 | ||||
| -rw-r--r-- | packages/backend/src/server/api/ApiCallService.ts | 5 | ||||
| -rw-r--r-- | packages/backend/src/server/api/ApiServerService.ts | 12 | ||||
| -rw-r--r-- | packages/backend/src/server/api/endpoints.ts | 4 |
11 files changed, 107 insertions, 55 deletions
diff --git a/packages/backend/src/GlobalModule.ts b/packages/backend/src/GlobalModule.ts index 35416209a0..801f1db741 100644 --- a/packages/backend/src/GlobalModule.ts +++ b/packages/backend/src/GlobalModule.ts @@ -1,3 +1,4 @@ +import { setTimeout } from 'node:timers/promises'; import { Global, Inject, Module } from '@nestjs/common'; import Redis from 'ioredis'; import { DataSource } from 'typeorm'; @@ -57,6 +58,14 @@ export class GlobalModule implements OnApplicationShutdown { ) {} async onApplicationShutdown(signal: string): Promise<void> { + if (process.env.NODE_ENV === 'test') { + // XXX: + // Shutting down the existing connections causes errors on Jest as + // Misskey has asynchronous postgres/redis connections that are not + // awaited. + // Let's wait for some random time for them to finish. + await setTimeout(5000); + } await Promise.all([ this.db.destroy(), this.redisClient.disconnect(), diff --git a/packages/backend/src/boot/common.ts b/packages/backend/src/boot/common.ts index 04aa26e652..279a1fe59d 100644 --- a/packages/backend/src/boot/common.ts +++ b/packages/backend/src/boot/common.ts @@ -16,12 +16,14 @@ export async function server() { app.enableShutdownHooks(); const serverService = app.get(ServerService); - serverService.launch(); + await serverService.launch(); app.get(ChartManagementService).start(); app.get(JanitorService).start(); app.get(QueueStatsService).start(); app.get(ServerStatsService).start(); + + return app; } export async function jobQueue() { diff --git a/packages/backend/src/core/CreateNotificationService.ts b/packages/backend/src/core/CreateNotificationService.ts index cd47844a75..eba7171fb6 100644 --- a/packages/backend/src/core/CreateNotificationService.ts +++ b/packages/backend/src/core/CreateNotificationService.ts @@ -1,4 +1,5 @@ -import { Inject, Injectable } from '@nestjs/common'; +import { setTimeout } from 'node:timers/promises'; +import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common'; import type { MutingsRepository, NotificationsRepository, UserProfilesRepository, UsersRepository } from '@/models/index.js'; import type { User } from '@/models/entities/User.js'; import type { Notification } from '@/models/entities/Notification.js'; @@ -10,7 +11,9 @@ import { PushNotificationService } from '@/core/PushNotificationService.js'; import { bindThis } from '@/decorators.js'; @Injectable() -export class CreateNotificationService { +export class CreateNotificationService implements OnApplicationShutdown { + #shutdownController = new AbortController(); + constructor( @Inject(DI.usersRepository) private usersRepository: UsersRepository, @@ -40,11 +43,11 @@ export class CreateNotificationService { if (data.notifierId && (notifieeId === data.notifierId)) { return null; } - + const profile = await this.userProfilesRepository.findOneBy({ userId: notifieeId }); - + const isMuted = profile?.mutingNotificationTypes.includes(type); - + // Create notification const notification = await this.notificationsRepository.insert({ id: this.idService.genId(), @@ -56,18 +59,18 @@ export class CreateNotificationService { ...data, } as Partial<Notification>) .then(x => this.notificationsRepository.findOneByOrFail(x.identifiers[0])); - + const packed = await this.notificationEntityService.pack(notification, {}); - + // Publish notification event this.globalEventService.publishMainStream(notifieeId, 'notification', packed); - + // 2秒経っても(今回作成した)通知が既読にならなかったら「未読の通知がありますよ」イベントを発行する - setTimeout(async () => { + setTimeout(2000, 'unread note', { signal: this.#shutdownController.signal }).then(async () => { const fresh = await this.notificationsRepository.findOneBy({ id: notification.id }); if (fresh == null) return; // 既に削除されているかもしれない if (fresh.isRead) return; - + //#region ただしミュートしているユーザーからの通知なら無視 const mutings = await this.mutingsRepository.findBy({ muterId: notifieeId, @@ -76,14 +79,14 @@ export class CreateNotificationService { return; } //#endregion - + this.globalEventService.publishMainStream(notifieeId, 'unreadNotification', packed); this.pushNotificationService.pushNotification(notifieeId, 'notification', packed); - + if (type === 'follow') this.emailNotificationFollow(notifieeId, await this.usersRepository.findOneByOrFail({ id: data.notifierId! })); if (type === 'receiveFollowRequest') this.emailNotificationReceiveFollowRequest(notifieeId, await this.usersRepository.findOneByOrFail({ id: data.notifierId! })); - }, 2000); - + }, () => { /* aborted, ignore it */ }); + return notification; } @@ -103,7 +106,7 @@ export class CreateNotificationService { sendEmail(userProfile.email, i18n.t('_email._follow.title'), `${follower.name} (@${Acct.toString(follower)})`, `${follower.name} (@${Acct.toString(follower)})`); */ } - + @bindThis private async emailNotificationReceiveFollowRequest(userId: User['id'], follower: User) { /* @@ -115,4 +118,8 @@ export class CreateNotificationService { sendEmail(userProfile.email, i18n.t('_email._receiveFollowRequest.title'), `${follower.name} (@${Acct.toString(follower)})`, `${follower.name} (@${Acct.toString(follower)})`); */ } + + onApplicationShutdown(signal?: string | undefined): void { + this.#shutdownController.abort(); + } } diff --git a/packages/backend/src/core/NoteCreateService.ts b/packages/backend/src/core/NoteCreateService.ts index 54c135a7c5..4c4261ba79 100644 --- a/packages/backend/src/core/NoteCreateService.ts +++ b/packages/backend/src/core/NoteCreateService.ts @@ -1,6 +1,7 @@ +import { setImmediate } from 'node:timers/promises'; import * as mfm from 'mfm-js'; import { In, DataSource } from 'typeorm'; -import { Inject, Injectable } from '@nestjs/common'; +import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common'; import { extractMentions } from '@/misc/extract-mentions.js'; import { extractCustomEmojisFromMfm } from '@/misc/extract-custom-emojis-from-mfm.js'; import { extractHashtags } from '@/misc/extract-hashtags.js'; @@ -137,7 +138,9 @@ type Option = { }; @Injectable() -export class NoteCreateService { +export class NoteCreateService implements OnApplicationShutdown { + #shutdownController = new AbortController(); + constructor( @Inject(DI.config) private config: Config, @@ -313,7 +316,10 @@ export class NoteCreateService { const note = await this.insertNote(user, data, tags, emojis, mentionedUsers); - setImmediate(() => this.postNoteCreated(note, user, data, silent, tags!, mentionedUsers!)); + setImmediate('post created', { signal: this.#shutdownController.signal }).then( + () => this.postNoteCreated(note, user, data, silent, tags!, mentionedUsers!), + () => { /* aborted, ignore this */ }, + ); return note; } @@ -756,4 +762,8 @@ export class NoteCreateService { return mentionedUsers; } + + onApplicationShutdown(signal?: string | undefined) { + this.#shutdownController.abort(); + } } diff --git a/packages/backend/src/core/NoteReadService.ts b/packages/backend/src/core/NoteReadService.ts index 84983d600e..d23fb8238b 100644 --- a/packages/backend/src/core/NoteReadService.ts +++ b/packages/backend/src/core/NoteReadService.ts @@ -1,4 +1,5 @@ -import { Inject, Injectable } from '@nestjs/common'; +import { setTimeout } from 'node:timers/promises'; +import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common'; import { In, IsNull, Not } from 'typeorm'; import { DI } from '@/di-symbols.js'; import type { User } from '@/models/entities/User.js'; @@ -15,7 +16,9 @@ import { AntennaService } from './AntennaService.js'; import { PushNotificationService } from './PushNotificationService.js'; @Injectable() -export class NoteReadService { +export class NoteReadService implements OnApplicationShutdown { + #shutdownController = new AbortController(); + constructor( @Inject(DI.usersRepository) private usersRepository: UsersRepository, @@ -60,14 +63,14 @@ export class NoteReadService { }); if (mute.map(m => m.muteeId).includes(note.userId)) return; //#endregion - + // スレッドミュート const threadMute = await this.noteThreadMutingsRepository.findOneBy({ userId: userId, threadId: note.threadId ?? note.id, }); if (threadMute) return; - + const unread = { id: this.idService.genId(), noteId: note.id, @@ -77,15 +80,15 @@ export class NoteReadService { noteChannelId: note.channelId, noteUserId: note.userId, }; - + await this.noteUnreadsRepository.insert(unread); - + // 2秒経っても既読にならなかったら「未読の投稿がありますよ」イベントを発行する - setTimeout(async () => { + setTimeout(2000, 'unread note', { signal: this.#shutdownController.signal }).then(async () => { const exist = await this.noteUnreadsRepository.findOneBy({ id: unread.id }); - + if (exist == null) return; - + if (params.isMentioned) { this.globalEventService.publishMainStream(userId, 'unreadMention', note.id); } @@ -95,8 +98,8 @@ export class NoteReadService { if (note.channelId) { this.globalEventService.publishMainStream(userId, 'unreadChannel', note.id); } - }, 2000); - } + }, () => { /* aborted, ignore it */ }); + } @bindThis public async read( @@ -113,24 +116,24 @@ export class NoteReadService { }, select: ['followeeId'], })).map(x => x.followeeId)); - + const myAntennas = (await this.antennaService.getAntennas()).filter(a => a.userId === userId); const readMentions: (Note | Packed<'Note'>)[] = []; const readSpecifiedNotes: (Note | Packed<'Note'>)[] = []; const readChannelNotes: (Note | Packed<'Note'>)[] = []; const readAntennaNotes: (Note | Packed<'Note'>)[] = []; - + for (const note of notes) { if (note.mentions && note.mentions.includes(userId)) { readMentions.push(note); } else if (note.visibleUserIds && note.visibleUserIds.includes(userId)) { readSpecifiedNotes.push(note); } - + if (note.channelId && followingChannels.has(note.channelId)) { readChannelNotes.push(note); } - + if (note.user != null) { // たぶんnullになることは無いはずだけど一応 for (const antenna of myAntennas) { if (await this.antennaService.checkHitAntenna(antenna, note, note.user)) { @@ -139,14 +142,14 @@ export class NoteReadService { } } } - + if ((readMentions.length > 0) || (readSpecifiedNotes.length > 0) || (readChannelNotes.length > 0)) { // Remove the record await this.noteUnreadsRepository.delete({ userId: userId, noteId: In([...readMentions.map(n => n.id), ...readSpecifiedNotes.map(n => n.id), ...readChannelNotes.map(n => n.id)]), }); - + // TODO: ↓まとめてクエリしたい this.noteUnreadsRepository.countBy({ @@ -183,7 +186,7 @@ export class NoteReadService { noteId: In([...readMentions.map(n => n.id), ...readSpecifiedNotes.map(n => n.id)]), }); } - + if (readAntennaNotes.length > 0) { await this.antennaNotesRepository.update({ antennaId: In(myAntennas.map(a => a.id)), @@ -191,14 +194,14 @@ export class NoteReadService { }, { read: true, }); - + // TODO: まとめてクエリしたい for (const antenna of myAntennas) { const count = await this.antennaNotesRepository.countBy({ antennaId: antenna.id, read: false, }); - + if (count === 0) { this.globalEventService.publishMainStream(userId, 'readAntenna', antenna); this.pushNotificationService.pushNotification(userId, 'readAntenna', { antennaId: antenna.id }); @@ -213,4 +216,8 @@ export class NoteReadService { }); } } + + onApplicationShutdown(signal?: string | undefined): void { + this.#shutdownController.abort(); + } } diff --git a/packages/backend/src/core/chart/ChartManagementService.ts b/packages/backend/src/core/chart/ChartManagementService.ts index dbde757676..03e3612658 100644 --- a/packages/backend/src/core/chart/ChartManagementService.ts +++ b/packages/backend/src/core/chart/ChartManagementService.ts @@ -62,8 +62,10 @@ export class ChartManagementService implements OnApplicationShutdown { async onApplicationShutdown(signal: string): Promise<void> { clearInterval(this.saveIntervalId); - await Promise.all( - this.charts.map(chart => chart.save()), - ); + if (process.env.NODE_ENV !== 'test') { + await Promise.all( + this.charts.map(chart => chart.save()), + ); + } } } diff --git a/packages/backend/src/core/chart/charts/per-user-notes.ts b/packages/backend/src/core/chart/charts/per-user-notes.ts index 1e2a579dfa..d8966f34c1 100644 --- a/packages/backend/src/core/chart/charts/per-user-notes.ts +++ b/packages/backend/src/core/chart/charts/per-user-notes.ts @@ -45,8 +45,8 @@ export default class PerUserNotesChart extends Chart<typeof schema> { } @bindThis - public async update(user: { id: User['id'] }, note: Note, isAdditional: boolean): Promise<void> { - await this.commit({ + public update(user: { id: User['id'] }, note: Note, isAdditional: boolean): void { + this.commit({ 'total': isAdditional ? 1 : -1, 'inc': isAdditional ? 1 : 0, 'dec': isAdditional ? 0 : 1, diff --git a/packages/backend/src/server/ServerService.ts b/packages/backend/src/server/ServerService.ts index 417f50f95d..e61383468c 100644 --- a/packages/backend/src/server/ServerService.ts +++ b/packages/backend/src/server/ServerService.ts @@ -1,7 +1,7 @@ import cluster from 'node:cluster'; import * as fs from 'node:fs'; -import { Inject, Injectable } from '@nestjs/common'; -import Fastify from 'fastify'; +import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common'; +import Fastify, { FastifyInstance } from 'fastify'; import { IsNull } from 'typeorm'; import { GlobalEventService } from '@/core/GlobalEventService.js'; import type { Config } from '@/config.js'; @@ -23,8 +23,9 @@ import { FileServerService } from './FileServerService.js'; import { ClientServerService } from './web/ClientServerService.js'; @Injectable() -export class ServerService { +export class ServerService implements OnApplicationShutdown { private logger: Logger; + #fastify: FastifyInstance; constructor( @Inject(DI.config) @@ -54,11 +55,12 @@ export class ServerService { } @bindThis - public launch() { + public async launch() { const fastify = Fastify({ trustProxy: true, logger: !['production', 'test'].includes(process.env.NODE_ENV ?? ''), }); + this.#fastify = fastify; // HSTS // 6months (15552000sec) @@ -203,5 +205,11 @@ export class ServerService { }); fastify.listen({ port: this.config.port, host: '0.0.0.0' }); + + await fastify.ready(); + } + + async onApplicationShutdown(signal: string): Promise<void> { + await this.#fastify.close(); } } diff --git a/packages/backend/src/server/api/ApiCallService.ts b/packages/backend/src/server/api/ApiCallService.ts index 6d8540dd4f..f84a3aa59b 100644 --- a/packages/backend/src/server/api/ApiCallService.ts +++ b/packages/backend/src/server/api/ApiCallService.ts @@ -100,9 +100,12 @@ export class ApiCallService implements OnApplicationShutdown { request: FastifyRequest<{ Body: Record<string, unknown>, Querystring: Record<string, unknown> }>, reply: FastifyReply, ) { - const multipartData = await request.file(); + const multipartData = await request.file().catch(() => { + /* Fastify throws if the remote didn't send multipart data. Return 400 below. */ + }); if (multipartData == null) { reply.code(400); + reply.send(); return; } diff --git a/packages/backend/src/server/api/ApiServerService.ts b/packages/backend/src/server/api/ApiServerService.ts index 501ce63877..115d60986c 100644 --- a/packages/backend/src/server/api/ApiServerService.ts +++ b/packages/backend/src/server/api/ApiServerService.ts @@ -73,28 +73,32 @@ export class ApiServerService { Params: { endpoint: string; }, Body: Record<string, unknown>, Querystring: Record<string, unknown>, - }>('/' + endpoint.name, (request, reply) => { + }>('/' + endpoint.name, async (request, reply) => { if (request.method === 'GET' && !endpoint.meta.allowGet) { reply.code(405); reply.send(); return; } - this.apiCallService.handleMultipartRequest(ep, request, reply); + // Await so that any error can automatically be translated to HTTP 500 + await this.apiCallService.handleMultipartRequest(ep, request, reply); + return reply; }); } else { fastify.all<{ Params: { endpoint: string; }, Body: Record<string, unknown>, Querystring: Record<string, unknown>, - }>('/' + endpoint.name, { bodyLimit: 1024 * 32 }, (request, reply) => { + }>('/' + endpoint.name, { bodyLimit: 1024 * 32 }, async (request, reply) => { if (request.method === 'GET' && !endpoint.meta.allowGet) { reply.code(405); reply.send(); return; } - this.apiCallService.handleRequest(ep, request, reply); + // Await so that any error can automatically be translated to HTTP 500 + await this.apiCallService.handleRequest(ep, request, reply); + return reply; }); } } diff --git a/packages/backend/src/server/api/endpoints.ts b/packages/backend/src/server/api/endpoints.ts index 4d5ed9fb62..4f521148e0 100644 --- a/packages/backend/src/server/api/endpoints.ts +++ b/packages/backend/src/server/api/endpoints.ts @@ -741,8 +741,8 @@ export interface IEndpoint { const endpoints: IEndpoint[] = (eps as [string, any]).map(([name, ep]) => { return { name: name, - meta: ep.meta ?? {}, - params: ep.paramDef, + get meta() { return ep.meta ?? {}; }, + get params() { return ep.paramDef; }, }; }); |