run.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385
  1. /* eslint-disable
  2. no-return-assign,
  3. no-unused-vars,
  4. */
  5. // TODO: This file was created by bulk-decaffeinate.
  6. // Fix any style issues and re-enable lint.
  7. /*
  8. * decaffeinate suggestions:
  9. * DS101: Remove unnecessary use of Array.from
  10. * DS102: Remove unnecessary code created because of implicit returns
  11. * DS202: Simplify dynamic range loops
  12. * DS205: Consider reworking code to avoid use of IIFEs
  13. * DS207: Consider shorter variations of null checks
  14. * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
  15. */
  16. const DocUpdaterClient = require('../../acceptance/js/helpers/DocUpdaterClient')
  17. // MockTrackChangesApi = require "../../acceptance/js/helpers/MockTrackChangesApi"
  18. // MockWebApi = require "../../acceptance/js/helpers/MockWebApi"
  19. const assert = require('assert')
  20. const async = require('async')
  21. const insert = function (string, pos, content) {
  22. const result = string.slice(0, pos) + content + string.slice(pos)
  23. return result
  24. }
  25. const transform = function (op1, op2) {
  26. if (op2.p < op1.p) {
  27. return {
  28. p: op1.p + op2.i.length,
  29. i: op1.i,
  30. }
  31. } else {
  32. return op1
  33. }
  34. }
  35. class StressTestClient {
  36. constructor(options) {
  37. if (options == null) {
  38. options = {}
  39. }
  40. this.options = options
  41. if (this.options.updateDelay == null) {
  42. this.options.updateDelay = 200
  43. }
  44. this.project_id = this.options.project_id || DocUpdaterClient.randomId()
  45. this.doc_id = this.options.doc_id || DocUpdaterClient.randomId()
  46. this.pos = this.options.pos || 0
  47. this.content = this.options.content || ''
  48. this.client_id = DocUpdaterClient.randomId()
  49. this.version = this.options.version || 0
  50. this.inflight_op = null
  51. this.charCode = 0
  52. this.counts = {
  53. conflicts: 0,
  54. local_updates: 0,
  55. remote_updates: 0,
  56. max_delay: 0,
  57. }
  58. DocUpdaterClient.subscribeToAppliedOps((channel, update) => {
  59. update = JSON.parse(update)
  60. if (update.error != null) {
  61. console.error(new Error(`Error from server: '${update.error}'`))
  62. return
  63. }
  64. if (update.doc_id === this.doc_id) {
  65. return this.processReply(update)
  66. }
  67. })
  68. }
  69. sendUpdate() {
  70. const data = String.fromCharCode(65 + (this.charCode++ % 26))
  71. this.content = insert(this.content, this.pos, data)
  72. this.inflight_op = {
  73. i: data,
  74. p: this.pos++,
  75. }
  76. this.resendUpdate()
  77. return (this.inflight_op_sent = Date.now())
  78. }
  79. resendUpdate() {
  80. assert(this.inflight_op != null)
  81. DocUpdaterClient.sendUpdate(this.project_id, this.doc_id, {
  82. doc: this.doc_id,
  83. op: [this.inflight_op],
  84. v: this.version,
  85. meta: {
  86. source: this.client_id,
  87. },
  88. dupIfSource: [this.client_id],
  89. })
  90. return (this.update_timer = setTimeout(() => {
  91. console.log(
  92. `[${new Date()}] \t[${this.client_id.slice(
  93. 0,
  94. 4
  95. )}] WARN: Resending update after 5 seconds`
  96. )
  97. return this.resendUpdate()
  98. }, 5000))
  99. }
  100. processReply(update) {
  101. if (update.op.v !== this.version) {
  102. if (update.op.v < this.version) {
  103. console.log(
  104. `[${new Date()}] \t[${this.client_id.slice(
  105. 0,
  106. 4
  107. )}] WARN: Duplicate ack (already seen version)`
  108. )
  109. return
  110. } else {
  111. console.error(
  112. `[${new Date()}] \t[${this.client_id.slice(
  113. 0,
  114. 4
  115. )}] ERROR: Version jumped ahead (client: ${this.version}, op: ${
  116. update.op.v
  117. })`
  118. )
  119. }
  120. }
  121. this.version++
  122. if (update.op.meta.source === this.client_id) {
  123. if (this.inflight_op != null) {
  124. this.counts.local_updates++
  125. this.inflight_op = null
  126. clearTimeout(this.update_timer)
  127. const delay = Date.now() - this.inflight_op_sent
  128. this.counts.max_delay = Math.max(this.counts.max_delay, delay)
  129. return this.continue()
  130. } else {
  131. return console.log(
  132. `[${new Date()}] \t[${this.client_id.slice(
  133. 0,
  134. 4
  135. )}] WARN: Duplicate ack`
  136. )
  137. }
  138. } else {
  139. assert(update.op.op.length === 1)
  140. this.counts.remote_updates++
  141. let externalOp = update.op.op[0]
  142. if (this.inflight_op != null) {
  143. this.counts.conflicts++
  144. this.inflight_op = transform(this.inflight_op, externalOp)
  145. externalOp = transform(externalOp, this.inflight_op)
  146. }
  147. if (externalOp.p < this.pos) {
  148. this.pos += externalOp.i.length
  149. }
  150. return (this.content = insert(this.content, externalOp.p, externalOp.i))
  151. }
  152. }
  153. continue() {
  154. if (this.updateCount > 0) {
  155. this.updateCount--
  156. return setTimeout(() => {
  157. return this.sendUpdate()
  158. }, this.options.updateDelay * (0.5 + Math.random()))
  159. } else {
  160. return this.updateCallback()
  161. }
  162. }
  163. runForNUpdates(n, callback) {
  164. if (callback == null) {
  165. callback = function () {}
  166. }
  167. this.updateCallback = callback
  168. this.updateCount = n
  169. return this.continue()
  170. }
  171. check(callback) {
  172. if (callback == null) {
  173. callback = function () {}
  174. }
  175. return DocUpdaterClient.getDoc(
  176. this.project_id,
  177. this.doc_id,
  178. (error, res, body) => {
  179. if (error != null) {
  180. throw error
  181. }
  182. if (body.lines == null) {
  183. return console.error(
  184. `[${new Date()}] \t[${this.client_id.slice(
  185. 0,
  186. 4
  187. )}] ERROR: Invalid response from get doc (${this.doc_id})`,
  188. body
  189. )
  190. }
  191. const content = body.lines.join('\n')
  192. const { version } = body
  193. if (content !== this.content) {
  194. if (version === this.version) {
  195. console.error(
  196. `[${new Date()}] \t[${this.client_id.slice(
  197. 0,
  198. 4
  199. )}] Error: Client content does not match server.`
  200. )
  201. console.error(`Server: ${content.split('a')}`)
  202. console.error(`Client: ${this.content.split('a')}`)
  203. } else {
  204. console.error(
  205. `[${new Date()}] \t[${this.client_id.slice(
  206. 0,
  207. 4
  208. )}] Error: Version mismatch (Server: '${version}', Client: '${
  209. this.version
  210. }')`
  211. )
  212. }
  213. }
  214. if (!this.isContentValid(this.content)) {
  215. const iterable = this.content.split('')
  216. for (let i = 0; i < iterable.length; i++) {
  217. const chunk = iterable[i]
  218. if (chunk != null && chunk !== 'a') {
  219. console.log(chunk, i)
  220. }
  221. }
  222. throw new Error('bad content')
  223. }
  224. return callback()
  225. }
  226. )
  227. }
  228. isChunkValid(chunk) {
  229. const char = 0
  230. for (let i = 0; i < chunk.length; i++) {
  231. const letter = chunk[i]
  232. if (letter.charCodeAt(0) !== 65 + (i % 26)) {
  233. console.error(
  234. `[${new Date()}] \t[${this.client_id.slice(0, 4)}] Invalid Chunk:`,
  235. chunk
  236. )
  237. return false
  238. }
  239. }
  240. return true
  241. }
  242. isContentValid(content) {
  243. for (const chunk of Array.from(content.split('a'))) {
  244. if (chunk != null && chunk !== '') {
  245. if (!this.isChunkValid(chunk)) {
  246. console.error(
  247. `[${new Date()}] \t[${this.client_id.slice(0, 4)}] Invalid content`,
  248. content
  249. )
  250. return false
  251. }
  252. }
  253. }
  254. return true
  255. }
  256. }
  257. const checkDocument = function (projectId, docId, clients, callback) {
  258. if (callback == null) {
  259. callback = function () {}
  260. }
  261. const jobs = clients.map(client => cb => client.check(cb))
  262. return async.parallel(jobs, callback)
  263. }
  264. const printSummary = function (docId, clients) {
  265. const slot = require('cluster-key-slot')
  266. const now = new Date()
  267. console.log(
  268. `[${now}] [${docId.slice(0, 4)} (slot: ${slot(docId)})] ${
  269. clients.length
  270. } clients...`
  271. )
  272. return (() => {
  273. const result = []
  274. for (const client of Array.from(clients)) {
  275. console.log(
  276. `[${now}] \t[${client.client_id.slice(0, 4)}] { local: ${
  277. client.counts.local_updates
  278. }, remote: ${client.counts.remote_updates}, conflicts: ${
  279. client.counts.conflicts
  280. }, max_delay: ${client.counts.max_delay} }`
  281. )
  282. result.push(
  283. (client.counts = {
  284. local_updates: 0,
  285. remote_updates: 0,
  286. conflicts: 0,
  287. max_delay: 0,
  288. })
  289. )
  290. }
  291. return result
  292. })()
  293. }
  294. const CLIENT_COUNT = parseInt(process.argv[2], 10)
  295. const UPDATE_DELAY = parseInt(process.argv[3], 10)
  296. const SAMPLE_INTERVAL = parseInt(process.argv[4], 10)
  297. for (const docAndProjectId of Array.from(process.argv.slice(5))) {
  298. ;(function (docAndProjectId) {
  299. const [projectId, docId] = Array.from(docAndProjectId.split(':'))
  300. console.log({ projectId, docId })
  301. return DocUpdaterClient.setDocLines(
  302. projectId,
  303. docId,
  304. [new Array(CLIENT_COUNT + 2).join('a')],
  305. null,
  306. null,
  307. error => {
  308. if (error != null) {
  309. throw error
  310. }
  311. return DocUpdaterClient.getDoc(projectId, docId, (error, res, body) => {
  312. let runBatch
  313. if (error != null) {
  314. throw error
  315. }
  316. if (body.lines == null) {
  317. return console.error(
  318. `[${new Date()}] ERROR: Invalid response from get doc (${docId})`,
  319. body
  320. )
  321. }
  322. const content = body.lines.join('\n')
  323. const { version } = body
  324. const clients = []
  325. for (
  326. let pos = 1, end = CLIENT_COUNT, asc = end >= 1;
  327. asc ? pos <= end : pos >= end;
  328. asc ? pos++ : pos--
  329. ) {
  330. ;(function (pos) {
  331. const client = new StressTestClient({
  332. doc_id: docId,
  333. project_id: projectId,
  334. content,
  335. pos,
  336. version,
  337. updateDelay: UPDATE_DELAY,
  338. })
  339. return clients.push(client)
  340. })(pos)
  341. }
  342. return (runBatch = function () {
  343. const jobs = clients.map(
  344. client => cb =>
  345. client.runForNUpdates(SAMPLE_INTERVAL / UPDATE_DELAY, cb)
  346. )
  347. return async.parallel(jobs, error => {
  348. if (error != null) {
  349. throw error
  350. }
  351. printSummary(docId, clients)
  352. return checkDocument(projectId, docId, clients, error => {
  353. if (error != null) {
  354. throw error
  355. }
  356. return runBatch()
  357. })
  358. })
  359. })()
  360. })
  361. }
  362. )
  363. })(docAndProjectId)
  364. }