- Updated lib/maintenance/data-cleanup.ts retention period: 28 days → 365 days - Storage requirements validated: 251 MB/year (negligible) - Rationale: 13× more historical data for better pattern analysis - Benefits: 260-390 blocked signals/year vs 20-30/month - Cleanup cutoff: Now Dec 2, 2024 (vs Nov 4, 2025 previously) - Deployment verified: Container restarted, cleanup scheduled for 3 AM daily
188 lines
5.1 KiB
TypeScript
188 lines
5.1 KiB
TypeScript
/**
|
||
* Data Cleanup Service (Dec 2, 2025)
|
||
*
|
||
* Purpose: Automatic cleanup of old market data to prevent database bloat
|
||
* Retention: 1 year of 1-minute data (extended from 4 weeks - minimal storage ~251 MB/year)
|
||
*
|
||
* Schedule: Runs daily at 3 AM
|
||
* Impact: ~1,576,800 rows per year (1 row/min × 60 min × 24 hr × 365 days × 3 symbols)
|
||
*/
|
||
|
||
import { getPrismaClient } from '../database/trades'
|
||
|
||
export class DataCleanupService {
|
||
private prisma = getPrismaClient()
|
||
private cleanupInterval: NodeJS.Timeout | null = null
|
||
private isRunning = false
|
||
|
||
/**
|
||
* Start the automatic cleanup job
|
||
* Runs daily at 3 AM
|
||
*/
|
||
public start(): void {
|
||
if (this.isRunning) {
|
||
console.log('⚠️ Data cleanup service already running')
|
||
return
|
||
}
|
||
|
||
console.log('🧹 Starting data cleanup service...')
|
||
this.isRunning = true
|
||
|
||
// Run immediately on start
|
||
this.runCleanup().catch(error => {
|
||
console.error('❌ Error in initial cleanup:', error)
|
||
})
|
||
|
||
// Calculate time until next 3 AM
|
||
const now = new Date()
|
||
const next3AM = new Date()
|
||
next3AM.setHours(3, 0, 0, 0)
|
||
|
||
if (now.getHours() >= 3) {
|
||
// If it's already past 3 AM today, schedule for tomorrow
|
||
next3AM.setDate(next3AM.getDate() + 1)
|
||
}
|
||
|
||
const msUntil3AM = next3AM.getTime() - now.getTime()
|
||
|
||
// Schedule first run at 3 AM
|
||
setTimeout(() => {
|
||
this.runCleanup().catch(error => {
|
||
console.error('❌ Error in scheduled cleanup:', error)
|
||
})
|
||
|
||
// Then run every 24 hours
|
||
this.cleanupInterval = setInterval(() => {
|
||
this.runCleanup().catch(error => {
|
||
console.error('❌ Error in cleanup:', error)
|
||
})
|
||
}, 24 * 60 * 60 * 1000) // 24 hours
|
||
}, msUntil3AM)
|
||
|
||
const hoursUntil3AM = Math.round(msUntil3AM / (60 * 60 * 1000))
|
||
console.log(`✅ Data cleanup scheduled for 3 AM (in ${hoursUntil3AM} hours)`)
|
||
}
|
||
|
||
/**
|
||
* Stop the cleanup service
|
||
*/
|
||
public stop(): void {
|
||
if (this.cleanupInterval) {
|
||
clearInterval(this.cleanupInterval)
|
||
this.cleanupInterval = null
|
||
}
|
||
this.isRunning = false
|
||
console.log('⏹️ Data cleanup service stopped')
|
||
}
|
||
|
||
/**
|
||
* Run the cleanup job now
|
||
*/
|
||
public async runCleanup(): Promise<void> {
|
||
try {
|
||
console.log('🧹 Starting data cleanup...')
|
||
const startTime = Date.now()
|
||
|
||
// Delete market data older than 1 year (365 days)
|
||
const oneYearAgo = new Date()
|
||
oneYearAgo.setDate(oneYearAgo.getDate() - 365)
|
||
|
||
const deletedCount = await this.prisma.marketData.deleteMany({
|
||
where: {
|
||
createdAt: {
|
||
lt: oneYearAgo
|
||
}
|
||
}
|
||
})
|
||
|
||
const duration = Date.now() - startTime
|
||
console.log(
|
||
`✅ Data cleanup complete: Deleted ${deletedCount.count} old market data rows ` +
|
||
`(older than ${oneYearAgo.toISOString().split('T')[0]}) in ${duration}ms`
|
||
)
|
||
|
||
// Log statistics
|
||
const totalRows = await this.prisma.marketData.count()
|
||
const oldestRow = await this.prisma.marketData.findFirst({
|
||
orderBy: { createdAt: 'asc' }
|
||
})
|
||
const newestRow = await this.prisma.marketData.findFirst({
|
||
orderBy: { createdAt: 'desc' }
|
||
})
|
||
|
||
console.log(`📊 Database stats:`)
|
||
console.log(` Total rows: ${totalRows.toLocaleString()}`)
|
||
if (oldestRow) {
|
||
const oldestDays = Math.round(
|
||
(Date.now() - oldestRow.createdAt.getTime()) / (24 * 60 * 60 * 1000)
|
||
)
|
||
console.log(` Oldest data: ${oldestDays} days old`)
|
||
}
|
||
if (newestRow) {
|
||
const newestMinutes = Math.round(
|
||
(Date.now() - newestRow.createdAt.getTime()) / (60 * 1000)
|
||
)
|
||
console.log(` Newest data: ${newestMinutes} minutes old`)
|
||
}
|
||
|
||
} catch (error) {
|
||
console.error('❌ Error in data cleanup:', error)
|
||
throw error
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Get cleanup statistics (for monitoring)
|
||
*/
|
||
public async getStats(): Promise<{
|
||
totalRows: number
|
||
oldestDate: Date | null
|
||
newestDate: Date | null
|
||
dataSpanDays: number
|
||
}> {
|
||
const totalRows = await this.prisma.marketData.count()
|
||
|
||
const oldestRow = await this.prisma.marketData.findFirst({
|
||
orderBy: { createdAt: 'asc' }
|
||
})
|
||
|
||
const newestRow = await this.prisma.marketData.findFirst({
|
||
orderBy: { createdAt: 'desc' }
|
||
})
|
||
|
||
let dataSpanDays = 0
|
||
if (oldestRow && newestRow) {
|
||
dataSpanDays = Math.round(
|
||
(newestRow.createdAt.getTime() - oldestRow.createdAt.getTime()) /
|
||
(24 * 60 * 60 * 1000)
|
||
)
|
||
}
|
||
|
||
return {
|
||
totalRows,
|
||
oldestDate: oldestRow?.createdAt || null,
|
||
newestDate: newestRow?.createdAt || null,
|
||
dataSpanDays
|
||
}
|
||
}
|
||
}
|
||
|
||
// Singleton instance
|
||
let cleanupService: DataCleanupService | null = null
|
||
|
||
export function getDataCleanupService(): DataCleanupService {
|
||
if (!cleanupService) {
|
||
cleanupService = new DataCleanupService()
|
||
console.log('🔧 Initialized Data Cleanup Service')
|
||
}
|
||
return cleanupService
|
||
}
|
||
|
||
/**
|
||
* Start the cleanup service (called from startup)
|
||
*/
|
||
export function startDataCleanup(): void {
|
||
const service = getDataCleanupService()
|
||
service.start()
|
||
}
|