UpdateManager.coffee 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170
  1. LockManager = require "./LockManager"
  2. RedisManager = require "./RedisManager"
  3. RealTimeRedisManager = require "./RealTimeRedisManager"
  4. ShareJsUpdateManager = require "./ShareJsUpdateManager"
  5. HistoryManager = require "./HistoryManager"
  6. Settings = require('settings-sharelatex')
  7. _ = require("lodash")
  8. async = require("async")
  9. logger = require('logger-sharelatex')
  10. Metrics = require "./Metrics"
  11. Errors = require "./Errors"
  12. DocumentManager = require "./DocumentManager"
  13. RangesManager = require "./RangesManager"
  14. SnapshotManager = require "./SnapshotManager"
  15. Profiler = require "./Profiler"
  16. module.exports = UpdateManager =
  17. processOutstandingUpdates: (project_id, doc_id, callback = (error) ->) ->
  18. timer = new Metrics.Timer("updateManager.processOutstandingUpdates")
  19. UpdateManager.fetchAndApplyUpdates project_id, doc_id, (error) ->
  20. timer.done()
  21. return callback(error) if error?
  22. callback()
  23. processOutstandingUpdatesWithLock: (project_id, doc_id, callback = (error) ->) ->
  24. profile = new Profiler("processOutstandingUpdatesWithLock", {project_id, doc_id})
  25. LockManager.tryLock doc_id, (error, gotLock, lockValue) =>
  26. return callback(error) if error?
  27. return callback() if !gotLock
  28. profile.log("tryLock")
  29. UpdateManager.processOutstandingUpdates project_id, doc_id, (error) ->
  30. return UpdateManager._handleErrorInsideLock(doc_id, lockValue, error, callback) if error?
  31. profile.log("processOutstandingUpdates")
  32. LockManager.releaseLock doc_id, lockValue, (error) =>
  33. return callback(error) if error?
  34. profile.log("releaseLock").end()
  35. UpdateManager.continueProcessingUpdatesWithLock project_id, doc_id, callback
  36. continueProcessingUpdatesWithLock: (project_id, doc_id, callback = (error) ->) ->
  37. RealTimeRedisManager.getUpdatesLength doc_id, (error, length) =>
  38. return callback(error) if error?
  39. if length > 0
  40. UpdateManager.processOutstandingUpdatesWithLock project_id, doc_id, callback
  41. else
  42. callback()
  43. fetchAndApplyUpdates: (project_id, doc_id, callback = (error) ->) ->
  44. profile = new Profiler("fetchAndApplyUpdates", {project_id, doc_id})
  45. RealTimeRedisManager.getPendingUpdatesForDoc doc_id, (error, updates) =>
  46. return callback(error) if error?
  47. logger.log {project_id: project_id, doc_id: doc_id, count: updates.length}, "processing updates"
  48. if updates.length == 0
  49. return callback()
  50. profile.log("getPendingUpdatesForDoc")
  51. doUpdate = (update, cb)->
  52. UpdateManager.applyUpdate project_id, doc_id, update, (err) ->
  53. profile.log("applyUpdate")
  54. cb(err)
  55. finalCallback = (err) ->
  56. profile.log("async done").end()
  57. callback(err)
  58. async.eachSeries updates, doUpdate, finalCallback
  59. applyUpdate: (project_id, doc_id, update, _callback = (error) ->) ->
  60. callback = (error) ->
  61. if error?
  62. RealTimeRedisManager.sendData {project_id, doc_id, error: error.message || error}
  63. profile.log("sendData")
  64. profile.end()
  65. _callback(error)
  66. profile = new Profiler("applyUpdate", {project_id, doc_id})
  67. UpdateManager._sanitizeUpdate update
  68. profile.log("sanitizeUpdate")
  69. DocumentManager.getDoc project_id, doc_id, (error, lines, version, ranges, pathname, projectHistoryId) ->
  70. profile.log("getDoc")
  71. return callback(error) if error?
  72. if !lines? or !version?
  73. return callback(new Errors.NotFoundError("document not found: #{doc_id}"))
  74. previousVersion = version
  75. ShareJsUpdateManager.applyUpdate project_id, doc_id, update, lines, version, (error, updatedDocLines, version, appliedOps) ->
  76. profile.log("sharejs.applyUpdate")
  77. return callback(error) if error?
  78. RangesManager.applyUpdate project_id, doc_id, ranges, appliedOps, updatedDocLines, (error, new_ranges, ranges_were_collapsed) ->
  79. UpdateManager._addProjectHistoryMetadataToOps(appliedOps, pathname, projectHistoryId, lines)
  80. profile.log("RangesManager.applyUpdate")
  81. return callback(error) if error?
  82. RedisManager.updateDocument project_id, doc_id, updatedDocLines, version, appliedOps, new_ranges, update.meta, (error, doc_ops_length, project_ops_length) ->
  83. profile.log("RedisManager.updateDocument")
  84. return callback(error) if error?
  85. HistoryManager.recordAndFlushHistoryOps project_id, doc_id, appliedOps, doc_ops_length, project_ops_length, (error) ->
  86. profile.log("recordAndFlushHistoryOps")
  87. return callback(error) if error?
  88. if ranges_were_collapsed
  89. logger.log {project_id, doc_id, previousVersion, lines, ranges, update}, "update collapsed some ranges, snapshotting previous content"
  90. # Do this last, since it's a mongo call, and so potentially longest running
  91. # If it overruns the lock, it's ok, since all of our redis work is done
  92. SnapshotManager.recordSnapshot project_id, doc_id, previousVersion, pathname, lines, ranges, (error) ->
  93. if error?
  94. logger.error {err: error, project_id, doc_id, version, lines, ranges}, "error recording snapshot"
  95. return callback(error)
  96. else
  97. callback()
  98. else
  99. callback()
  100. lockUpdatesAndDo: (method, project_id, doc_id, args..., callback) ->
  101. profile = new Profiler("lockUpdatesAndDo", {project_id, doc_id})
  102. LockManager.getLock doc_id, (error, lockValue) ->
  103. profile.log("getLock")
  104. return callback(error) if error?
  105. UpdateManager.processOutstandingUpdates project_id, doc_id, (error) ->
  106. return UpdateManager._handleErrorInsideLock(doc_id, lockValue, error, callback) if error?
  107. profile.log("processOutstandingUpdates")
  108. method project_id, doc_id, args..., (error, response_args...) ->
  109. return UpdateManager._handleErrorInsideLock(doc_id, lockValue, error, callback) if error?
  110. profile.log("method")
  111. LockManager.releaseLock doc_id, lockValue, (error) ->
  112. return callback(error) if error?
  113. profile.log("releaseLock").end()
  114. callback null, response_args...
  115. # We held the lock for a while so updates might have queued up
  116. UpdateManager.continueProcessingUpdatesWithLock project_id, doc_id
  117. _handleErrorInsideLock: (doc_id, lockValue, original_error, callback = (error) ->) ->
  118. LockManager.releaseLock doc_id, lockValue, (lock_error) ->
  119. callback(original_error)
  120. _sanitizeUpdate: (update) ->
  121. # In Javascript, characters are 16-bits wide. It does not understand surrogates as characters.
  122. #
  123. # From Wikipedia (http://en.wikipedia.org/wiki/Plane_(Unicode)#Basic_Multilingual_Plane):
  124. # "The High Surrogates (U+D800–U+DBFF) and Low Surrogate (U+DC00–U+DFFF) codes are reserved
  125. # for encoding non-BMP characters in UTF-16 by using a pair of 16-bit codes: one High Surrogate
  126. # and one Low Surrogate. A single surrogate code point will never be assigned a character.""
  127. #
  128. # The main offender seems to be \uD835 as a stand alone character, which would be the first
  129. # 16-bit character of a blackboard bold character (http://www.fileformat.info/info/unicode/char/1d400/index.htm).
  130. # Something must be going on client side that is screwing up the encoding and splitting the
  131. # two 16-bit characters so that \uD835 is standalone.
  132. for op in update.op or []
  133. if op.i?
  134. # Replace high and low surrogate characters with 'replacement character' (\uFFFD)
  135. op.i = op.i.replace(/[\uD800-\uDFFF]/g, "\uFFFD")
  136. return update
  137. _addProjectHistoryMetadataToOps: (updates, pathname, projectHistoryId, lines) ->
  138. doc_length = _.reduce lines,
  139. (chars, line) -> chars + line.length,
  140. 0
  141. doc_length += lines.length - 1 # count newline characters
  142. updates.forEach (update) ->
  143. update.projectHistoryId = projectHistoryId
  144. update.meta ||= {}
  145. update.meta.pathname = pathname
  146. update.meta.doc_length = doc_length
  147. # Each update may contain multiple ops, i.e.
  148. # [{
  149. # ops: [{i: "foo", p: 4}, {d: "bar", p:8}]
  150. # }, {
  151. # ops: [{d: "baz", p: 40}, {i: "qux", p:8}]
  152. # }]
  153. # We want to include the doc_length at the start of each update,
  154. # before it's ops are applied. However, we need to track any
  155. # changes to it for the next update.
  156. for op in update.op
  157. if op.i?
  158. doc_length += op.i.length
  159. if op.d?
  160. doc_length -= op.d.length