File size: 4,370 Bytes
521a9b6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
import { config, validateConfig } from './config';
import { logger, setWorkerId } from './utils/logger';
import { createApp } from './server/app';
import { startWorkerManager, stopWorkerManager } from './worker/manager';
import { closeBrowser } from './worker/browser-pool';
import { runQuotaCheckCron } from './cron/quota-check';
import { runCleanupReportsCron } from './cron/cleanup-reports';
import { runStaleRecoveryCron } from './cron/stale-recovery';
import * as cron from 'node-cron';

async function main(): Promise<void> {
  // Validate environment
  try {
    validateConfig();
  } catch (err) {
    console.error('Configuration error:', err instanceof Error ? err.message : err);
    process.exit(1);
  }

  setWorkerId(config.workerId);
  logger.info('Turnitin Worker starting', {
    workerId: config.workerId,
    maxWorkers: config.maxWorkers,
    port: config.port,
    enableWorker: config.enableWorker,
    enableCron: config.enableCron,
  });

  // Start Express server
  const app = createApp();
  const server = app.listen(config.port, () => {
    logger.info(`HTTP server listening on port ${config.port}`);
  });

  if (config.enableWorker) {
    // Start worker manager (polls for jobs)
    startWorkerManager().catch((err) => {
      logger.error('Worker manager fatal error', {
        error: err instanceof Error ? err.message : String(err),
      });
    });
  } else {
    logger.info('Worker manager disabled by ENABLE_WORKER=false');
  }

  if (config.enableCron) {
    // Schedule cron jobs
    // Quota check: every N minutes
    const quotaCronExpr = `*/${config.quotaCheckInterval} * * * *`;
    cron.schedule(quotaCronExpr, () => {
      runQuotaCheckCron().catch((err) => {
        logger.error('Quota check cron error', {
          error: err instanceof Error ? err.message : String(err),
        });
      });
    });
    logger.info(`Quota check cron scheduled: ${quotaCronExpr}`);

    // Report cleanup: every N minutes
    const cleanupCronExpr = `*/${config.cleanupInterval} * * * *`;
    cron.schedule(cleanupCronExpr, () => {
      runCleanupReportsCron().catch((err) => {
        logger.error('Report cleanup cron error', {
          error: err instanceof Error ? err.message : String(err),
        });
      });
    });
    logger.info(`Report cleanup cron scheduled: ${cleanupCronExpr}`);

    // Stale recovery: every N minutes
    const staleCronExpr = `*/${config.staleRecoveryInterval} * * * *`;
    cron.schedule(staleCronExpr, () => {
      runStaleRecoveryCron().catch((err) => {
        logger.error('Stale recovery cron error', {
          error: err instanceof Error ? err.message : String(err),
        });
      });
    });
    logger.info(`Stale recovery cron scheduled: ${staleCronExpr}`);
  } else {
    logger.info('Cron jobs disabled by ENABLE_CRON=false');
  }

  logger.info('All systems started successfully');

  // Graceful shutdown
  const shutdown = async (signal: string) => {
    logger.info(`Received ${signal}, shutting down gracefully...`);

    // Stop accepting new jobs
    stopWorkerManager();

    // Close HTTP server
    server.close(() => {
      logger.info('HTTP server closed');
    });

    // Wait for active workers to finish (max 60 seconds)
    const maxWait = 60000;
    const start = Date.now();
    const { getActiveWorkerCount } = await import('./worker/manager');
    while (getActiveWorkerCount() > 0 && Date.now() - start < maxWait) {
      logger.info(`Waiting for ${getActiveWorkerCount()} active workers to finish...`);
      await new Promise((r) => setTimeout(r, 2000));
    }

    // Close browser
    await closeBrowser();

    logger.info('Shutdown complete');
    process.exit(0);
  };

  process.on('SIGTERM', () => shutdown('SIGTERM'));
  process.on('SIGINT', () => shutdown('SIGINT'));

  // Handle uncaught errors
  process.on('uncaughtException', (err) => {
    logger.error('Uncaught exception', {
      error: err.message,
      stack: err.stack,
    });
    // Don't exit on uncaught exceptions in worker service
    // The worker loop has its own error handling
  });

  process.on('unhandledRejection', (reason) => {
    logger.error('Unhandled promise rejection', {
      reason: reason instanceof Error ? reason.message : String(reason),
    });
  });
}

main().catch((err) => {
  console.error('Fatal startup error:', err);
  process.exit(1);
});