run.js 11 KB

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