wekan/server/boardMigrationDetector.js

435 lines
12 KiB
JavaScript
Raw Normal View History

/**
* 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 and version
const updateQuery = {};
updateQuery[`migrationMarkers.${migrationType}Migrated`] = true;
updateQuery['migrationMarkers.lastMigration'] = new Date();
updateQuery['migrationVersion'] = 1; // Set migration version to prevent re-migration
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();
// Note: Automatic migration detector is disabled - migrations only run when opening boards
// 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);
},
'boardMigration.fixStuckBoards'() {
if (!this.userId) {
throw new Meteor.Error('not-authorized');
}
// Find boards that have migration markers but no migrationVersion
const stuckBoards = Boards.find({
'migrationMarkers.fullMigrationCompleted': true,
$or: [
{ migrationVersion: { $exists: false } },
{ migrationVersion: { $lt: 1 } }
]
}).fetch();
let fixedCount = 0;
stuckBoards.forEach(board => {
try {
Boards.update(board._id, {
$set: {
migrationVersion: 1,
'migrationMarkers.lastMigration': new Date()
}
});
fixedCount++;
console.log(`Fixed stuck board: ${board._id} (${board.title})`);
} catch (error) {
console.error(`Error fixing board ${board._id}:`, error);
}
});
return {
message: `Fixed ${fixedCount} stuck boards`,
fixedCount,
totalStuck: stuckBoards.length
};
}
});