StateCacheService.ts 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141
  1. import { Logger } from 'winston'
  2. import { ReadonlyConfig, StorageNodeDownloadResponse } from '../../types'
  3. import { LoggingService } from '../logging'
  4. import fs from 'fs'
  5. export interface PendingDownloadData {
  6. objectSize: number
  7. promise: Promise<StorageNodeDownloadResponse>
  8. }
  9. export interface StorageNodeEndpointData {
  10. responseTimes: number[]
  11. }
  12. export class StateCacheService {
  13. private logger: Logger
  14. private config: ReadonlyConfig
  15. private cacheFilePath: string
  16. private saveInterval: NodeJS.Timeout
  17. private memoryState = {
  18. pendingDownloadsByContentHash: new Map<string, PendingDownloadData>(),
  19. contentHashByObjectId: new Map<string, string>(),
  20. storageNodeEndpointDataByEndpoint: new Map<string, StorageNodeEndpointData>(),
  21. }
  22. private storedState = {
  23. lruContentHashes: new Set<string>(),
  24. mimeTypeByContentHash: new Map<string, string>(),
  25. }
  26. public constructor(config: ReadonlyConfig, logging: LoggingService, saveIntervalMs = 60 * 1000) {
  27. this.logger = logging.createLogger('StateCacheService')
  28. this.cacheFilePath = `${config.directories.cache}/cache.json`
  29. this.config = config
  30. this.saveInterval = setInterval(() => this.save(), saveIntervalMs)
  31. }
  32. public setContentMimeType(contentHash: string, mimeType: string): void {
  33. this.storedState.mimeTypeByContentHash.set(contentHash, mimeType)
  34. }
  35. public getContentMimeType(contentHash: string): string | undefined {
  36. return this.storedState.mimeTypeByContentHash.get(contentHash)
  37. }
  38. public setObjectContentHash(objectId: string, hash: string): void {
  39. this.memoryState.contentHashByObjectId.set(objectId, hash)
  40. }
  41. public getObjectContentHash(objectId: string): string | undefined {
  42. return this.memoryState.contentHashByObjectId.get(objectId)
  43. }
  44. public useContent(contentHash: string): void {
  45. if (this.storedState.lruContentHashes.has(contentHash)) {
  46. this.storedState.lruContentHashes.delete(contentHash)
  47. }
  48. this.storedState.lruContentHashes.add(contentHash)
  49. }
  50. public newPendingDownload(
  51. contentHash: string,
  52. objectSize: number,
  53. promise: Promise<StorageNodeDownloadResponse>
  54. ): PendingDownloadData {
  55. const pendingDownload: PendingDownloadData = {
  56. objectSize,
  57. promise,
  58. }
  59. this.memoryState.pendingDownloadsByContentHash.set(contentHash, pendingDownload)
  60. return pendingDownload
  61. }
  62. public getPendingDownload(contentHash: string): PendingDownloadData | undefined {
  63. return this.memoryState.pendingDownloadsByContentHash.get(contentHash)
  64. }
  65. public dropPendingDownload(contentHash: string): void {
  66. this.memoryState.pendingDownloadsByContentHash.delete(contentHash)
  67. }
  68. public dropByHash(contentHash: string): void {
  69. this.storedState.mimeTypeByContentHash.delete(contentHash)
  70. this.storedState.lruContentHashes.delete(contentHash)
  71. }
  72. public setStorageNodeEndpointResponseTime(endpoint: string, time: number): void {
  73. const data = this.memoryState.storageNodeEndpointDataByEndpoint.get(endpoint) || { responseTimes: [] }
  74. data.responseTimes.push(time)
  75. }
  76. public getStorageNodeEndpointData(endpoint: string): StorageNodeEndpointData | undefined {
  77. return this.memoryState.storageNodeEndpointDataByEndpoint.get(endpoint)
  78. }
  79. private serializeData() {
  80. const { lruContentHashes, mimeTypeByContentHash } = this.storedState
  81. return JSON.stringify({
  82. lruContentHashes: Array.from(lruContentHashes),
  83. mimeTypeByContentHash: Array.from(mimeTypeByContentHash.entries()),
  84. })
  85. }
  86. public async save(): Promise<boolean> {
  87. return new Promise((resolve) => {
  88. const serialized = this.serializeData()
  89. const fd = fs.openSync(this.cacheFilePath, 'w')
  90. fs.write(fd, serialized, (err) => {
  91. fs.closeSync(fd)
  92. if (err) {
  93. this.logger.error('Cache file save error', { err })
  94. resolve(false)
  95. } else {
  96. this.logger.info('Cache file updated')
  97. resolve(true)
  98. }
  99. })
  100. })
  101. }
  102. public saveSync(): void {
  103. const serialized = this.serializeData()
  104. fs.writeFileSync(this.cacheFilePath, serialized)
  105. }
  106. public load(): void {
  107. if (fs.existsSync(this.cacheFilePath)) {
  108. this.logger.info('Loading cache from file', { file: this.cacheFilePath })
  109. const fileContent = JSON.parse(fs.readFileSync(this.cacheFilePath).toString())
  110. this.storedState.lruContentHashes = new Set<string>(fileContent.lruContentHashes || [])
  111. this.storedState.mimeTypeByContentHash = new Map<string, string>(fileContent.mimeTypeByContentHash || [])
  112. } else {
  113. this.logger.warn(`Cache file (${this.cacheFilePath}) is empty. Starting from scratch`)
  114. }
  115. }
  116. public clearInterval(): void {
  117. clearInterval(this.saveInterval)
  118. }
  119. }