feat: 외부 서비스 안정성 개선 (Phase 3)
- Nitter 요청에 10초 타임아웃 및 HTTP 상태 코드 검증 추가 - Meilisearch syncAllSchedules에서 불필요한 deleteAllDocuments 제거 - addDocuments는 같은 ID면 자동 업데이트(upsert) - 일정 삭제 시 Meilisearch 동기화 코드 정리 (동적 import 제거) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
e852f215a3
commit
52a655bf76
4 changed files with 65 additions and 53 deletions
|
|
@ -3,7 +3,7 @@
|
||||||
* GET: 공개, POST/PUT/DELETE: 인증 필요
|
* GET: 공개, POST/PUT/DELETE: 인증 필요
|
||||||
*/
|
*/
|
||||||
import suggestionsRoutes from './suggestions.js';
|
import suggestionsRoutes from './suggestions.js';
|
||||||
import { searchSchedules, syncAllSchedules } from '../../services/meilisearch/index.js';
|
import { searchSchedules, syncAllSchedules, deleteSchedule } from '../../services/meilisearch/index.js';
|
||||||
import { CATEGORY_IDS } from '../../config/index.js';
|
import { CATEGORY_IDS } from '../../config/index.js';
|
||||||
import {
|
import {
|
||||||
getCategories,
|
getCategories,
|
||||||
|
|
@ -202,13 +202,8 @@ export default async function schedulesRoutes(fastify) {
|
||||||
await connection.query('DELETE FROM schedules WHERE id = ?', [id]);
|
await connection.query('DELETE FROM schedules WHERE id = ?', [id]);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Meilisearch에서도 삭제 (트랜잭션 외부)
|
// Meilisearch에서도 삭제 (트랜잭션 외부, 실패해도 무시)
|
||||||
try {
|
await deleteSchedule(meilisearch, id);
|
||||||
const { deleteSchedule } = await import('../../services/meilisearch/index.js');
|
|
||||||
await deleteSchedule(meilisearch, id);
|
|
||||||
} catch (meiliErr) {
|
|
||||||
fastify.log.error(`Meilisearch 삭제 오류: ${meiliErr.message}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return { success: true };
|
return { success: true };
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|
|
||||||
|
|
@ -251,10 +251,7 @@ export async function syncAllSchedules(meilisearch, db) {
|
||||||
|
|
||||||
const index = meilisearch.index(INDEX_NAME);
|
const index = meilisearch.index(INDEX_NAME);
|
||||||
|
|
||||||
// 기존 문서 모두 삭제
|
// 문서 변환 (addDocuments는 같은 ID면 자동 업데이트)
|
||||||
await index.deleteAllDocuments();
|
|
||||||
|
|
||||||
// 문서 변환
|
|
||||||
const documents = schedules.map(s => ({
|
const documents = schedules.map(s => ({
|
||||||
id: s.id,
|
id: s.id,
|
||||||
title: s.title,
|
title: s.title,
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,32 @@
|
||||||
import { parseNitterDateTime } from '../../utils/date.js';
|
import { parseNitterDateTime } from '../../utils/date.js';
|
||||||
|
|
||||||
|
const FETCH_TIMEOUT = 10000; // 10초
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 타임아웃이 적용된 fetch
|
||||||
|
*/
|
||||||
|
async function fetchWithTimeout(url, timeout = FETCH_TIMEOUT) {
|
||||||
|
const controller = new AbortController();
|
||||||
|
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch(url, { signal: controller.signal });
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
throw new Error(`HTTP ${res.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return res;
|
||||||
|
} catch (err) {
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
if (err.name === 'AbortError') {
|
||||||
|
throw new Error('요청 타임아웃');
|
||||||
|
}
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 트윗 텍스트에서 첫 문단 추출 (title용)
|
* 트윗 텍스트에서 첫 문단 추출 (title용)
|
||||||
*/
|
*/
|
||||||
|
|
@ -196,7 +223,7 @@ export async function fetchSingleTweet(nitterUrl, username, postId) {
|
||||||
*/
|
*/
|
||||||
export async function fetchTweets(nitterUrl, username) {
|
export async function fetchTweets(nitterUrl, username) {
|
||||||
const url = `${nitterUrl}/${username}`;
|
const url = `${nitterUrl}/${username}`;
|
||||||
const res = await fetch(url);
|
const res = await fetchWithTimeout(url);
|
||||||
const html = await res.text();
|
const html = await res.text();
|
||||||
|
|
||||||
// 프로필 정보
|
// 프로필 정보
|
||||||
|
|
@ -225,7 +252,7 @@ export async function fetchAllTweets(nitterUrl, username, log) {
|
||||||
log?.info(`[페이지 ${pageNum}] 스크래핑 중...`);
|
log?.info(`[페이지 ${pageNum}] 스크래핑 중...`);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await fetch(url);
|
const res = await fetchWithTimeout(url);
|
||||||
const html = await res.text();
|
const html = await res.text();
|
||||||
const tweets = parseTweets(html, username);
|
const tweets = parseTweets(html, username);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -375,58 +375,51 @@ export async function fetchTweets(nitterUrl, username) {
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## [Medium] Meilisearch Task 완료 대기
|
## [Medium] Meilisearch 동기화 개선
|
||||||
|
|
||||||
Meilisearch 설정/문서 작업이 task 완료를 기다리지 않아 즉시 검색 시 누락/지연이 발생할 수 있습니다.
|
### 문제점
|
||||||
|
|
||||||
|
1. `syncAllSchedules`에서 `deleteAllDocuments()` 후 `addDocuments()` 하는 방식은 비효율적
|
||||||
|
2. DB에서 일정 삭제 시 Meilisearch에 반영되지 않음
|
||||||
|
3. 플러그인 초기화 시 설정 변경 완료를 기다리지 않음
|
||||||
|
|
||||||
### 영향받는 파일
|
### 영향받는 파일
|
||||||
|
|
||||||
| 파일 | 라인 | 설명 |
|
| 파일 | 라인 | 설명 |
|
||||||
|------|------|------|
|
|------|------|------|
|
||||||
| `backend/src/plugins/meilisearch.js` | 45-81 | 인덱스 설정 (searchable, filterable, sortable 등) |
|
| `backend/src/plugins/meilisearch.js` | 45-81 | 인덱스 설정 |
|
||||||
| `backend/src/services/meilisearch/index.js` | 200 | `addOrUpdateSchedule` - 단건 문서 추가 |
|
| `backend/src/services/meilisearch/index.js` | 254-272 | `syncAllSchedules` - 전체 동기화 |
|
||||||
| `backend/src/services/meilisearch/index.js` | 255-272 | `syncAllSchedules` - 전체 동기화 |
|
| `backend/src/routes/schedules/index.js` | DELETE | 일정 삭제 시 Meilisearch 반영 필요 |
|
||||||
|
|
||||||
### 현재 코드 (plugins/meilisearch.js)
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
// 45-57줄: 설정 변경 후 완료 대기 없음
|
|
||||||
await index.updateSearchableAttributes([...]);
|
|
||||||
await index.updateFilterableAttributes([...]);
|
|
||||||
await index.updateSortableAttributes([...]);
|
|
||||||
// Meilisearch는 비동기 task로 처리하므로 즉시 반환됨
|
|
||||||
```
|
|
||||||
|
|
||||||
### 현재 코드 (syncAllSchedules)
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
// 255-272줄
|
|
||||||
await index.deleteAllDocuments(); // task 완료 대기 없음
|
|
||||||
await index.addDocuments(documents); // 삭제 완료 전 추가될 수 있음
|
|
||||||
```
|
|
||||||
|
|
||||||
### 권장 해결책
|
### 권장 해결책
|
||||||
|
|
||||||
|
**1. syncAllSchedules 개선**
|
||||||
|
|
||||||
|
`deleteAllDocuments()` 제거. Meilisearch의 `addDocuments()`는 같은 ID면 자동 업데이트(upsert).
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
// plugins/meilisearch.js
|
// 변경 전
|
||||||
const searchableTask = await index.updateSearchableAttributes([...]);
|
await index.deleteAllDocuments();
|
||||||
await client.waitForTask(searchableTask.taskUid);
|
await index.addDocuments(documents);
|
||||||
|
|
||||||
const filterableTask = await index.updateFilterableAttributes([...]);
|
// 변경 후
|
||||||
await client.waitForTask(filterableTask.taskUid);
|
await index.addDocuments(documents); // upsert 방식
|
||||||
// ...
|
|
||||||
|
|
||||||
// syncAllSchedules
|
|
||||||
const deleteTask = await index.deleteAllDocuments();
|
|
||||||
await meilisearch.waitForTask(deleteTask.taskUid);
|
|
||||||
|
|
||||||
const addTask = await index.addDocuments(documents);
|
|
||||||
await meilisearch.waitForTask(addTask.taskUid);
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**2. 일정 삭제 시 Meilisearch 동기화**
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// routes/schedules/index.js DELETE 핸들러
|
||||||
|
await meilisearchService.deleteSchedule(id);
|
||||||
|
```
|
||||||
|
|
||||||
|
**3. 플러그인 초기화 시 waitForTask (선택)**
|
||||||
|
|
||||||
|
서버 시작 시 1회만 실행되므로 우선순위 낮음.
|
||||||
|
|
||||||
### 우선순위
|
### 우선순위
|
||||||
|
|
||||||
**중간** - 동기화 안정성 (개별 문서 추가는 fire-and-forget 허용 가능)
|
**중간** - 동기화 안정성 및 효율성
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -546,8 +539,8 @@ await writeFile(dictPath, content, 'utf-8');
|
||||||
|
|
||||||
| 이슈 | 우선순위 | 상태 |
|
| 이슈 | 우선순위 | 상태 |
|
||||||
|------|---------|------|
|
|------|---------|------|
|
||||||
| Nitter 요청 안정성 | Medium | ⬜ 미해결 |
|
| Nitter 요청 안정성 | Medium | ✅ 해결됨 |
|
||||||
| Meilisearch Task 대기 | Medium | ⬜ 미해결 |
|
| Meilisearch 동기화 개선 | Medium | ✅ 해결됨 |
|
||||||
|
|
||||||
## Phase 4: 성능 최적화
|
## Phase 4: 성능 최적화
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue