215 lines
5.3 KiB
TypeScript
215 lines
5.3 KiB
TypeScript
import { query, queryOne, execute } from './db'
|
|
|
|
// 상수 정의
|
|
const INTERVAL_SUCCESS = 5 * 60 * 1000 // 5분
|
|
const INTERVAL_FAILURE = 1 * 60 * 1000 // 1분
|
|
const REQUEST_TIMEOUT = 10 * 1000 // 10초
|
|
|
|
// 타입 정의
|
|
interface PubnetTarget {
|
|
id: number
|
|
name: string
|
|
url: string
|
|
is_active: number
|
|
}
|
|
|
|
interface PubnetStatus {
|
|
id: number
|
|
current_index: number
|
|
check_interval: number
|
|
is_healthy: number
|
|
last_target_id: number | null
|
|
last_checked_at: string | null
|
|
scheduler_running: number
|
|
}
|
|
|
|
interface CheckResult {
|
|
targetId: number
|
|
targetName: string
|
|
url: string
|
|
isSuccess: boolean
|
|
}
|
|
|
|
class PubnetScheduler {
|
|
private timer: ReturnType<typeof setTimeout> | null = null
|
|
private isRunning: boolean = false
|
|
|
|
/**
|
|
* 스케줄러 시작
|
|
*/
|
|
start(): void {
|
|
if (this.isRunning) {
|
|
console.log('[PubnetScheduler] Already running')
|
|
return
|
|
}
|
|
|
|
this.isRunning = true
|
|
this.updateSchedulerRunning(1)
|
|
console.log('[PubnetScheduler] Started')
|
|
|
|
// 즉시 첫 체크 실행
|
|
this.runCheck()
|
|
}
|
|
|
|
/**
|
|
* 스케줄러 중지
|
|
*/
|
|
stop(): void {
|
|
if (this.timer) {
|
|
clearTimeout(this.timer)
|
|
this.timer = null
|
|
}
|
|
this.isRunning = false
|
|
this.updateSchedulerRunning(0)
|
|
console.log('[PubnetScheduler] Stopped')
|
|
}
|
|
|
|
/**
|
|
* 실행 상태 확인
|
|
*/
|
|
getIsRunning(): boolean {
|
|
return this.isRunning
|
|
}
|
|
|
|
/**
|
|
* 체크 실행
|
|
*/
|
|
private async runCheck(): Promise<void> {
|
|
if (!this.isRunning) return
|
|
|
|
try {
|
|
const result = await this.checkCurrentTargets()
|
|
|
|
// 결과에 따라 다음 간격 결정
|
|
const nextInterval = result.isSuccess ? INTERVAL_SUCCESS : INTERVAL_FAILURE
|
|
|
|
// 로그 저장 (1개만)
|
|
await this.saveLog(result)
|
|
|
|
// 상태 업데이트 (인덱스 +1)
|
|
await this.updateStatus(result, nextInterval)
|
|
|
|
console.log(
|
|
`[PubnetScheduler] ${result.targetName} (${result.url}) - ` +
|
|
`${result.isSuccess ? 'SUCCESS' : 'FAILED'} - ` +
|
|
`Next check in ${nextInterval / 1000}s`
|
|
)
|
|
|
|
// 다음 체크 예약
|
|
this.timer = setTimeout(() => this.runCheck(), nextInterval)
|
|
|
|
} catch (error) {
|
|
console.error('[PubnetScheduler] Error:', error)
|
|
// 에러 발생 시 1분 후 재시도
|
|
this.timer = setTimeout(() => this.runCheck(), INTERVAL_FAILURE)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 현재 타겟 1개 체크
|
|
*/
|
|
private async checkCurrentTargets(): Promise<CheckResult> {
|
|
// 활성화된 타겟 목록 조회
|
|
const targets = await query<PubnetTarget>(`
|
|
SELECT * FROM pubnet_targets
|
|
WHERE is_active = 1
|
|
ORDER BY id ASC
|
|
`)
|
|
|
|
if (targets.length === 0) {
|
|
throw new Error('No active targets found')
|
|
}
|
|
|
|
// 현재 인덱스 조회
|
|
const status = await queryOne<PubnetStatus>('SELECT * FROM pubnet_status WHERE id = 1')
|
|
if (!status) {
|
|
throw new Error('Status not found')
|
|
}
|
|
|
|
// 1개 타겟 선택
|
|
const idx = status.current_index % targets.length
|
|
const target = targets[idx]
|
|
|
|
// 단일 타겟 체크
|
|
let isSuccess = false
|
|
try {
|
|
const controller = new AbortController()
|
|
const timeoutId = setTimeout(() => controller.abort(), REQUEST_TIMEOUT)
|
|
|
|
const response = await fetch(target.url, {
|
|
method: 'HEAD',
|
|
signal: controller.signal,
|
|
headers: {
|
|
'User-Agent': 'OSOLIT-Monitor/1.0'
|
|
}
|
|
})
|
|
|
|
clearTimeout(timeoutId)
|
|
isSuccess = response.status === 200
|
|
} catch (err: any) {
|
|
isSuccess = false
|
|
}
|
|
|
|
return {
|
|
targetId: target.id,
|
|
targetName: target.name,
|
|
url: target.url,
|
|
isSuccess
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 체크 결과 로그 저장
|
|
*/
|
|
private async saveLog(result: CheckResult): Promise<void> {
|
|
await execute(
|
|
`INSERT INTO pubnet_logs (target_id, is_success) VALUES ($1, $2)`,
|
|
[result.targetId, result.isSuccess ? 1 : 0]
|
|
)
|
|
}
|
|
|
|
/**
|
|
* 상태 업데이트 (인덱스 +1 순환)
|
|
*/
|
|
private async updateStatus(result: CheckResult, nextInterval: number): Promise<void> {
|
|
// 활성 타겟 수 조회
|
|
const countResult = await queryOne<{ cnt: string }>(
|
|
`SELECT COUNT(*) as cnt FROM pubnet_targets WHERE is_active = 1`
|
|
)
|
|
const totalCount = parseInt(countResult?.cnt || '0')
|
|
|
|
const status = await queryOne<{ current_index: number }>(
|
|
'SELECT current_index FROM pubnet_status WHERE id = 1'
|
|
)
|
|
|
|
// 인덱스 +1 (순환)
|
|
const nextIndex = ((status?.current_index || 0) + 1) % totalCount
|
|
|
|
await execute(`
|
|
UPDATE pubnet_status SET
|
|
current_index = $1,
|
|
check_interval = $2,
|
|
is_healthy = $3,
|
|
last_target_id = $4,
|
|
last_checked_at = to_char(NOW(), 'YYYY-MM-DD HH24:MI:SS'),
|
|
updated_at = to_char(NOW(), 'YYYY-MM-DD HH24:MI:SS')
|
|
WHERE id = 1
|
|
`, [nextIndex, nextInterval, result.isSuccess ? 1 : 0, result.targetId])
|
|
}
|
|
|
|
/**
|
|
* 스케줄러 실행 상태 업데이트
|
|
*/
|
|
private async updateSchedulerRunning(running: number): Promise<void> {
|
|
await execute(`
|
|
UPDATE pubnet_status SET
|
|
scheduler_running = $1,
|
|
updated_at = to_char(NOW(), 'YYYY-MM-DD HH24:MI:SS')
|
|
WHERE id = 1
|
|
`, [running])
|
|
}
|
|
}
|
|
|
|
// 싱글톤 인스턴스
|
|
export const pubnetScheduler = new PubnetScheduler()
|