screwdriver-queue-service 6.0.0 → 6.0.2

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "screwdriver-queue-service",
3
- "version": "6.0.0",
3
+ "version": "6.0.2",
4
4
  "description": "Screwdriver Queue Service API",
5
5
  "main": "app.js",
6
6
  "directories": {
package/plugins/helper.js CHANGED
@@ -85,12 +85,15 @@ function formatOptions(method, url, token, json, retryStrategyFn) {
85
85
  * Update build status
86
86
  * @method updateBuildStatus
87
87
  * @param {Object} updateConfig build config of the job
88
+ * @param {Object} updateConfig.buildConfig Optional buildConfig to avoid Redis lookup
88
89
  * @return {Promise}
89
90
  */
90
91
  async function updateBuildStatus(updateConfig) {
91
- const { redisInstance, status, statusMessage, buildId } = updateConfig;
92
+ const { redisInstance, status, statusMessage, buildId, buildConfig: providedConfig } = updateConfig;
92
93
 
93
- const buildConfig = await redisInstance.hget(`${queuePrefix}buildConfigs`, buildId).then(JSON.parse);
94
+ // Use provided buildConfig if available, otherwise fetch from Redis
95
+ const buildConfig =
96
+ providedConfig || (await redisInstance.hget(`${queuePrefix}buildConfigs`, buildId).then(JSON.parse));
94
97
 
95
98
  if (!buildConfig) return null;
96
99
 
@@ -118,11 +121,15 @@ async function updateBuildStatus(updateConfig) {
118
121
  * @param {String} stepConfig.buildId
119
122
  * @param {String} stepConfig.stepName
120
123
  * @param {Integer} stepConfig.code
124
+ * @param {Object} stepConfig.buildConfig Optional buildConfig to avoid Redis lookup
121
125
  * @return {Promise} response body or error
122
126
  */
123
127
  async function updateStepStop(stepConfig) {
124
- const { redisInstance, buildId, stepName, code } = stepConfig;
125
- const buildConfig = await redisInstance.hget(`${queuePrefix}buildConfigs`, buildId).then(JSON.parse);
128
+ const { redisInstance, buildId, stepName, code, buildConfig: providedConfig } = stepConfig;
129
+
130
+ // Use provided buildConfig if available, otherwise fetch from Redis
131
+ const buildConfig =
132
+ providedConfig || (await redisInstance.hget(`${queuePrefix}buildConfigs`, buildId).then(JSON.parse));
126
133
 
127
134
  // if buildConfig got deleted already, do not update
128
135
  if (!buildConfig) return null;
@@ -149,11 +156,15 @@ async function updateStepStop(stepConfig) {
149
156
  * @param {Object} stepConfig
150
157
  * @param {Object} stepConfig.redisInstance
151
158
  * @param {String} stepConfig.buildId
159
+ * @param {Object} stepConfig.buildConfig Optional buildConfig to avoid Redis lookup
152
160
  * @return {Promise} active step or error
153
161
  */
154
162
  async function getCurrentStep(stepConfig) {
155
- const { redisInstance, buildId } = stepConfig;
156
- const buildConfig = await redisInstance.hget(`${queuePrefix}buildConfigs`, buildId).then(JSON.parse);
163
+ const { redisInstance, buildId, buildConfig: providedConfig } = stepConfig;
164
+
165
+ // Use provided buildConfig if available, otherwise fetch from Redis
166
+ const buildConfig =
167
+ providedConfig || (await redisInstance.hget(`${queuePrefix}buildConfigs`, buildId).then(JSON.parse));
157
168
 
158
169
  // if buildConfig got deleted already, do not update
159
170
  if (!buildConfig) return null;
@@ -493,7 +493,7 @@ async function start(executor, config) {
493
493
  {
494
494
  buildId,
495
495
  jobId,
496
- blockedBy: blockedBy.toString(),
496
+ blockedBy,
497
497
  blockedBySameJob,
498
498
  blockedBySameJobWaitTime
499
499
  }
@@ -758,13 +758,7 @@ async function stopTimer(executor, config) {
758
758
  async function stop(executor, config) {
759
759
  await executor.connect();
760
760
 
761
- const { buildId, jobId } = config; // in case config contains something else
762
-
763
- let blockedBy;
764
-
765
- if (config.blockedBy !== undefined) {
766
- blockedBy = config.blockedBy.toString();
767
- }
761
+ const { buildId, jobId, blockedBy } = config; // in case config contains something else
768
762
 
769
763
  const numDeleted = await executor.queueBreaker.runCommand('del', executor.buildQueue, 'start', [
770
764
  {
@@ -134,11 +134,18 @@ class BlockedBy extends NodeResque.Plugin {
134
134
  */
135
135
  async reEnqueue(buildConfig, decision) {
136
136
  const { buildId } = buildConfig;
137
- const blockedBy = decision.blockedBy || [];
137
+ const blockedByArray = decision.blockedBy || [];
138
+ const runningBuildId = decision.runningBuildId;
139
+
140
+ const allBlockingBuilds = [...blockedByArray];
141
+
142
+ if (runningBuildId) {
143
+ allBlockingBuilds.push(runningBuildId);
144
+ }
138
145
 
139
146
  let statusMessage = 'Blocked by these running build(s): ';
140
147
 
141
- statusMessage += blockedBy
148
+ statusMessage += allBlockingBuilds
142
149
  .map(blockingBuildId => `<a href="/builds/${blockingBuildId}">${blockingBuildId}</a>`)
143
150
  .join(', ');
144
151
 
@@ -177,7 +184,8 @@ class BlockedBy extends NodeResque.Plugin {
177
184
  redisInstance: this.queueObject.connection.redis,
178
185
  buildId,
179
186
  status: 'COLLAPSED',
180
- statusMessage: newestBuild ? `Collapsed to build: ${newestBuild}` : 'Collapsed'
187
+ statusMessage: newestBuild ? `Collapsed to build: ${newestBuild}` : 'Collapsed',
188
+ buildConfig
181
189
  })
182
190
  .catch(err => {
183
191
  logger.error(`Failed to update build status to COLLAPSED for build:${buildId}:${err}`);
@@ -44,6 +44,32 @@ local waitingBuilds = redis.call("LRANGE", waitingKey, 0, -1)
44
44
  local lastRunningBuildId = redis.call("GET", lastRunningKey)
45
45
  local deleteKeyExists = redis.call("EXISTS", deleteKey)
46
46
 
47
+ -- Early exit: If buildConfig doesn't exist, build was already processed/deleted
48
+ -- Clean up any remaining state and abort
49
+ if not buildConfig then
50
+ redis.log(redis.LOG_WARNING, string.format(
51
+ "[startBuild] BUILD_CONFIG_MISSING: build=%s job=%s - buildConfig deleted externally",
52
+ buildId, jobId
53
+ ))
54
+
55
+ redis.call("LREM", waitingKey, 0, buildId)
56
+ redis.call("DEL", deleteKey)
57
+
58
+ -- If this build is somehow marked as running, clean that up too
59
+ if runningBuildId and tostring(runningBuildId) == tostring(buildId) then
60
+ redis.call("DEL", runningKey)
61
+ end
62
+ if lastRunningBuildId and tostring(lastRunningBuildId) == tostring(buildId) then
63
+ redis.call("DEL", lastRunningKey)
64
+ end
65
+
66
+ return cjson.encode({
67
+ action = "ABORT",
68
+ reason = "BUILD_CONFIG_MISSING",
69
+ buildId = buildId
70
+ })
71
+ end
72
+
47
73
  -- Check if build was aborted (deleteKey exists)
48
74
  local isAborted = (deleteKeyExists == 1)
49
75
 
@@ -58,25 +84,19 @@ end
58
84
 
59
85
  -- Check which dependencies are currently running
60
86
  local runningBuilds = {}
87
+ local runningJobIds = {}
61
88
  for _, depJobId in ipairs(dependencies) do
62
89
  local depRunningBuild = redis.call("GET", runningJobsPrefix .. tostring(depJobId))
63
90
  if depRunningBuild then
64
91
  table.insert(runningBuilds, depRunningBuild)
92
+ table.insert(runningJobIds, depJobId)
65
93
  end
66
94
  end
67
95
 
68
96
  -- Helper: Check if blocked by dependencies
69
- local function isBlockedByDependencies(deps, running)
70
- local blockedByBuilds = {}
71
- for _, dep in ipairs(deps) do
72
- for _, runningId in ipairs(running) do
73
- if tostring(dep) == tostring(runningId) then
74
- table.insert(blockedByBuilds, dep)
75
- break
76
- end
77
- end
78
- end
79
- return #blockedByBuilds > 0, blockedByBuilds
97
+ -- Returns: isBlocked (boolean), blockedByBuilds (array of buildIds that are blocking)
98
+ local function isBlockedByDependencies(runningBuildIds)
99
+ return #runningBuildIds > 0, runningBuildIds
80
100
  end
81
101
 
82
102
  -- Helper: Check if blocked by same job
@@ -121,10 +141,17 @@ local function shouldCollapse()
121
141
  return false, nil, "IS_NEWEST_BUILD"
122
142
  end
123
143
 
124
- local isBlockedByDeps, blockedByBuilds = isBlockedByDependencies(dependencies, runningBuilds)
144
+ local isBlockedByDeps, blockedByBuilds = isBlockedByDependencies(runningBuilds)
125
145
  local isBlockedBySelf = isBlockedBySameJob()
126
146
  local shouldCollapseFlag, newestBuild, collapseReason = shouldCollapse()
127
147
 
148
+ redis.log(redis.LOG_NOTICE, string.format(
149
+ "[startBuild] build=%s job=%s deps=%d blockedByDeps=%s blockedBySelf=%s runningBuildId=%s lastRunning=%s shouldCollapse=%s deleteKeyExists=%s",
150
+ buildId, jobId, #dependencies, tostring(isBlockedByDeps), tostring(isBlockedBySelf),
151
+ tostring(runningBuildId or "nil"), tostring(lastRunningBuildId or "nil"),
152
+ tostring(shouldCollapseFlag), tostring(deleteKeyExists == 1)
153
+ ))
154
+
128
155
  -- Determine final action (Priority: ABORT > COLLAPSE > BLOCK > START)
129
156
  local action, reason, actionData
130
157
 
@@ -154,7 +181,11 @@ end
154
181
 
155
182
  -- Update Redis state based on decision
156
183
  if action == "ABORT" then
157
- -- Build was aborted, no state changes needed
184
+ -- Build was aborted - clean up waiting queue and configs
185
+ redis.call("HDEL", buildConfigKey, buildId)
186
+ redis.call("LREM", waitingKey, 0, buildId)
187
+ redis.call("DEL", deleteKey)
188
+
158
189
  return cjson.encode({
159
190
  action = "ABORT",
160
191
  reason = reason,
@@ -163,6 +194,10 @@ if action == "ABORT" then
163
194
 
164
195
  elseif action == "COLLAPSE" then
165
196
  -- Collapse this build - remove from configs and waiting queue
197
+ redis.log(redis.LOG_NOTICE, string.format(
198
+ "[startBuild] COLLAPSE: Deleting buildConfig for build=%s job=%s reason=%s",
199
+ buildId, jobId, reason
200
+ ))
166
201
  redis.call("HDEL", buildConfigKey, buildId)
167
202
  redis.call("LREM", waitingKey, 0, buildId)
168
203
 
@@ -187,9 +222,6 @@ elseif action == "BLOCK" then
187
222
  redis.call("RPUSH", waitingKey, buildId)
188
223
  end
189
224
 
190
- -- Set deleteKey with timeout (prevents zombie builds)
191
- redis.call("SET", deleteKey, buildId, "EX", blockTimeout * 60)
192
-
193
225
  return cjson.encode({
194
226
  action = "BLOCK",
195
227
  reason = reason,
@@ -25,8 +25,6 @@
25
25
  }
26
26
  ]]
27
27
 
28
- -- Load cjson for JSON encoding/decoding
29
- local cjson = require("cjson")
30
28
 
31
29
  -- Parse arguments
32
30
  local buildId = ARGV[1]
@@ -31,9 +31,10 @@ const hash = `${queuePrefix}timeoutConfigs`;
31
31
  * @param {String} buildId
32
32
  * @param {Object} redis
33
33
  * @param {String} workerId
34
+ * @param {Object} buildConfig - Build config fetched before Lua execution
34
35
  * @return {Promise}
35
36
  */
36
- async function executeTimeout(decision, buildId, redis, workerId) {
37
+ async function executeTimeout(decision, buildId, redis, workerId, buildConfig) {
37
38
  const { timeoutMinutes } = decision;
38
39
 
39
40
  // Get and update current step
@@ -42,7 +43,8 @@ async function executeTimeout(decision, buildId, redis, workerId) {
42
43
  try {
43
44
  step = await helper.getCurrentStep({
44
45
  redisInstance: redis,
45
- buildId
46
+ buildId,
47
+ buildConfig
46
48
  });
47
49
  } catch (err) {
48
50
  logger.error(`worker[${workerId}] -> No active step found for ${buildId}`);
@@ -53,7 +55,8 @@ async function executeTimeout(decision, buildId, redis, workerId) {
53
55
  redisInstance: redis,
54
56
  buildId,
55
57
  stepName: step.name,
56
- code: TIMEOUT_CODE
58
+ code: TIMEOUT_CODE,
59
+ buildConfig
57
60
  });
58
61
  }
59
62
 
@@ -61,7 +64,8 @@ async function executeTimeout(decision, buildId, redis, workerId) {
61
64
  redisInstance: redis,
62
65
  buildId,
63
66
  status: 'FAILURE',
64
- statusMessage: `Build failed due to timeout (${timeoutMinutes} minutes)`
67
+ statusMessage: `Build failed due to timeout (${timeoutMinutes} minutes)`,
68
+ buildConfig
65
69
  });
66
70
 
67
71
  logger.info(`worker[${workerId}] -> Timeout cleanup completed for build ${buildId}`);
@@ -73,14 +77,15 @@ async function executeTimeout(decision, buildId, redis, workerId) {
73
77
  * @param {String} buildId - Build ID
74
78
  * @param {Object} redis - Redis instance
75
79
  * @param {String} workerId - Worker ID
80
+ * @param {Object} buildConfig - Build config fetched before Lua execution
76
81
  * @return {Promise}
77
82
  */
78
- async function handleDecision(decision, buildId, redis, workerId) {
83
+ async function handleDecision(decision, buildId, redis, workerId, buildConfig) {
79
84
  switch (decision.action) {
80
85
  case 'TIMEOUT':
81
86
  // Build has timed out - execute cleanup
82
87
  logger.info(`worker[${workerId}] -> Build has timed out ${buildId}`);
83
- await executeTimeout(decision, buildId, redis, workerId);
88
+ await executeTimeout(decision, buildId, redis, workerId, buildConfig);
84
89
  break;
85
90
 
86
91
  case 'CLEANUP':
@@ -119,6 +124,10 @@ async function process(timeoutConfig, buildId, redis, workerId) {
119
124
  }
120
125
 
121
126
  try {
127
+ // Fetch buildConfig BEFORE Lua execution (Lua script will delete it if timeout)
128
+ const buildConfigJson = await redis.hget(`${queuePrefix}buildConfigs`, buildId);
129
+ const buildConfig = buildConfigJson ? JSON.parse(buildConfigJson) : null;
130
+
122
131
  const loader = getLuaScriptLoader();
123
132
  const result = await loader.executeScript(
124
133
  'checkTimeout.lua',
@@ -139,7 +148,7 @@ async function process(timeoutConfig, buildId, redis, workerId) {
139
148
 
140
149
  logger.info(`worker[${workerId}] -> Build ${buildId}: action=${decision.action}, reason=${decision.reason}`);
141
150
 
142
- await handleDecision(decision, buildId, redis, workerId);
151
+ await handleDecision(decision, buildId, redis, workerId, buildConfig);
143
152
  } catch (err) {
144
153
  logger.error(`Error in timeout check for build ${buildId}: ${err.message}`);
145
154
  logger.error(err.stack);
@@ -94,7 +94,8 @@ async function invoke() {
94
94
  redisInstance: redis,
95
95
  buildId: job.args[0].buildId,
96
96
  status: 'FAILURE',
97
- statusMessage: `${failure}`
97
+ statusMessage: `${failure}`,
98
+ buildConfig: job.args[0] // Pass buildConfig to avoid Redis lookup
98
99
  })
99
100
  .then(() => {
100
101
  logger.info(