ProjectHistoryRedisManager.js 4.6 KB

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