index.ts 6.0 KB

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