MongoPackManager.coffee 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250
  1. async = require "async"
  2. _ = require "underscore"
  3. module.exports = MongoPackManager =
  4. # The following functions implement methods like a mongo find, but
  5. # expands any documents containing a 'pack' field into multiple
  6. # values
  7. #
  8. # e.g. a single update looks like
  9. #
  10. # {
  11. # "doc_id" : 549dae9e0a2a615c0c7f0c98,
  12. # "project_id" : 549dae9c0a2a615c0c7f0c8c,
  13. # "op" : [ {"p" : 6981, "d" : "?" } ],
  14. # "meta" : { "user_id" : 52933..., "start_ts" : 1422310693931, "end_ts" : 1422310693931 },
  15. # "v" : 17082
  16. # }
  17. #
  18. # and a pack looks like this
  19. #
  20. # {
  21. # "doc_id" : 549dae9e0a2a615c0c7f0c98,
  22. # "project_id" : 549dae9c0a2a615c0c7f0c8c,
  23. # "pack" : [ D1, D2, D3, ....],
  24. # "meta" : { "user_id" : 52933..., "start_ts" : 1422310693931, "end_ts" : 1422310693931 },
  25. # "v" : 17082
  26. # }
  27. #
  28. # where D1, D2, D3, .... are single updates stripped of their
  29. # doc_id and project_id fields (which are the same for all the
  30. # updates in the pack). The meta and v fields of the pack itself
  31. # are those of the first entry in the pack D1 (this makes it
  32. # possible to treat packs and single updates in the same way).
  33. findDocResults: (collection, query, limit, callback) ->
  34. # query - the mongo query selector, includes both the doc_id/project_id and
  35. # the range on v
  36. # limit - the mongo limit, we need to apply it after unpacking any
  37. # packs
  38. sort = {}
  39. sort['v'] = -1;
  40. cursor = collection
  41. .find( query )
  42. .sort( sort )
  43. # if we have packs, we will trim the results more later after expanding them
  44. if limit?
  45. cursor.limit(limit)
  46. # take the part of the query which selects the range over the parameter
  47. rangeQuery = query['v']
  48. # helper function to check if an item from a pack is inside the
  49. # desired range
  50. filterFn = (item) ->
  51. return false if rangeQuery?['$gte']? && item['v'] < rangeQuery['$gte']
  52. return false if rangeQuery?['$lte']? && item['v'] > rangeQuery['$lte']
  53. return false if rangeQuery?['$lt']? && item['v'] >= rangeQuery['$lt']
  54. return false if rangeQuery?['$gt']? && item['v'] <= rangeQuery['$gt']
  55. return true
  56. # create a query which can be used to select the entries BEFORE
  57. # the range because we sometimes need to find extra ones (when the
  58. # boundary falls in the middle of a pack)
  59. extraQuery = _.clone(query)
  60. # The pack uses its first entry for its metadata and v, so the
  61. # only queries where we might not get all the packs are those for
  62. # $gt and $gte (i.e. we need to find packs which start before our
  63. # range but end in it)
  64. if rangeQuery?['$gte']?
  65. extraQuery['v'] = {'$lt' : rangeQuery['$gte']}
  66. else if rangeQuery?['$gt']
  67. extraQuery['v'] = {'$lte' : rangeQuery['$gt']}
  68. else
  69. delete extraQuery['v']
  70. needMore = false # keep track of whether we need to load more data
  71. updates = [] # used to accumulate the set of results
  72. cursor.toArray (err, result) ->
  73. unpackedSet = MongoPackManager._unpackResults(result)
  74. updates = MongoPackManager._filterAndLimit(updates, unpackedSet, filterFn, limit)
  75. # check if we need to retrieve more data, because there is a
  76. # pack that crosses into our range
  77. last = if unpackedSet.length then unpackedSet[unpackedSet.length-1] else null
  78. if limit? && updates.length == limit
  79. needMore = false
  80. else if extraQuery['v']? && last? && filterFn(last)
  81. needMore = true
  82. else if extraQuery['v']? && updates.length == 0
  83. needMore = true
  84. if needMore
  85. # we do need an extra result set
  86. extra = collection
  87. .find(extraQuery)
  88. .sort(sort)
  89. .limit(1)
  90. extra.toArray (err, result2) ->
  91. if err?
  92. return callback err, updates
  93. else
  94. extraSet = MongoPackManager._unpackResults(result2)
  95. updates = MongoPackManager._filterAndLimit(updates, extraSet, filterFn, limit)
  96. callback err, updates
  97. return
  98. if err?
  99. callback err, result
  100. else
  101. callback err, updates
  102. findProjectResults: (collection, query, limit, callback) ->
  103. # query - the mongo query selector, includes both the doc_id/project_id and
  104. # the range on v or meta.end_ts
  105. # limit - the mongo limit, we need to apply it after unpacking any
  106. # packs
  107. sort = {}
  108. sort['meta.end_ts'] = -1;
  109. projection = {"op":false, "pack.op": false}
  110. cursor = collection
  111. .find( query, projection ) # no need to return the op only need version info
  112. .sort( sort )
  113. # if we have packs, we will trim the results more later after expanding them
  114. if limit?
  115. cursor.limit(limit)
  116. # take the part of the query which selects the range over the parameter
  117. before = query['meta.end_ts']?['$lt'] # may be null
  118. updates = [] # used to accumulate the set of results
  119. cursor.toArray (err, result) ->
  120. if err?
  121. return callback err, result
  122. if result.length == 0 && not before? # no results and no time range specified
  123. return callback err, result
  124. unpackedSet = MongoPackManager._unpackResults(result)
  125. if limit?
  126. unpackedSet = unpackedSet.slice(0, limit)
  127. # find the end time of the last result, we will take all the
  128. # results up to this, and then all the changes at that time
  129. # (without imposing a limit) and any overlapping packs
  130. cutoff = if unpackedSet.length then unpackedSet[unpackedSet.length-1].meta.end_ts else null
  131. #console.log 'before is', before
  132. #console.log 'cutoff is', cutoff
  133. #console.log 'limit is', limit
  134. filterFn = (item) ->
  135. ts = item?.meta?.end_ts
  136. #console.log 'checking', ts, before, cutoff
  137. return false if before? && ts >= before
  138. return false if cutoff? && ts < cutoff
  139. return true
  140. updates = MongoPackManager._filterAndLimit(updates, unpackedSet, filterFn, limit)
  141. #console.log 'initial updates are', updates
  142. # get all elements on the lower bound (cutoff)
  143. tailQuery = _.clone(query)
  144. tailQuery['meta.end_ts'] = cutoff
  145. tail = collection
  146. .find(tailQuery, projection)
  147. .sort(sort)
  148. #console.log 'tailQuery is', tailQuery
  149. # now find any packs that overlap with the time window
  150. overlapQuery = _.clone(query)
  151. if before? && cutoff?
  152. overlapQuery['meta.end_ts'] = {"$gte": before}
  153. overlapQuery['pack.0.meta.end_ts'] = {"$lte": before }
  154. else if before? && not cutoff?
  155. overlapQuery['meta.end_ts'] = {"$gte": before}
  156. overlapQuery['pack.0.meta.end_ts'] = {"$lte": before }
  157. else if not before? && cutoff?
  158. overlapQuery['meta.end_ts'] = {"$gte": cutoff}
  159. overlapQuery['pack.0.meta.end_ts'] = {"$gte": 0 }
  160. else if not before? && not cutoff?
  161. overlapQuery['meta.end_ts'] = {"$gte": 0 }
  162. overlapQuery['pack.0.meta.end_ts'] = {"$gte": 0 }
  163. overlap = collection
  164. .find(overlapQuery, projection)
  165. .sort(sort)
  166. #console.log 'overlapQuery is', overlapQuery
  167. # we don't specify a limit here, as there could be any number of overlaps
  168. # NB. need to catch items in original query and followup query for duplicates
  169. applyAndUpdate = (result) ->
  170. extraSet = MongoPackManager._unpackResults(result)
  171. # note: final argument is null, no limit applied because we
  172. # need all the updates at the final time to avoid breaking
  173. # the changeset into parts
  174. updates = MongoPackManager._filterAndLimit(updates, extraSet, filterFn, null)
  175. #console.log 'extra updates after filterandlimit', updates
  176. # remove duplicates
  177. seen = {}
  178. updates = updates.filter (item) ->
  179. key = item.doc_id + ' ' + item.v
  180. #console.log 'key is', key
  181. if seen[key]
  182. return false
  183. else
  184. seen[key] = true
  185. return true
  186. #console.log 'extra updates are', updates
  187. tail.toArray (err, result2) ->
  188. if err?
  189. return callback err, updates
  190. else
  191. applyAndUpdate result2
  192. overlap.toArray (err, result3) ->
  193. if err?
  194. return callback err, updates
  195. else
  196. applyAndUpdate result3
  197. callback err, updates
  198. _unpackResults: (updates) ->
  199. # iterate over the updates, if there's a pack, expand it into ops and
  200. # insert it into the array at that point
  201. result = []
  202. updates.forEach (item) ->
  203. if item.pack?
  204. all = MongoPackManager._explodePackToOps item
  205. result = result.concat all
  206. else
  207. result.push item
  208. return result
  209. _explodePackToOps: (packObj) ->
  210. # convert a pack into an array of ops
  211. doc_id = packObj.doc_id
  212. project_id = packObj.project_id
  213. result = packObj.pack.map (item) ->
  214. item.doc_id = doc_id
  215. item.project_id = project_id
  216. item
  217. return result.reverse()
  218. _filterAndLimit: (results, extra, filterFn, limit) ->
  219. # update results with extra docs, after filtering and limiting
  220. filtered = extra.filter(filterFn)
  221. newResults = results.concat filtered
  222. newResults.slice(0, limit) if limit?
  223. return newResults