ProjectHistoryRedisManager.js 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163
  1. const Settings = require('@overleaf/settings')
  2. const { callbackifyAll } = require('@overleaf/promise-utils')
  3. const projectHistoryKeys = Settings.redis?.project_history?.key_schema
  4. const rclient = require('@overleaf/redis-wrapper').createClient(
  5. Settings.redis.project_history
  6. )
  7. const logger = require('@overleaf/logger')
  8. const metrics = require('./Metrics')
  9. const { docIsTooLarge } = require('./Limits')
  10. const OError = require('@overleaf/o-error')
  11. const ProjectHistoryRedisManager = {
  12. async queueOps(projectId, ...ops) {
  13. // Record metric for ops pushed onto queue
  14. for (const op of ops) {
  15. metrics.summary('redis.projectHistoryOps', op.length, { status: 'push' })
  16. }
  17. const multi = rclient.multi()
  18. // Push the ops onto the project history queue
  19. multi.rpush(
  20. projectHistoryKeys.projectHistoryOps({ project_id: projectId }),
  21. ...ops
  22. )
  23. // To record the age of the oldest op on the queue set a timestamp if not
  24. // already present (SETNX).
  25. multi.setnx(
  26. projectHistoryKeys.projectHistoryFirstOpTimestamp({
  27. project_id: projectId,
  28. }),
  29. Date.now()
  30. )
  31. const result = await multi.exec()
  32. return result[0]
  33. },
  34. async queueRenameEntity(
  35. projectId,
  36. projectHistoryId,
  37. entityType,
  38. entityId,
  39. userId,
  40. projectUpdate,
  41. source
  42. ) {
  43. projectUpdate = {
  44. pathname: projectUpdate.pathname,
  45. new_pathname: projectUpdate.newPathname,
  46. meta: {
  47. user_id: userId,
  48. ts: new Date(),
  49. },
  50. version: projectUpdate.version,
  51. projectHistoryId,
  52. }
  53. projectUpdate[entityType] = entityId
  54. if (source != null) {
  55. projectUpdate.meta.source = source
  56. if (source !== 'editor') {
  57. projectUpdate.meta.type = 'external'
  58. }
  59. }
  60. logger.debug(
  61. { projectId, projectUpdate },
  62. 'queue rename operation to project-history'
  63. )
  64. const jsonUpdate = JSON.stringify(projectUpdate)
  65. return await ProjectHistoryRedisManager.queueOps(projectId, jsonUpdate)
  66. },
  67. async queueAddEntity(
  68. projectId,
  69. projectHistoryId,
  70. entityType,
  71. entityId,
  72. userId,
  73. projectUpdate,
  74. source
  75. ) {
  76. projectUpdate = {
  77. pathname: projectUpdate.pathname,
  78. docLines: projectUpdate.docLines,
  79. url: projectUpdate.url,
  80. meta: {
  81. user_id: userId,
  82. ts: new Date(),
  83. },
  84. version: projectUpdate.version,
  85. projectHistoryId,
  86. }
  87. projectUpdate[entityType] = entityId
  88. if (source != null) {
  89. projectUpdate.meta.source = source
  90. if (source !== 'editor') {
  91. projectUpdate.meta.type = 'external'
  92. }
  93. }
  94. logger.debug(
  95. { projectId, projectUpdate },
  96. 'queue add operation to project-history'
  97. )
  98. const jsonUpdate = JSON.stringify(projectUpdate)
  99. return await ProjectHistoryRedisManager.queueOps(projectId, jsonUpdate)
  100. },
  101. async queueResyncProjectStructure(projectId, projectHistoryId, docs, files) {
  102. logger.debug({ projectId, docs, files }, 'queue project structure resync')
  103. const projectUpdate = {
  104. resyncProjectStructure: { docs, files },
  105. projectHistoryId,
  106. meta: {
  107. ts: new Date(),
  108. },
  109. }
  110. const jsonUpdate = JSON.stringify(projectUpdate)
  111. return await ProjectHistoryRedisManager.queueOps(projectId, jsonUpdate)
  112. },
  113. async queueResyncDocContent(
  114. projectId,
  115. projectHistoryId,
  116. docId,
  117. lines,
  118. version,
  119. pathname
  120. ) {
  121. logger.debug(
  122. { projectId, docId, lines, version, pathname },
  123. 'queue doc content resync'
  124. )
  125. const projectUpdate = {
  126. resyncDocContent: {
  127. content: lines.join('\n'),
  128. version,
  129. },
  130. projectHistoryId,
  131. path: pathname,
  132. doc: docId,
  133. meta: {
  134. ts: new Date(),
  135. },
  136. }
  137. const jsonUpdate = JSON.stringify(projectUpdate)
  138. // Do an optimised size check on the docLines using the serialised
  139. // project update length as an upper bound
  140. const sizeBound = jsonUpdate.length
  141. if (docIsTooLarge(sizeBound, lines, Settings.max_doc_length)) {
  142. throw new OError(
  143. 'blocking resync doc content insert into project history queue: doc is too large',
  144. { projectId, docId, docSize: sizeBound }
  145. )
  146. }
  147. return await ProjectHistoryRedisManager.queueOps(projectId, jsonUpdate)
  148. },
  149. }
  150. module.exports = {
  151. ...callbackifyAll(ProjectHistoryRedisManager),
  152. promises: ProjectHistoryRedisManager,
  153. }