index.ts 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151
  1. import { ReadonlyConfig } from '../types'
  2. import { NetworkingService } from '../services/networking'
  3. import { LoggingService } from '../services/logging'
  4. import { StateCacheService } from '../services/cache/StateCacheService'
  5. import { ContentService } from '../services/content/ContentService'
  6. import { HttpApiService } from '../services/httpApi/HttpApiService'
  7. import { Logger } from 'winston'
  8. import fs from 'fs'
  9. import nodeCleanup from 'node-cleanup'
  10. import { AppIntervals } from '../types/app'
  11. export class App {
  12. private config: ReadonlyConfig
  13. private content: ContentService
  14. private stateCache: StateCacheService
  15. private networking: NetworkingService
  16. private httpApi: HttpApiService
  17. private logging: LoggingService
  18. private logger: Logger
  19. private intervals: AppIntervals | undefined
  20. constructor(config: ReadonlyConfig) {
  21. this.config = config
  22. this.logging = LoggingService.withAppConfig(config)
  23. this.stateCache = new StateCacheService(config, this.logging)
  24. this.networking = new NetworkingService(config, this.stateCache, this.logging)
  25. this.content = new ContentService(config, this.logging, this.networking, this.stateCache)
  26. this.httpApi = new HttpApiService(config, this.stateCache, this.content, this.logging, this.networking)
  27. this.logger = this.logging.createLogger('App')
  28. }
  29. private setIntervals() {
  30. this.intervals = {
  31. saveCacheState: setInterval(() => this.stateCache.save(), this.config.intervals.saveCacheState * 1000),
  32. checkStorageNodeResponseTimes: setInterval(
  33. () => this.networking.checkActiveStorageNodeEndpoints(),
  34. this.config.intervals.checkStorageNodeResponseTimes * 1000
  35. ),
  36. cacheCleanup: setInterval(() => this.content.cacheCleanup(), this.config.intervals.cacheCleanup * 1000),
  37. }
  38. }
  39. private clearIntervals() {
  40. if (this.intervals) {
  41. Object.values(this.intervals).forEach((interval) => clearInterval(interval))
  42. }
  43. }
  44. private checkConfigDirectories(): void {
  45. Object.entries(this.config.directories).forEach(([name, path]) => {
  46. if (path === undefined) {
  47. return
  48. }
  49. const dirInfo = `${name} directory (${path})`
  50. if (!fs.existsSync(path)) {
  51. try {
  52. fs.mkdirSync(path, { recursive: true })
  53. } catch (e) {
  54. throw new Error(`${dirInfo} doesn't exist and cannot be created!`)
  55. }
  56. }
  57. try {
  58. fs.accessSync(path, fs.constants.R_OK)
  59. } catch (e) {
  60. throw new Error(`${dirInfo} is not readable`)
  61. }
  62. try {
  63. fs.accessSync(path, fs.constants.W_OK)
  64. } catch (e) {
  65. throw new Error(`${dirInfo} is not writable`)
  66. }
  67. })
  68. }
  69. public async start(): Promise<void> {
  70. this.logger.info('Starting the app', { config: this.config })
  71. try {
  72. this.checkConfigDirectories()
  73. this.stateCache.load()
  74. await this.content.startupInit()
  75. this.setIntervals()
  76. this.httpApi.start()
  77. } catch (err) {
  78. this.logger.error('Node initialization failed!', { err })
  79. process.exit(-1)
  80. }
  81. nodeCleanup(this.exitHandler.bind(this))
  82. }
  83. private async exitGracefully(): Promise<void> {
  84. // Async exit handler - ideally should not take more than 10 sec
  85. // We can try to wait until some pending downloads are finished here etc.
  86. this.logger.info('Graceful exit initialized')
  87. // Try to process remaining downloads
  88. const MAX_RETRY_ATTEMPTS = 3
  89. let retryCounter = 0
  90. while (retryCounter < MAX_RETRY_ATTEMPTS && this.stateCache.getPendingDownloadsCount()) {
  91. const pendingDownloadsCount = this.stateCache.getPendingDownloadsCount()
  92. this.logger.info(`${pendingDownloadsCount} pending downloads in progress... Retrying exit in 5 sec...`, {
  93. retryCounter,
  94. pendingDownloadsCount,
  95. })
  96. await new Promise((resolve) => setTimeout(resolve, 5000))
  97. this.stateCache.saveSync()
  98. ++retryCounter
  99. }
  100. if (this.stateCache.getPendingDownloadsCount()) {
  101. this.logger.warn('Limit reached: Could not finish all pending downloads.', {
  102. pendingDownloadsCount: this.stateCache.getPendingDownloadsCount(),
  103. })
  104. }
  105. this.logger.info('Graceful exit finished')
  106. await this.logging.end()
  107. }
  108. private exitCritically(): void {
  109. // Some additional synchronous work if required...
  110. this.logger.info('Critical exit finished')
  111. }
  112. private exitHandler(exitCode: number | null, signal: string | null): boolean | undefined {
  113. this.logger.info('Exiting...')
  114. // Clear intervals
  115. this.clearIntervals()
  116. // Stop the http api
  117. this.httpApi.stop()
  118. // Save cache
  119. this.stateCache.saveSync()
  120. if (signal) {
  121. // Async exit can be executed
  122. this.exitGracefully()
  123. .then(() => {
  124. process.kill(process.pid, signal)
  125. })
  126. .catch((err) => {
  127. this.logger.error('Graceful exit error', { err })
  128. this.logging.end().finally(() => {
  129. process.kill(process.pid, signal)
  130. })
  131. })
  132. nodeCleanup.uninstall()
  133. return false
  134. } else {
  135. // Only synchronous work can be done here
  136. this.exitCritically()
  137. }
  138. }
  139. }