boardMigrationDetector.js 10 KB

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