-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathartillery-task.js
More file actions
207 lines (185 loc) · 7.45 KB
/
artillery-task.js
File metadata and controls
207 lines (185 loc) · 7.45 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
/* eslint no-underscore-dangle: 0 */
const aws = require('aws-sdk') // eslint-disable-line import/no-extraneous-dependencies
const { run } = require('artillery')
const modes = require('./modes.js')
const artilleryAcceptance = require('./artillery-acceptance.js')
const artilleryMonitoring = require('./artillery-monitoring.js')
const artilleryPerformance = require('./artillery-performance.js')
const artilleryTask = {
/**
* Given an artillery script, return the appropriate artillery task type.
*
* @param script The artillery script to run, annotated with the expected mode.
* @returns An artillery task fit for purpose.
*/
createArtilleryTask: (script) => {
modes.validateScriptMode(script)
if (modes.isAcceptanceScript(script)) return artilleryAcceptance(artilleryTask)
if (modes.isMonitoringScript(script)) return artilleryMonitoring(artilleryTask)
return artilleryPerformance(artilleryTask)
},
/**
* Invoke the deployed AWS Lambda function to provide the load specified.
*
* @param script The artillery script to run, annotated with the expected mode.
* @param type Invocation type for the Lambda.
* @returns Parsed `Payload` property from AWS Lambda.
*/
invoke: (script, type) => {
const lambda = new aws.Lambda({
maxRetries: 0,
region: process.env.AWS_REGION || 'us-east-1',
})
const params = {
FunctionName: script._funcAws.functionName, // eslint-disable-line no-underscore-dangle
InvocationType: type || 'Event',
Payload: JSON.stringify(script),
}
if (process.env.SERVERLESS_STAGE) {
params.FunctionName += `:${process.env.SERVERLESS_STAGE}`
}
// AWS Lambda (platform-specific) invocation
return lambda.invoke(params).promise()
.then((res) => {
try {
return JSON.parse(res.Payload)
} catch (ex) {
console.error(`Error parsing lambda execution payload:\n${res.Payload}\nCaused error:\n${ex.stack}`)
return undefined // ignore error
}
})
.catch((ex) => {
console.error('Error invoking self:')
console.error(ex.stack)
return Promise.reject(new Error(`ERROR invoking self: ${ex.message}`))
})
},
/**
* Delay for the given number of milliseconds before resolving the returned promise.
*
* @param ms The number of milliseconds to delay before resolving the returned promise.
* @returns {Promise<any>}
*/
delay: (ms) => {
if (ms > 0) return new Promise(resolve => setTimeout(resolve, ms))
return Promise.resolve()
},
/**
* Wait the requested time delay before simulating execution (simulation mode) or sending the given script to a new
* copy of this function for execution (standard mode).
*
* @param timeDelay The amount of time to delay before sending the remaining jobs for execution
* @param script The script containing the remaining jobs that is to be sent to the next Lambda
* @param invocationType The lambda invocationType
* @returns {Promise<any>}
*/
invokeSelf(timeDelay, script, invocationType) {
const trace = script._trace ? console.log : () => {}
const exec = () => {
trace(`invoking self for ${script._genesis} in ${script._start} @ ${Date.now()}`)
return artilleryTask.invoke(script, invocationType)
.then((result) => {
trace(`invoke self complete for ${script._genesis} in ${script._start} @ ${Date.now()}`)
return result
})
}
trace(`scheduling self invocation for ${script._genesis} in ${script._start} with a ${timeDelay} ms delay`)
return artilleryTask.delay(timeDelay).then(exec)
},
/**
* Execute the given plans distributed across copies of this function
*
* @param timeNow The time ID of the current function
* @param script The script that caused the execution of the current function
* @param settings The settings to use for executing in the current function
* @param plans The plans (each an event) to distribute over copies of this function
* @returns {Promise<any>}
*/
distribute: (timeNow, script, settings, plans) => {
const trace = script._trace ? console.log : () => {
}
trace(`distributing ${plans.length} plans from ${script._genesis} in ${timeNow}`)
const invocations = plans.map(plan => artilleryTask.invokeSelf(
(plan._start - Date.now()) - settings.timeBufferInMilliseconds,
plan,
plan._invokeType // eslint-disable-line comma-dangle
).then((result) => {
trace(`load test from ${script._genesis} executed by ${timeNow} partially complete @ ${Date.now()}`)
return result
}))
return Promise.all(invocations)
.then((reports) => {
trace(`load test from ${script._genesis} in ${timeNow} completed @ ${Date.now()}`)
return Promise.resolve({
timeNow,
script,
settings,
reports,
})
})
},
/**
* Execute the given event in place, which is to say in the current function.
*
* @param timeNow The time ID of the current function
* @param script The artillery script to execute in the current function
* @returns {Promise<*>}
*/
execute: (timeNow, script) => new Promise((resolve, reject) => {
// Since Artillery will call process.exit() upon termination,
// we monkey-patch it to load result and resolve/reject the Promise.
const { exit } = process
let testResults = null
process.exit = (code) => {
process.exit = exit // Unpatch
console.log('Artillery done.')
if (code !== 0) {
reject(new Error(`Artillery exited with non-zero code: ${code}`))
} else if (!testResults) {
reject(new Error('Artillery exited with zero, but test results not set.'))
} else {
resolve(testResults)
}
}
console.log('Starting Artillery...')
run(script, { output: (result) => { testResults = result.aggregate } })
}).catch((ex) => {
const msg = `ERROR exception encountered while executing load from ${script._genesis} in ${timeNow}: ${ex.message}\n${ex.stack}`
console.error(msg)
throw new Error(msg)
}),
/**
* Given a plan or set of plans, distribute or execute load as appropriate.
*
* @param script The script that caused the execution of the current function
* @param settings The settings to use for executing in the current function
* @param plans The plans (each an event) to distribute over copies of this function
* @param timeNow The time ID of the current function
* @returns Promise<*> Result of the execution or distribution.
*/
executeAll: (script, settings, plans, timeNow) => {
if (plans.length > 1) {
return artilleryTask.distribute(timeNow, script, settings, plans)
} else if (plans.length === 1) {
return artilleryTask.execute(timeNow, plans[0])
} else {
const msg = `ERROR, no executable content in:\n${JSON.stringify(script)}!`
console.error(msg)
return Promise.reject(new Error(msg))
}
},
/**
* Entry point for the ArtilleryTask. Instantiates the appropriate concrete
* artillery task to run and calls execute().
*
* @param timeNow The time ID of the current function
* @param script The artillery script to execute in the current function
* @returns {Promise<*>}
*/
executeTask: (script, platformSettings) => {
const theArtilleryTask = artilleryTask.createArtilleryTask(script)
const timeNow = Date.now()
return theArtilleryTask.execute(timeNow, script, platformSettings)
},
}
module.exports = artilleryTask