boardMigrationDetector.js 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370
  1. /**
  2. * Board Migration Detector
  3. * Detects boards that need migration and manages automatic migration scheduling
  4. */
  5. import { Meteor } from 'meteor/meteor';
  6. import { ReactiveVar } from 'meteor/reactive-var';
  7. import { cronJobStorage } from './cronJobStorage';
  8. // Reactive variables for board migration tracking
  9. export const unmigratedBoards = new ReactiveVar([]);
  10. export const migrationScanInProgress = new ReactiveVar(false);
  11. export const lastMigrationScan = new ReactiveVar(null);
  12. class BoardMigrationDetector {
  13. constructor() {
  14. this.scanInterval = null;
  15. this.isScanning = false;
  16. this.migrationCheckInterval = 30000; // Check every 30 seconds
  17. this.scanInterval = 60000; // Full scan every minute
  18. }
  19. /**
  20. * Start the automatic migration detector
  21. */
  22. start() {
  23. if (this.scanInterval) {
  24. return; // Already running
  25. }
  26. // Check for idle migration opportunities
  27. this.scanInterval = Meteor.setInterval(() => {
  28. this.checkForIdleMigration();
  29. }, this.migrationCheckInterval);
  30. // Full board scan every minute
  31. this.fullScanInterval = Meteor.setInterval(() => {
  32. this.scanUnmigratedBoards();
  33. }, this.scanInterval);
  34. console.log('Board migration detector started');
  35. }
  36. /**
  37. * Stop the automatic migration detector
  38. */
  39. stop() {
  40. if (this.scanInterval) {
  41. Meteor.clearInterval(this.scanInterval);
  42. this.scanInterval = null;
  43. }
  44. if (this.fullScanInterval) {
  45. Meteor.clearInterval(this.fullScanInterval);
  46. this.fullScanInterval = null;
  47. }
  48. }
  49. /**
  50. * Check if system is idle and can run migrations
  51. */
  52. isSystemIdle() {
  53. const resources = cronJobStorage.getSystemResources();
  54. const queueStats = cronJobStorage.getQueueStats();
  55. // Check if no jobs are running
  56. if (queueStats.running > 0) {
  57. return false;
  58. }
  59. // Check if CPU usage is low
  60. if (resources.cpuUsage > 30) { // Lower threshold for idle migration
  61. return false;
  62. }
  63. // Check if memory usage is reasonable
  64. if (resources.memoryUsage > 70) {
  65. return false;
  66. }
  67. return true;
  68. }
  69. /**
  70. * Check for idle migration opportunities
  71. */
  72. async checkForIdleMigration() {
  73. if (!this.isSystemIdle()) {
  74. return;
  75. }
  76. // Get unmigrated boards
  77. const unmigrated = unmigratedBoards.get();
  78. if (unmigrated.length === 0) {
  79. return; // No boards to migrate
  80. }
  81. // Check if we can start a new job
  82. const canStart = cronJobStorage.canStartNewJob();
  83. if (!canStart.canStart) {
  84. return;
  85. }
  86. // Start migrating the next board
  87. const boardToMigrate = unmigrated[0];
  88. await this.startBoardMigration(boardToMigrate);
  89. }
  90. /**
  91. * Scan for unmigrated boards
  92. */
  93. async scanUnmigratedBoards() {
  94. if (this.isScanning) {
  95. return; // Already scanning
  96. }
  97. this.isScanning = true;
  98. migrationScanInProgress.set(true);
  99. try {
  100. console.log('Scanning for unmigrated boards...');
  101. // Get all boards from the database
  102. const boards = this.getAllBoards();
  103. const unmigrated = [];
  104. for (const board of boards) {
  105. if (await this.needsMigration(board)) {
  106. unmigrated.push(board);
  107. }
  108. }
  109. unmigratedBoards.set(unmigrated);
  110. lastMigrationScan.set(new Date());
  111. console.log(`Found ${unmigrated.length} unmigrated boards`);
  112. } catch (error) {
  113. console.error('Error scanning for unmigrated boards:', error);
  114. } finally {
  115. this.isScanning = false;
  116. migrationScanInProgress.set(false);
  117. }
  118. }
  119. /**
  120. * Get all boards from the database
  121. */
  122. getAllBoards() {
  123. // This would need to be implemented based on your board model
  124. // For now, we'll simulate getting boards
  125. try {
  126. // Assuming you have a Boards collection
  127. if (typeof Boards !== 'undefined') {
  128. return Boards.find({}, { fields: { _id: 1, title: 1, createdAt: 1, modifiedAt: 1 } }).fetch();
  129. }
  130. // Fallback: return empty array if Boards collection not available
  131. return [];
  132. } catch (error) {
  133. console.error('Error getting boards:', error);
  134. return [];
  135. }
  136. }
  137. /**
  138. * Check if a board needs migration
  139. */
  140. async needsMigration(board) {
  141. try {
  142. // Check if board has been migrated by looking for migration markers
  143. const migrationMarkers = this.getMigrationMarkers(board._id);
  144. // Check for specific migration indicators
  145. const needsListMigration = !migrationMarkers.listsMigrated;
  146. const needsAttachmentMigration = !migrationMarkers.attachmentsMigrated;
  147. const needsSwimlaneMigration = !migrationMarkers.swimlanesMigrated;
  148. return needsListMigration || needsAttachmentMigration || needsSwimlaneMigration;
  149. } catch (error) {
  150. console.error(`Error checking migration status for board ${board._id}:`, error);
  151. return false;
  152. }
  153. }
  154. /**
  155. * Get migration markers for a board
  156. */
  157. getMigrationMarkers(boardId) {
  158. try {
  159. // Check if board has migration metadata
  160. const board = Boards.findOne(boardId, { fields: { migrationMarkers: 1 } });
  161. if (!board || !board.migrationMarkers) {
  162. return {
  163. listsMigrated: false,
  164. attachmentsMigrated: false,
  165. swimlanesMigrated: false
  166. };
  167. }
  168. return board.migrationMarkers;
  169. } catch (error) {
  170. console.error(`Error getting migration markers for board ${boardId}:`, error);
  171. return {
  172. listsMigrated: false,
  173. attachmentsMigrated: false,
  174. swimlanesMigrated: false
  175. };
  176. }
  177. }
  178. /**
  179. * Start migration for a specific board
  180. */
  181. async startBoardMigration(board) {
  182. try {
  183. console.log(`Starting migration for board: ${board.title || board._id}`);
  184. // Create migration job for this board
  185. const jobId = `board_migration_${board._id}_${Date.now()}`;
  186. // Add to job queue with high priority
  187. cronJobStorage.addToQueue(jobId, 'board_migration', 1, {
  188. boardId: board._id,
  189. boardTitle: board.title,
  190. migrationType: 'full_board_migration'
  191. });
  192. // Save initial job status
  193. cronJobStorage.saveJobStatus(jobId, {
  194. jobType: 'board_migration',
  195. status: 'pending',
  196. progress: 0,
  197. boardId: board._id,
  198. boardTitle: board.title,
  199. migrationType: 'full_board_migration',
  200. createdAt: new Date()
  201. });
  202. // Remove from unmigrated list
  203. const currentUnmigrated = unmigratedBoards.get();
  204. const updatedUnmigrated = currentUnmigrated.filter(b => b._id !== board._id);
  205. unmigratedBoards.set(updatedUnmigrated);
  206. return jobId;
  207. } catch (error) {
  208. console.error(`Error starting migration for board ${board._id}:`, error);
  209. throw error;
  210. }
  211. }
  212. /**
  213. * Get migration statistics
  214. */
  215. getMigrationStats() {
  216. const unmigrated = unmigratedBoards.get();
  217. const lastScan = lastMigrationScan.get();
  218. const isScanning = migrationScanInProgress.get();
  219. return {
  220. unmigratedCount: unmigrated.length,
  221. lastScanTime: lastScan,
  222. isScanning,
  223. nextScanIn: this.scanInterval ? this.scanInterval / 1000 : 0
  224. };
  225. }
  226. /**
  227. * Force a full scan of all boards
  228. */
  229. async forceScan() {
  230. console.log('Forcing full board migration scan...');
  231. await this.scanUnmigratedBoards();
  232. }
  233. /**
  234. * Get detailed migration status for a specific board
  235. */
  236. getBoardMigrationStatus(boardId) {
  237. const unmigrated = unmigratedBoards.get();
  238. const isUnmigrated = unmigrated.some(b => b._id === boardId);
  239. if (!isUnmigrated) {
  240. return { needsMigration: false, reason: 'Board is already migrated' };
  241. }
  242. const migrationMarkers = this.getMigrationMarkers(boardId);
  243. const needsMigration = !migrationMarkers.listsMigrated ||
  244. !migrationMarkers.attachmentsMigrated ||
  245. !migrationMarkers.swimlanesMigrated;
  246. return {
  247. needsMigration,
  248. migrationMarkers,
  249. reason: needsMigration ? 'Board requires migration' : 'Board is up to date'
  250. };
  251. }
  252. /**
  253. * Mark a board as migrated
  254. */
  255. markBoardAsMigrated(boardId, migrationType) {
  256. try {
  257. // Update migration markers
  258. const updateQuery = {};
  259. updateQuery[`migrationMarkers.${migrationType}Migrated`] = true;
  260. updateQuery['migrationMarkers.lastMigration'] = new Date();
  261. Boards.update(boardId, { $set: updateQuery });
  262. // Remove from unmigrated list if present
  263. const currentUnmigrated = unmigratedBoards.get();
  264. const updatedUnmigrated = currentUnmigrated.filter(b => b._id !== boardId);
  265. unmigratedBoards.set(updatedUnmigrated);
  266. console.log(`Marked board ${boardId} as migrated for ${migrationType}`);
  267. } catch (error) {
  268. console.error(`Error marking board ${boardId} as migrated:`, error);
  269. }
  270. }
  271. }
  272. // Export singleton instance
  273. export const boardMigrationDetector = new BoardMigrationDetector();
  274. // Start the detector on server startup
  275. Meteor.startup(() => {
  276. // Wait a bit for the system to initialize
  277. Meteor.setTimeout(() => {
  278. boardMigrationDetector.start();
  279. }, 10000); // Start after 10 seconds
  280. });
  281. // Meteor methods for client access
  282. Meteor.methods({
  283. 'boardMigration.getStats'() {
  284. if (!this.userId) {
  285. throw new Meteor.Error('not-authorized');
  286. }
  287. return boardMigrationDetector.getMigrationStats();
  288. },
  289. 'boardMigration.forceScan'() {
  290. if (!this.userId) {
  291. throw new Meteor.Error('not-authorized');
  292. }
  293. return boardMigrationDetector.forceScan();
  294. },
  295. 'boardMigration.getBoardStatus'(boardId) {
  296. if (!this.userId) {
  297. throw new Meteor.Error('not-authorized');
  298. }
  299. return boardMigrationDetector.getBoardMigrationStatus(boardId);
  300. },
  301. 'boardMigration.markAsMigrated'(boardId, migrationType) {
  302. if (!this.userId) {
  303. throw new Meteor.Error('not-authorized');
  304. }
  305. return boardMigrationDetector.markBoardAsMigrated(boardId, migrationType);
  306. }
  307. });