qdone 2.0.12-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
- if (this.stats.activeJobs === 0 && this.jobs.length === 0) {
38
- clearTimeout(this.maintainVisibilityTimeout)
39
- }
38
+ // Trigger a maintenance run right away in case it speeds us up
39
+ clearTimeout(this.maintainVisibilityTimeout)
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 }
@@ -107,18 +118,17 @@ export class JobExecutor {
107
118
  const result = await getSQSClient().send(new ChangeMessageVisibilityBatchCommand(input))
108
119
  debug('ChangeMessageVisibilityBatch returned', result)
109
120
  this.stats.sqsCalls++
110
- if (result.Successful) this.stats.timeoutsExtended += result.Successful.length || 0
121
+ if (result.Successful) {
122
+ const count = result.Successful.length || 0
123
+ this.stats.timeoutsExtended += count
124
+ if (this.opt.verbose) {
125
+ console.error(chalk.blue('Extended'), count, chalk.blue('jobs'))
126
+ } else if (!this.opt.disableLog) {
127
+ console.log(JSON.stringify({ event: 'EXTEND_VISIBILITY_TIMEOUTS', timestamp: start, count, qrl }))
128
+ }
129
+ }
111
130
  // TODO Sentry
112
131
  }
113
- if (this.opt.verbose) {
114
- console.error(chalk.blue('Extended these jobs: '), jobsToExtend)
115
- } else if (!this.opt.disableLog) {
116
- console.log(JSON.stringify({
117
- event: 'EXTEND_VISIBILITY_TIMEOUTS',
118
- timestamp: now,
119
- messageIds: jobsToExtend.map(({ message }) => message.MessageId)
120
- }))
121
- }
122
132
  }
123
133
 
124
134
  // Delete in batches for each queue
@@ -137,43 +147,62 @@ export class JobExecutor {
137
147
  }
138
148
  entries.push(entry)
139
149
  }
150
+ debug({ entries })
140
151
 
141
152
  // Delete batch
142
153
  const input = { QueueUrl: qrl, Entries: entries }
143
154
  debug({ DeleteMessageBatch: input })
144
155
  const result = await getSQSClient().send(new DeleteMessageBatchCommand(input))
145
156
  this.stats.sqsCalls++
146
- if (result.Successful) this.stats.jobsDeleted += result.Successful.length || 0
157
+ if (result.Successful) {
158
+ const count = result.Successful.length || 0
159
+ this.stats.jobsDeleted += count
160
+ if (this.opt.verbose) {
161
+ console.error(chalk.blue('Deleted'), count, chalk.blue('jobs'))
162
+ } else if (!this.opt.disableLog) {
163
+ console.log(JSON.stringify({ event: 'DELETE_MESSAGES', timestamp: start, count, qrl }))
164
+ }
165
+ }
147
166
  debug('DeleteMessageBatch returned', result)
148
167
  // TODO Sentry
149
168
  }
150
- if (this.opt.verbose) {
151
- console.error(chalk.blue('Deleted these finished jobs: '), jobsToDelete)
152
- } else if (!this.opt.disableLog) {
153
- console.log(JSON.stringify({
154
- event: 'DELETE_MESSAGES',
155
- timestamp: now,
156
- messageIds: jobsToDelete.map(({ message }) => message.MessageId)
157
- }))
158
- }
159
169
  }
160
170
 
161
171
  // Get rid of deleted and failed jobs
162
- 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
+ })
163
181
 
164
- // Check again later, unless we are shutting down and nothing's left
182
+ // Bail if we are shutting down
165
183
  if (this.shutdownRequested && this.stats.activeJobs === 0 && this.jobs.length === 0) return
166
- this.maintainVisibilityTimeout = setTimeout(() => this.maintainVisibility(), 10 * 1000)
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)
167
196
  }
168
197
 
169
198
  async executeJob (message, callback, qname, qrl) {
170
199
  // Create job entry and track it
171
200
  const payload = this.opt.json ? JSON.parse(message.Body) : message.Body
172
- const visibilityTimeout = 30
201
+ const visibilityTimeout = 60
173
202
  const job = {
174
203
  status: 'processing',
175
204
  start: new Date(),
176
- visibilityTimeout: 30,
205
+ visibilityTimeout,
177
206
  extendAtSecond: visibilityTimeout / 2,
178
207
  payload: this.opt.json ? JSON.parse(message.Body) : message.Body,
179
208
  message,
@@ -181,8 +210,23 @@ export class JobExecutor {
181
210
  qname,
182
211
  qrl
183
212
  }
184
- 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)
185
228
  this.jobs.push(job)
229
+ this.jobsByMessageId[job.message.MessageId] = job
186
230
  this.stats.activeJobs++
187
231
  if (this.opt.verbose) {
188
232
  console.error(chalk.blue('Executing:'), qname, chalk.blue('-->'), job.payload)
@@ -190,7 +234,7 @@ export class JobExecutor {
190
234
  console.log(JSON.stringify({
191
235
  event: 'MESSAGE_PROCESSING_START',
192
236
  timestamp: new Date(),
193
- qname,
237
+ qrl,
194
238
  messageId: message.MessageId,
195
239
  payload: job.payload
196
240
  }))
@@ -219,7 +263,7 @@ export class JobExecutor {
219
263
  }
220
264
  this.stats.jobsSucceeded++
221
265
  } catch (err) {
222
- debug('exec.catch')
266
+ // debug('exec.catch', err)
223
267
  // Fail path for job execution
224
268
  if (this.opt.verbose) {
225
269
  console.error(chalk.red('FAILED'), message.Body)
@@ -229,7 +273,7 @@ export class JobExecutor {
229
273
  console.log(JSON.stringify({
230
274
  event: 'MESSAGE_PROCESSING_FAILED',
231
275
  reason: 'exception thrown',
232
- qname,
276
+ qrl,
233
277
  timestamp: new Date(),
234
278
  messageId: message.MessageId,
235
279
  payload,
@@ -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
  }