feat: Meilisearch 버전 체크 기반 자동 동기화
- 4시~4시 5분간 1분 간격으로 Meilisearch 버전 체크 - watchtower 업데이트로 버전 변경 감지 시 즉시 동기화 - 동기화 오류 시 인덱스 삭제 후 재생성하여 재시도 - 기존 고정 시간(4:05) cron 방식에서 버전 감지 방식으로 변경 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
52a655bf76
commit
8091f4ac67
3 changed files with 185 additions and 3 deletions
|
|
@ -3,7 +3,7 @@ export default [
|
||||||
id: 'meilisearch-sync',
|
id: 'meilisearch-sync',
|
||||||
type: 'meilisearch',
|
type: 'meilisearch',
|
||||||
name: 'Meilisearch 동기화',
|
name: 'Meilisearch 동기화',
|
||||||
cron: '5 4 * * *',
|
cron: '0 4 * * *', // 4시부터 5분간 버전 체크, 변경 시 동기화
|
||||||
enabled: true,
|
enabled: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import fp from 'fastify-plugin';
|
import fp from 'fastify-plugin';
|
||||||
import cron from 'node-cron';
|
import cron from 'node-cron';
|
||||||
import bots from '../config/bots.js';
|
import bots from '../config/bots.js';
|
||||||
import { syncAllSchedules } from '../services/meilisearch/index.js';
|
import { syncWithRetry, getVersion } from '../services/meilisearch/index.js';
|
||||||
|
|
||||||
const REDIS_PREFIX = 'bot:status:';
|
const REDIS_PREFIX = 'bot:status:';
|
||||||
|
|
||||||
|
|
@ -45,13 +45,116 @@ async function schedulerPlugin(fastify, opts) {
|
||||||
return fastify.xBot.syncNewTweets;
|
return fastify.xBot.syncNewTweets;
|
||||||
} else if (bot.type === 'meilisearch') {
|
} else if (bot.type === 'meilisearch') {
|
||||||
return async () => {
|
return async () => {
|
||||||
const count = await syncAllSchedules(fastify.meilisearch, fastify.db);
|
const count = await syncWithRetry(fastify.meilisearch, fastify.db);
|
||||||
return { addedCount: count, total: count };
|
return { addedCount: count, total: count };
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Meilisearch 버전 체크 및 동기화 (업데이트 감지용)
|
||||||
|
*/
|
||||||
|
async function startMeilisearchVersionCheck(botId, bot) {
|
||||||
|
const REDIS_VERSION_KEY = 'meilisearch:version';
|
||||||
|
const CHECK_INTERVAL = 60 * 1000; // 1분
|
||||||
|
const CHECK_DURATION = 5 * 60 * 1000; // 5분간 체크
|
||||||
|
|
||||||
|
// 체크 시작 cron (매일 4시)
|
||||||
|
const task = cron.schedule(bot.cron, async () => {
|
||||||
|
fastify.log.info(`[${botId}] 버전 체크 시작 (5분간 1분 간격)`);
|
||||||
|
await updateStatus(botId, { status: 'running' });
|
||||||
|
|
||||||
|
const startTime = Date.now();
|
||||||
|
let synced = false;
|
||||||
|
let checkCount = 0;
|
||||||
|
|
||||||
|
// 초기 버전 저장
|
||||||
|
const initialVersion = await getVersion(fastify.meilisearch);
|
||||||
|
if (!initialVersion) {
|
||||||
|
fastify.log.error(`[${botId}] Meilisearch 연결 실패`);
|
||||||
|
await updateStatus(botId, { status: 'error', errorMessage: 'Meilisearch 연결 실패' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const savedVersion = await fastify.redis.get(REDIS_VERSION_KEY);
|
||||||
|
fastify.log.info(`[${botId}] 현재 버전: ${initialVersion}, 저장된 버전: ${savedVersion || '없음'}`);
|
||||||
|
|
||||||
|
// 버전이 이미 다르면 즉시 동기화
|
||||||
|
if (savedVersion && savedVersion !== initialVersion) {
|
||||||
|
fastify.log.info(`[${botId}] 버전 변경 감지! ${savedVersion} → ${initialVersion}`);
|
||||||
|
await performSync(botId, initialVersion, REDIS_VERSION_KEY);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5분간 1분 간격으로 체크
|
||||||
|
const intervalId = setInterval(async () => {
|
||||||
|
checkCount++;
|
||||||
|
const elapsed = Date.now() - startTime;
|
||||||
|
|
||||||
|
if (synced || elapsed >= CHECK_DURATION) {
|
||||||
|
clearInterval(intervalId);
|
||||||
|
if (!synced) {
|
||||||
|
fastify.log.info(`[${botId}] 버전 변경 없음, 체크 종료`);
|
||||||
|
await updateStatus(botId, {
|
||||||
|
status: 'running',
|
||||||
|
lastCheckAt: new Date().toISOString(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentVersion = await getVersion(fastify.meilisearch);
|
||||||
|
fastify.log.info(`[${botId}] 체크 #${checkCount}: 버전 ${currentVersion}`);
|
||||||
|
|
||||||
|
if (currentVersion && currentVersion !== initialVersion) {
|
||||||
|
synced = true;
|
||||||
|
clearInterval(intervalId);
|
||||||
|
fastify.log.info(`[${botId}] 버전 변경 감지! ${initialVersion} → ${currentVersion}`);
|
||||||
|
await performSync(botId, currentVersion, REDIS_VERSION_KEY);
|
||||||
|
}
|
||||||
|
}, CHECK_INTERVAL);
|
||||||
|
});
|
||||||
|
|
||||||
|
tasks.set(botId, task);
|
||||||
|
await updateStatus(botId, { status: 'running' });
|
||||||
|
fastify.log.info(`[${botId}] 버전 체크 스케줄 시작 (cron: ${bot.cron})`);
|
||||||
|
|
||||||
|
// 초기 버전 저장 (최초 실행 시)
|
||||||
|
const currentVersion = await getVersion(fastify.meilisearch);
|
||||||
|
if (currentVersion) {
|
||||||
|
const savedVersion = await fastify.redis.get(REDIS_VERSION_KEY);
|
||||||
|
if (!savedVersion) {
|
||||||
|
await fastify.redis.set(REDIS_VERSION_KEY, currentVersion);
|
||||||
|
fastify.log.info(`[${botId}] 초기 버전 저장: ${currentVersion}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 동기화 실행 및 상태 업데이트
|
||||||
|
*/
|
||||||
|
async function performSync(botId, newVersion, versionKey) {
|
||||||
|
try {
|
||||||
|
const count = await syncWithRetry(fastify.meilisearch, fastify.db);
|
||||||
|
await fastify.redis.set(versionKey, newVersion);
|
||||||
|
await updateStatus(botId, {
|
||||||
|
status: 'running',
|
||||||
|
lastCheckAt: new Date().toISOString(),
|
||||||
|
lastAddedCount: count,
|
||||||
|
errorMessage: null,
|
||||||
|
});
|
||||||
|
fastify.log.info(`[${botId}] 동기화 완료: ${count}개, 새 버전: ${newVersion}`);
|
||||||
|
} catch (err) {
|
||||||
|
await updateStatus(botId, {
|
||||||
|
status: 'error',
|
||||||
|
lastCheckAt: new Date().toISOString(),
|
||||||
|
errorMessage: err.message,
|
||||||
|
});
|
||||||
|
fastify.log.error(`[${botId}] 동기화 오류: ${err.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 동기화 결과 처리 (중복 코드 제거)
|
* 동기화 결과 처리 (중복 코드 제거)
|
||||||
*/
|
*/
|
||||||
|
|
@ -88,6 +191,12 @@ async function schedulerPlugin(fastify, opts) {
|
||||||
tasks.delete(botId);
|
tasks.delete(botId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Meilisearch는 버전 체크 방식 사용
|
||||||
|
if (bot.type === 'meilisearch') {
|
||||||
|
await startMeilisearchVersionCheck(botId, bot);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const syncFn = getSyncFunction(bot);
|
const syncFn = getSyncFunction(bot);
|
||||||
if (!syncFn) {
|
if (!syncFn) {
|
||||||
throw new Error(`지원하지 않는 봇 타입: ${bot.type}`);
|
throw new Error(`지원하지 않는 봇 타입: ${bot.type}`);
|
||||||
|
|
|
||||||
|
|
@ -275,3 +275,76 @@ export async function syncAllSchedules(meilisearch, db) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Meilisearch 버전 조회
|
||||||
|
*/
|
||||||
|
export async function getVersion(meilisearch) {
|
||||||
|
try {
|
||||||
|
const version = await meilisearch.getVersion();
|
||||||
|
return version.pkgVersion;
|
||||||
|
} catch (err) {
|
||||||
|
logger.error(`버전 조회 오류: ${err.message}`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 인덱스 삭제 후 재생성
|
||||||
|
*/
|
||||||
|
async function recreateIndex(meilisearch) {
|
||||||
|
const index = meilisearch.index(INDEX_NAME);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 인덱스 삭제
|
||||||
|
const deleteTask = await meilisearch.deleteIndex(INDEX_NAME);
|
||||||
|
await meilisearch.waitForTask(deleteTask.taskUid);
|
||||||
|
logger.info('기존 인덱스 삭제 완료');
|
||||||
|
} catch (err) {
|
||||||
|
// 인덱스가 없으면 무시
|
||||||
|
}
|
||||||
|
|
||||||
|
// 인덱스 재생성
|
||||||
|
const createTask = await meilisearch.createIndex(INDEX_NAME, { primaryKey: 'id' });
|
||||||
|
await meilisearch.waitForTask(createTask.taskUid);
|
||||||
|
|
||||||
|
// 설정 복원
|
||||||
|
await index.updateSearchableAttributes([
|
||||||
|
'title', 'member_names', 'description', 'source_name', 'category_name',
|
||||||
|
]);
|
||||||
|
await index.updateFilterableAttributes(['category_id', 'date']);
|
||||||
|
await index.updateSortableAttributes(['date', 'time']);
|
||||||
|
await index.updateRankingRules([
|
||||||
|
'words', 'typo', 'proximity', 'attribute', 'exactness', 'date:desc',
|
||||||
|
]);
|
||||||
|
await index.updateTypoTolerance({
|
||||||
|
enabled: true,
|
||||||
|
minWordSizeForTypos: { oneTypo: 2, twoTypos: 4 },
|
||||||
|
});
|
||||||
|
await index.updatePagination({ maxTotalHits: 10000 });
|
||||||
|
|
||||||
|
logger.info('인덱스 재생성 완료');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 동기화 (오류 시 인덱스 재생성 후 재시도)
|
||||||
|
*/
|
||||||
|
export async function syncWithRetry(meilisearch, db) {
|
||||||
|
try {
|
||||||
|
const count = await syncAllSchedules(meilisearch, db);
|
||||||
|
if (count > 0) return count;
|
||||||
|
|
||||||
|
// 0개면 오류일 수 있으므로 재시도
|
||||||
|
throw new Error('동기화 결과 0개');
|
||||||
|
} catch (err) {
|
||||||
|
logger.warn(`동기화 실패, 인덱스 재생성 후 재시도: ${err.message}`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await recreateIndex(meilisearch);
|
||||||
|
return await syncAllSchedules(meilisearch, db);
|
||||||
|
} catch (retryErr) {
|
||||||
|
logger.error(`재시도 실패: ${retryErr.message}`);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue