/** * Board Migration Detector * Detects boards that need migration and manages automatic migration scheduling */ import { Meteor } from 'meteor/meteor'; import { ReactiveVar } from 'meteor/reactive-var'; import { check, Match } from 'meteor/check'; import { cronJobStorage } from './cronJobStorage'; import Boards from '/models/boards'; // Reactive variables for board migration tracking export const unmigratedBoards = new ReactiveVar([]); export const migrationScanInProgress = new ReactiveVar(false); export const lastMigrationScan = new ReactiveVar(null); class BoardMigrationDetector { constructor() { this.scanInterval = null; this.isScanning = false; this.migrationCheckInterval = 30000; // Check every 30 seconds this.scanInterval = 60000; // Full scan every minute } /** * Start the automatic migration detector */ start() { if (this.scanInterval) { return; // Already running } // Check for idle migration opportunities this.scanInterval = Meteor.setInterval(() => { this.checkForIdleMigration(); }, this.migrationCheckInterval); // Full board scan every minute this.fullScanInterval = Meteor.setInterval(() => { this.scanUnmigratedBoards(); }, this.scanInterval); // Board migration detector started } /** * Stop the automatic migration detector */ stop() { if (this.scanInterval) { Meteor.clearInterval(this.scanInterval); this.scanInterval = null; } if (this.fullScanInterval) { Meteor.clearInterval(this.fullScanInterval); this.fullScanInterval = null; } } /** * Check if system is idle and can run migrations */ isSystemIdle() { const resources = cronJobStorage.getSystemResources(); const queueStats = cronJobStorage.getQueueStats(); // Check if no jobs are running if (queueStats.running > 0) { return false; } // Check if CPU usage is low if (resources.cpuUsage > 30) { // Lower threshold for idle migration return false; } // Check if memory usage is reasonable if (resources.memoryUsage > 85) { return false; } return true; } /** * Check for idle migration opportunities */ async checkForIdleMigration() { if (!this.isSystemIdle()) { return; } // Get unmigrated boards const unmigrated = unmigratedBoards.get(); if (unmigrated.length === 0) { return; // No boards to migrate } // Check if we can start a new job const canStart = cronJobStorage.canStartNewJob(); if (!canStart.canStart) { return; } // Start migrating the next board const boardToMigrate = unmigrated[0]; await this.startBoardMigration(boardToMigrate); } /** * Scan for unmigrated boards */ async scanUnmigratedBoards() { if (this.isScanning) { return; // Already scanning } this.isScanning = true; migrationScanInProgress.set(true); try { // Scanning for unmigrated boards // Get all boards from the database const boards = this.getAllBoards(); const unmigrated = []; for (const board of boards) { if (await this.needsMigration(board)) { unmigrated.push(board); } } unmigratedBoards.set(unmigrated); lastMigrationScan.set(new Date()); // Found unmigrated boards } catch (error) { console.error('Error scanning for unmigrated boards:', error); } finally { this.isScanning = false; migrationScanInProgress.set(false); } } /** * Get all boards from the database */ getAllBoards() { // This would need to be implemented based on your board model // For now, we'll simulate getting boards try { // Assuming you have a Boards collection if (typeof Boards !== 'undefined') { return Boards.find({}, { fields: { _id: 1, title: 1, createdAt: 1, modifiedAt: 1 } }).fetch(); } // Fallback: return empty array if Boards collection not available return []; } catch (error) { console.error('Error getting boards:', error); return []; } } /** * Check if a board needs migration */ async needsMigration(board) { try { // Check if board has been migrated by looking for migration markers const migrationMarkers = this.getMigrationMarkers(board._id); // Check for specific migration indicators const needsListMigration = !migrationMarkers.listsMigrated; const needsAttachmentMigration = !migrationMarkers.attachmentsMigrated; const needsSwimlaneMigration = !migrationMarkers.swimlanesMigrated; return needsListMigration || needsAttachmentMigration || needsSwimlaneMigration; } catch (error) { console.error(`Error checking migration status for board ${board._id}:`, error); return false; } } /** * Get migration markers for a board */ getMigrationMarkers(boardId) { try { // Check if board has migration metadata const board = Boards.findOne(boardId, { fields: { migrationMarkers: 1 } }); if (!board || !board.migrationMarkers) { return { listsMigrated: false, attachmentsMigrated: false, swimlanesMigrated: false }; } return board.migrationMarkers; } catch (error) { console.error(`Error getting migration markers for board ${boardId}:`, error); return { listsMigrated: false, attachmentsMigrated: false, swimlanesMigrated: false }; } } /** * Start migration for a specific board */ async startBoardMigration(boardId) { try { const board = Boards.findOne(boardId); if (!board) { throw new Error(`Board ${boardId} not found`); } // Check if board already has latest migration version if (board.migrationVersion && board.migrationVersion >= 1) { console.log(`Board ${boardId} already has latest migration version`); return null; } // Create migration job for this board const jobId = `board_migration_${board._id}_${Date.now()}`; // Add to job queue with high priority cronJobStorage.addToQueue(jobId, 'board_migration', 1, { boardId: board._id, boardTitle: board.title, migrationType: 'full_board_migration' }); // Save initial job status cronJobStorage.saveJobStatus(jobId, { jobType: 'board_migration', status: 'pending', progress: 0, boardId: board._id, boardTitle: board.title, migrationType: 'full_board_migration', createdAt: new Date() }); // Remove from unmigrated list const currentUnmigrated = unmigratedBoards.get(); const updatedUnmigrated = currentUnmigrated.filter(b => b._id !== board._id); unmigratedBoards.set(updatedUnmigrated); return jobId; } catch (error) { console.error(`Error starting migration for board ${boardId}:`, error); throw error; } } /** * Get migration statistics */ getMigrationStats() { const unmigrated = unmigratedBoards.get(); const lastScan = lastMigrationScan.get(); const isScanning = migrationScanInProgress.get(); return { unmigratedCount: unmigrated.length, lastScanTime: lastScan, isScanning, nextScanIn: this.scanInterval ? this.scanInterval / 1000 : 0 }; } /** * Force a full scan of all boards */ async forceScan() { // Forcing full board migration scan await this.scanUnmigratedBoards(); } /** * Get detailed migration status for a specific board */ getBoardMigrationStatus(boardId) { const unmigrated = unmigratedBoards.get(); const isUnmigrated = unmigrated.some(b => b._id === boardId); if (!isUnmigrated) { return { needsMigration: false, reason: 'Board is already migrated' }; } const migrationMarkers = this.getMigrationMarkers(boardId); const needsMigration = !migrationMarkers.listsMigrated || !migrationMarkers.attachmentsMigrated || !migrationMarkers.swimlanesMigrated; return { needsMigration, migrationMarkers, reason: needsMigration ? 'Board requires migration' : 'Board is up to date' }; } /** * Mark a board as migrated */ markBoardAsMigrated(boardId, migrationType) { try { // Update migration markers const updateQuery = {}; updateQuery[`migrationMarkers.${migrationType}Migrated`] = true; updateQuery['migrationMarkers.lastMigration'] = new Date(); Boards.update(boardId, { $set: updateQuery }); // Remove from unmigrated list if present const currentUnmigrated = unmigratedBoards.get(); const updatedUnmigrated = currentUnmigrated.filter(b => b._id !== boardId); unmigratedBoards.set(updatedUnmigrated); // Marked board as migrated } catch (error) { console.error(`Error marking board ${boardId} as migrated:`, error); } } } // Export singleton instance export const boardMigrationDetector = new BoardMigrationDetector(); // Start the detector on server startup Meteor.startup(() => { // Wait a bit for the system to initialize Meteor.setTimeout(() => { boardMigrationDetector.start(); }, 10000); // Start after 10 seconds }); // Meteor methods for client access Meteor.methods({ 'boardMigration.getStats'() { if (!this.userId) { throw new Meteor.Error('not-authorized'); } return boardMigrationDetector.getMigrationStats(); }, 'boardMigration.forceScan'() { if (!this.userId) { throw new Meteor.Error('not-authorized'); } return boardMigrationDetector.forceScan(); }, 'boardMigration.getBoardStatus'(boardId) { check(boardId, String); if (!this.userId) { throw new Meteor.Error('not-authorized'); } return boardMigrationDetector.getBoardMigrationStatus(boardId); }, 'boardMigration.markAsMigrated'(boardId, migrationType) { check(boardId, String); check(migrationType, String); if (!this.userId) { throw new Meteor.Error('not-authorized'); } return boardMigrationDetector.markBoardAsMigrated(boardId, migrationType); }, 'boardMigration.startBoardMigration'(boardId) { check(boardId, String); if (!this.userId) { throw new Meteor.Error('not-authorized'); } return boardMigrationDetector.startBoardMigration(boardId); } });