qdone 2.0.13-alpha → 2.0.14-alpha

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -20,6 +20,7 @@ export class JobExecutor {
20
20
  constructor (opt) {
21
21
  this.opt = opt
22
22
  this.jobs = []
23
+ this.jobsByMessageId = {}
23
24
  this.stats = {
24
25
  activeJobs: 0,
25
26
  sqsCalls: 0,
@@ -28,15 +29,16 @@ export class JobExecutor {
28
29
  jobsFailed: 0,
29
30
  jobsDeleted: 0
30
31
  }
31
- this.maintainVisibility()
32
+ this.maintainPromise = this.maintainVisibility()
32
33
  debug({ this: this })
33
34
  }
34
35
 
35
- shutdown () {
36
+ async shutdown () {
36
37
  this.shutdownRequested = true
37
38
  // Trigger a maintenance run right away in case it speeds us up
38
39
  clearTimeout(this.maintainVisibilityTimeout)
39
- this.maintainVisibility()
40
+ await this.maintainPromise
41
+ await this.maintainVisibility()
40
42
  }
41
43
 
42
44
  activeJobCount () {
@@ -47,8 +49,9 @@ export class JobExecutor {
47
49
  * Changes message visibility on all running jobs using as few calls as possible.
48
50
  */
49
51
  async maintainVisibility () {
50
- debug('maintainVisibility', this.jobs)
51
- const now = new Date()
52
+ clearTimeout(this.maintainVisibilityTimeout)
53
+ // debug('maintainVisibility', this.jobs)
54
+ const start = new Date()
52
55
  const jobsToExtendByQrl = {}
53
56
  const jobsToDeleteByQrl = {}
54
57
  const jobsToCleanup = new Set()
@@ -59,29 +62,36 @@ export class JobExecutor {
59
62
  }
60
63
 
61
64
  // Build list of jobs we need to deal with
62
- for (const job of this.jobs) {
63
- const jobRunTime = (now - job.start) / 1000
65
+ for (let i = 0; i < this.jobs.length; i++) {
66
+ const job = this.jobs[i]
67
+ const jobRunTime = Math.round((start - job.start) / 1000)
68
+ // debug('considering job', job)
64
69
  if (job.status === 'complete') {
65
70
  const jobsToDelete = jobsToDeleteByQrl[job.qrl] || []
71
+ job.status = 'deleting'
66
72
  jobsToDelete.push(job)
67
73
  jobsToDeleteByQrl[job.qrl] = jobsToDelete
68
74
  } else if (job.status === 'failed') {
69
75
  jobsToCleanup.add(job)
70
- } else if (jobRunTime >= job.exendAtSecond) {
71
- // Add it to our organized list of jobs
72
- const jobsToExtend = jobsToExtendByQrl[job.qrl] || []
73
- jobsToExtend.push(job)
74
- jobsToExtendByQrl[job.qrl] = jobsToExtend
75
-
76
- // Update the visibility timeout, double every time, up to max
77
- const doubled = job.visibilityTimeout * 2
78
- const secondsUntilMax = maxJobSeconds - jobRunTime
79
- const secondsUntilKill = this.opt.killAfter - jobRunTime
80
- job.visibilityTimeout = Math.min(double, secondsUntilMax, secondsUntilKill)
81
- job.extendAtSecond = jobRunTime + job.visibilityTimeout // this is what we use next time
76
+ } else if (job.status === 'processing') {
77
+ debug('processing', { job, jobRunTime })
78
+ if (jobRunTime >= job.extendAtSecond) {
79
+ // Add it to our organized list of jobs
80
+ const jobsToExtend = jobsToExtendByQrl[job.qrl] || []
81
+ jobsToExtend.push(job)
82
+ jobsToExtendByQrl[job.qrl] = jobsToExtend
83
+
84
+ // Update the visibility timeout, double every time, up to max
85
+ const doubled = job.visibilityTimeout * 2
86
+ const secondsUntilMax = Math.max(1, maxJobSeconds - jobRunTime)
87
+ // const secondsUntilKill = Math.max(1, this.opt.killAfter - jobRunTime)
88
+ job.visibilityTimeout = Math.min(doubled, secondsUntilMax) //, secondsUntilKill)
89
+ job.extendAtSecond = Math.round(jobRunTime + job.visibilityTimeout) // this is what we use next time
90
+ debug({ doubled, secondsUntilMax, job })
91
+ }
82
92
  }
83
93
  }
84
- debug('maintainVisibility', { jobsToDeleteByQrl, jobsToExtendByQrl })
94
+ // debug('maintainVisibility', { jobsToDeleteByQrl, jobsToExtendByQrl })
85
95
 
86
96
  // Extend in batches for each queue
87
97
  for (const qrl in jobsToExtendByQrl) {
@@ -100,6 +110,7 @@ export class JobExecutor {
100
110
  }
101
111
  entries.push(entry)
102
112
  }
113
+ debug({ entries })
103
114
 
104
115
  // Change batch
105
116
  const input = { QueueUrl: qrl, Entries: entries }
@@ -113,7 +124,7 @@ export class JobExecutor {
113
124
  if (this.opt.verbose) {
114
125
  console.error(chalk.blue('Extended'), count, chalk.blue('jobs'))
115
126
  } else if (!this.opt.disableLog) {
116
- console.log(JSON.stringify({ event: 'EXTEND_VISIBILITY_TIMEOUTS', timestamp: now, count, qrl }))
127
+ console.log(JSON.stringify({ event: 'EXTEND_VISIBILITY_TIMEOUTS', timestamp: start, count, qrl }))
117
128
  }
118
129
  }
119
130
  // TODO Sentry
@@ -136,6 +147,7 @@ export class JobExecutor {
136
147
  }
137
148
  entries.push(entry)
138
149
  }
150
+ debug({ entries })
139
151
 
140
152
  // Delete batch
141
153
  const input = { QueueUrl: qrl, Entries: entries }
@@ -148,7 +160,7 @@ export class JobExecutor {
148
160
  if (this.opt.verbose) {
149
161
  console.error(chalk.blue('Deleted'), count, chalk.blue('jobs'))
150
162
  } else if (!this.opt.disableLog) {
151
- console.log(JSON.stringify({ event: 'DELETE_MESSAGES', timestamp: now, count, qrl }))
163
+ console.log(JSON.stringify({ event: 'DELETE_MESSAGES', timestamp: start, count, qrl }))
152
164
  }
153
165
  }
154
166
  debug('DeleteMessageBatch returned', result)
@@ -157,22 +169,40 @@ export class JobExecutor {
157
169
  }
158
170
 
159
171
  // Get rid of deleted and failed jobs
160
- this.jobs = this.jobs.filter(j => j.status === 'processing')
172
+ this.jobs = this.jobs.filter(job => {
173
+ if (job.status === 'deleting' || job.status === 'failed') {
174
+ debug('removed', job.message.MessageId)
175
+ delete this.jobsByMessageId[job.message.MessageId]
176
+ return false
177
+ } else {
178
+ return true
179
+ }
180
+ })
161
181
 
162
- // Check again later, unless we are shutting down and nothing's left
182
+ // Bail if we are shutting down
163
183
  if (this.shutdownRequested && this.stats.activeJobs === 0 && this.jobs.length === 0) return
164
- const nextCheckInMs = this.shutdownRequested ? 1 * 1000 : 10 * 1000
165
- this.maintainVisibilityTimeout = setTimeout(() => this.maintainVisibility(), nextCheckInMs)
184
+
185
+ // Check later, but count the time we spent. Make sure we check at least
186
+ // every period seconds.
187
+ const msElapsed = new Date() - start
188
+ const msPeriod = this.shutdownRequested ? 1 * 1000 : 10 * 1000
189
+ const msLeft = Math.max(0, msPeriod - msElapsed)
190
+ const msMin = this.shutdownRequested ? 1000 : 0
191
+ const nextCheckInMs = Math.max(msMin, msLeft)
192
+ debug({ msElapsed, msPeriod, msLeft, msMin, nextCheckInMs })
193
+ this.maintainVisibilityTimeout = setTimeout(() => {
194
+ this.maintainPromise = this.maintainVisibility()
195
+ }, nextCheckInMs)
166
196
  }
167
197
 
168
198
  async executeJob (message, callback, qname, qrl) {
169
199
  // Create job entry and track it
170
200
  const payload = this.opt.json ? JSON.parse(message.Body) : message.Body
171
- const visibilityTimeout = 30
201
+ const visibilityTimeout = 60
172
202
  const job = {
173
203
  status: 'processing',
174
204
  start: new Date(),
175
- visibilityTimeout: 30,
205
+ visibilityTimeout,
176
206
  extendAtSecond: visibilityTimeout / 2,
177
207
  payload: this.opt.json ? JSON.parse(message.Body) : message.Body,
178
208
  message,
@@ -180,8 +210,23 @@ export class JobExecutor {
180
210
  qname,
181
211
  qrl
182
212
  }
183
- debug('executeJob', job)
213
+
214
+ // See if we are already executing this job
215
+ const oldJob = this.jobsByMessageId[job.message.MessageId]
216
+ if (oldJob) {
217
+ // If we actually see the same job again, we fucked up, probably due to
218
+ // the system being overloaded and us missing our extension call. So
219
+ // we'll celebrate this occasion by throwing a big fat error.
220
+ debug({ oldJob })
221
+ const e = new Error(`Saw job ${oldJob.message.MessageId} twice`)
222
+ e.job = oldJob
223
+ // TODO: sentry breadcrumb
224
+ throw e
225
+ }
226
+
227
+ // debug('executeJob', job)
184
228
  this.jobs.push(job)
229
+ this.jobsByMessageId[job.message.MessageId] = job
185
230
  this.stats.activeJobs++
186
231
  if (this.opt.verbose) {
187
232
  console.error(chalk.blue('Executing:'), qname, chalk.blue('-->'), job.payload)
@@ -218,7 +263,7 @@ export class JobExecutor {
218
263
  }
219
264
  this.stats.jobsSucceeded++
220
265
  } catch (err) {
221
- debug('exec.catch')
266
+ // debug('exec.catch', err)
222
267
  // Fail path for job execution
223
268
  if (this.opt.verbose) {
224
269
  console.error(chalk.red('FAILED'), message.Body)
@@ -7,7 +7,6 @@ import Debug from 'debug'
7
7
 
8
8
  import { normalizeQueueName, getQnameUrlPairs } from '../qrlCache.js'
9
9
  import { cheapIdleCheck } from '../idleQueues.js'
10
- import { getSQSClient } from '../sqs.js'
11
10
 
12
11
  const debug = Debug('qdone:queueManager')
13
12
 
@@ -17,57 +16,120 @@ export class QueueManager {
17
16
  this.queues = queues
18
17
  this.resolveSeconds = resolveSeconds
19
18
  this.selectedPairs = []
19
+ this.icehouse = {}
20
20
  this.resolveTimeout = undefined
21
21
  this.shutdownRequested = false
22
- this.resolveQueues()
22
+ this.resolvePromise = this.resolveQueues()
23
+ }
24
+
25
+ // Sends a queue to the icehouse, where it waits for a while before being
26
+ // checked again
27
+ updateIcehouse (qrl, emptyReceive) {
28
+ const foundEntry = !!this.icehouse[qrl]
29
+ const { lastCheck, secondsToWait, numEmptyReceives } = this.icehouse[qrl] || {
30
+ lastCheck: new Date(),
31
+ secondsToWait: Math.round(20 + 10 * Math.random()),
32
+ numEmptyReceives: 0 + emptyReceive
33
+ }
34
+ if (emptyReceive) {
35
+ const now = new Date()
36
+ const secondsElapsed = lastCheck - now
37
+ const minWait = 10
38
+ const maxWait = 600
39
+ const baseSeconds = numEmptyReceives ** 2 * 20
40
+ const jitterSeconds = Math.round((Math.random() - 0.5) * baseSeconds)
41
+ const newSecondsToWait = Math.max(minWait, Math.min(maxWait, baseSeconds + jitterSeconds))
42
+ const newEntry = { lastCheck: now, secondsToWait: newSecondsToWait, numEmptyReceives: numEmptyReceives + 1 }
43
+ this.icehouse[qrl] = newEntry
44
+ if (this.opt.verbose) {
45
+ console.error(chalk.blue('Sending queue to icehouse'), qrl, chalk.blue('for'), newSecondsToWait, chalk.blue('seconds'))
46
+ }
47
+ debug({ foundEntry, newEntry, lastCheck, secondsToWait, now, secondsElapsed, maxWait, minWait, baseSeconds, jitterSeconds })
48
+ } else {
49
+ delete this.icehouse[qrl]
50
+ }
51
+ }
52
+
53
+ // Returns true if the queue should be kept in the icehouse
54
+ keepInIcehouse (qrl, now) {
55
+ if (this.icehouse[qrl]) {
56
+ const { lastCheck, secondsToWait } = this.icehouse[qrl]
57
+ const secondsElapsed = Math.round((now - lastCheck) / 1000)
58
+ debug({ icehouseCheck: { qrl, lastCheck, secondsToWait, secondsElapsed } })
59
+ const letOut = secondsElapsed > secondsToWait
60
+ if (letOut && this.opt.verbose) {
61
+ console.error(chalk.blue('Coming out of icehouse:'), qrl)
62
+ }
63
+ return !letOut
64
+ } else {
65
+ return false
66
+ }
23
67
  }
24
68
 
25
69
  async resolveQueues () {
70
+ clearTimeout(this.resolveTimeout)
26
71
  if (this.shutdownRequested) return
27
72
 
28
73
  // Start processing
29
- if (this.opt.verbose) console.error(chalk.blue('Resolving queues: ') + this.queues.join(' '))
30
74
  const qnames = this.queues.map(queue => normalizeQueueName(queue, this.opt))
31
75
  const pairs = await getQnameUrlPairs(qnames, this.opt)
76
+ if (this.opt.verbose) console.error(chalk.blue('Resolving queues:'))
32
77
 
33
78
  if (this.shutdownRequested) return
34
79
 
80
+ // Filter out queues
81
+ const now = new Date()
82
+ const filteredPairs = pairs
83
+ // first failed
84
+ .filter(({ qname, qrl }) => {
85
+ const suf = this.opt.failSuffix + (this.opt.fifo ? '.fifo' : '')
86
+ const isFailQueue = qname.slice(-suf.length) === suf
87
+ return this.opt.includeFailed ? true : !isFailQueue
88
+ })
89
+ // first fifo
90
+ .filter(({ qname, qrl }) => {
91
+ const isFifo = qname.endsWith('.fifo')
92
+ return this.opt.fifo ? isFifo : !isFifo
93
+ })
94
+ // then icehouse
95
+ .filter(({ qname, qrl }) => !this.keepInIcehouse(qrl, now))
96
+
35
97
  // Figure out which pairs are active
36
98
  const activePairs = []
37
99
  if (this.opt.activeOnly) {
38
- debug({ pairsBeforeCheck: pairs })
39
- await Promise.all(pairs.map(async pair => {
40
- const { idle } = await cheapIdleCheck(pair.qname, pair.qrl, this.opt)
41
- if (!idle) activePairs.push(pair)
100
+ if (this.opt.verbose) {
101
+ console.error(chalk.blue(' checking active only'))
102
+ }
103
+ await Promise.all(filteredPairs.map(async ({ qname, qrl }) => {
104
+ const { result: { idle } } = await cheapIdleCheck(qname, qrl, this.opt)
105
+ debug({ idle, qname })
106
+ if (!idle) activePairs.push({ qname, qrl })
42
107
  }))
43
108
  }
44
109
 
45
110
  if (this.shutdownRequested) return
46
111
 
47
- // Finished resolving
48
- if (this.opt.verbose) {
49
- console.error(chalk.blue(' done'))
50
- console.error()
51
- }
52
-
53
112
  // Figure out which queues we want to listen on, choosing between active and
54
113
  // all, filtering out failed queues if the user wants that
55
- this.selectedPairs = (this.opt.activeOnly ? activePairs : pairs)
56
- .filter(({ qname }) => {
57
- const suf = this.opt.failSuffix + (this.opt.fifo ? '.fifo' : '')
58
- const isFailQueue = qname.slice(-suf.length) === suf
59
- const shouldInclude = this.opt.includeFailed ? true : !isFailQueue
60
- return shouldInclude
61
- })
114
+ this.selectedPairs = (this.opt.activeOnly ? activePairs : filteredPairs)
62
115
 
63
116
  // Randomize order
64
117
  this.selectedPairs.sort(() => 0.5 - Math.random())
118
+ if (this.opt.verbose) console.error(chalk.blue(' selected:\n ') + this.selectedPairs.map(({ qname }) => qname).join('\n '))
65
119
  debug('selectedPairs', this.selectedPairs)
66
120
 
121
+ // Finished resolving
122
+ if (this.opt.verbose) {
123
+ console.error(chalk.blue(' done'))
124
+ console.error()
125
+ }
126
+
67
127
  if (this.opt.verbose) {
68
128
  console.error(chalk.blue('Will resolve queues again in ' + this.resolveSeconds + ' seconds'))
69
129
  }
70
- this.resolveTimeout = setTimeout(() => this.resolveQueues(), this.resolveSeconds * 1000)
130
+ this.resolveTimeout = setTimeout(() => {
131
+ this.resolvePromise = this.resolveQueues()
132
+ }, this.resolveSeconds * 1000)
71
133
  }
72
134
 
73
135
  // Return the next queue in the lineup
@@ -81,8 +143,9 @@ export class QueueManager {
81
143
  return this.selectedPairs
82
144
  }
83
145
 
84
- shutdown () {
85
- clearTimeout(this.resolveTimeout)
146
+ async shutdown () {
86
147
  this.shutdownRequested = true
148
+ clearTimeout(this.resolveTimeout)
149
+ await this.resolvePromise
87
150
  }
88
151
  }
@@ -4,55 +4,41 @@
4
4
  */
5
5
 
6
6
  export class SystemMonitor {
7
- constructor (opt, smoothingFactor = 0.5, reportSeconds = 5) {
8
- this.opt = opt
9
- this.smoothingFactor = smoothingFactor
7
+ constructor (reportCallback, reportSeconds = 1) {
8
+ this.reportCallback = reportCallback || console.log
10
9
  this.reportSeconds = reportSeconds
11
- this.measurements = {
12
- setTimeout: [],
13
- setImmediate: []
14
- }
15
- this.timeouts = {
16
- setTimeout: undefined,
17
- setImmediate: undefined,
18
- reportLatency: undefined
19
- }
20
- this.measureLatencySetTimeout()
10
+ this.measurements = []
11
+ this.measure()
21
12
  this.reportLatency()
22
13
  }
23
14
 
24
- measureLatencySetTimeout () {
15
+ measure () {
16
+ clearTimeout(this.measureTimeout)
25
17
  const start = new Date()
26
- this.timeouts.setTimeout = setTimeout(() => {
18
+ this.measureTimeout = setTimeout(() => {
27
19
  const latency = new Date() - start
28
- this.measurements.setTimeout.push(latency)
29
- if (this.measurements.setTimeout.length > 1000) this.measurements.setTimeout.shift()
30
- this.measureLatencySetTimeout()
20
+ this.measurements.push(latency)
21
+ if (this.measurements.length > 1000) this.measurements.shift()
22
+ this.measure()
31
23
  })
32
24
  }
33
25
 
34
26
  getLatency () {
35
- const results = {}
36
- for (const k in this.measurements) {
37
- const values = this.measurements[k]
38
- results[k] = values.length ? values.reduce((a, b) => a + b, 0) / values.length : 0
39
- }
40
- return results
27
+ return this.measurements.length ? this.measurements.reduce((a, b) => a + b, 0) / this.measurements.length : 0
41
28
  }
42
29
 
43
30
  reportLatency () {
44
- this.timeouts.reportLatency = setTimeout(() => {
45
- for (const k in this.measurements) {
46
- const values = this.measurements[k]
47
- const mean = values.length ? values.reduce((a, b) => a + b, 0) / values.length : 0
48
- console.log({ [k]: mean })
49
- }
31
+ clearTimeout(this.reportTimeout)
32
+ this.reportTimeout = setTimeout(() => {
33
+ const latency = this.getLatency()
34
+ // console.log({ latency })
35
+ if (this.reportCallback) this.reportCallback(latency)
50
36
  this.reportLatency()
51
37
  }, this.reportSeconds * 1000)
52
38
  }
53
39
 
54
40
  shutdown () {
55
- console.log(this.measurements)
56
- for (const k in this.timeouts) clearTimeout(this.timeouts[k])
41
+ clearTimeout(this.measureTimeout)
42
+ clearTimeout(this.reportTimeout)
57
43
  }
58
44
  }