stats-aggregation.scheduler.ts 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146
  1. import { Injectable, Logger, OnModuleInit } from '@nestjs/common';
  2. import { Cron, CronExpression } from '@nestjs/schedule';
  3. import { ConfigService } from '@nestjs/config';
  4. import { StatsAggregationService } from './stats-aggregation.service';
  5. type AggregationResult = {
  6. successCount: number;
  7. totalProcessed: number;
  8. errorCount: number;
  9. };
  10. @Injectable()
  11. export class StatsAggregationScheduler implements OnModuleInit {
  12. private readonly logger = new Logger(StatsAggregationScheduler.name);
  13. private enabled = true;
  14. private windowDays?: number;
  15. // guardrails: avoid overlapping runs + spam
  16. private runningAds = false;
  17. private runningVideo = false;
  18. constructor(
  19. private readonly configService: ConfigService,
  20. private readonly statsAggregation: StatsAggregationService,
  21. ) {}
  22. onModuleInit(): void {
  23. // Evaluate config on module init (safer than constructor for startup ordering)
  24. const enabledRaw = this.configService
  25. .get<string>('STATS_AGGREGATION_ENABLED')
  26. ?.trim()
  27. .toLowerCase();
  28. // default: enabled (unless explicitly "false" or "0" etc.)
  29. this.enabled = !['false', '0', 'off', 'no'].includes(enabledRaw ?? '');
  30. const daysRaw = this.configService
  31. .get<string>('STATS_AGGREGATION_WINDOW_DAYS')
  32. ?.trim();
  33. if (daysRaw) {
  34. const parsed = Number.parseInt(daysRaw, 10);
  35. if (Number.isFinite(parsed) && parsed > 0) {
  36. this.windowDays = parsed;
  37. } else {
  38. this.logger.warn(
  39. `Invalid STATS_AGGREGATION_WINDOW_DAYS="${daysRaw}" (expected positive integer). Falling back to "all time".`,
  40. );
  41. this.windowDays = undefined;
  42. }
  43. }
  44. if (this.enabled) {
  45. this.logger.log(
  46. `📊 Stats aggregation scheduler enabled (windowDays=${
  47. this.windowDays ?? 'all time'
  48. }, interval=${CronExpression.EVERY_5_MINUTES})`,
  49. );
  50. } else {
  51. this.logger.warn(
  52. `📊 Stats aggregation scheduler DISABLED (STATS_AGGREGATION_ENABLED="${enabledRaw ?? ''}")`,
  53. );
  54. }
  55. }
  56. @Cron(CronExpression.EVERY_5_MINUTES, { name: 'stats-aggregation-ads' })
  57. async runAdsAggregation(): Promise<void> {
  58. if (!this.enabled) return;
  59. if (this.runningAds) {
  60. this.logger.warn(
  61. '⏭️ Skip ads aggregation: previous run still in progress',
  62. );
  63. return;
  64. }
  65. this.runningAds = true;
  66. const start = Date.now();
  67. this.logger.log(
  68. `⏰ Ads aggregation start (windowDays=${this.windowDays ?? 'all time'})`,
  69. );
  70. try {
  71. const result = (await this.statsAggregation.aggregateAdsStats({
  72. windowDays: this.windowDays,
  73. })) as AggregationResult;
  74. const ms = Date.now() - start;
  75. this.logger.log(
  76. `✅ Ads aggregation done in ${ms}ms (${result.successCount}/${result.totalProcessed} updated, ${result.errorCount} errors)`,
  77. );
  78. } catch (err) {
  79. const ms = Date.now() - start;
  80. this.logger.error(
  81. `❌ Ads aggregation failed after ${ms}ms: ${
  82. err instanceof Error ? err.message : String(err)
  83. }`,
  84. err instanceof Error ? err.stack : undefined,
  85. );
  86. } finally {
  87. this.runningAds = false;
  88. }
  89. }
  90. @Cron(CronExpression.EVERY_5_MINUTES, { name: 'stats-aggregation-video' })
  91. async runVideoAggregation(): Promise<void> {
  92. if (!this.enabled) return;
  93. if (this.runningVideo) {
  94. this.logger.warn(
  95. '⏭️ Skip video aggregation: previous run still in progress',
  96. );
  97. return;
  98. }
  99. this.runningVideo = true;
  100. const start = Date.now();
  101. this.logger.log(
  102. `⏰ Video aggregation start (windowDays=${this.windowDays ?? 'all time'})`,
  103. );
  104. try {
  105. const result = (await this.statsAggregation.aggregateVideoStats({
  106. windowDays: this.windowDays,
  107. })) as AggregationResult;
  108. const ms = Date.now() - start;
  109. this.logger.log(
  110. `✅ Video aggregation done in ${ms}ms (${result.successCount}/${result.totalProcessed} updated, ${result.errorCount} errors)`,
  111. );
  112. } catch (err) {
  113. const ms = Date.now() - start;
  114. this.logger.error(
  115. `❌ Video aggregation failed after ${ms}ms: ${
  116. err instanceof Error ? err.message : String(err)
  117. }`,
  118. err instanceof Error ? err.stack : undefined,
  119. );
  120. } finally {
  121. this.runningVideo = false;
  122. }
  123. }
  124. }