Skip to content

Commit 56bf4a7

Browse files
bors[bot]eddiewebb
andauthored
Merge #46
46: add confidence checks to mitigate race conditions [semver:minor] r=eddiewebb a=eddiewebb ### Checklist <!-- thank you for contributing to CircleCI Concurrency Control Orb! before submitting your request, please go through the following items and place an x in the [ ] if they have been completed --> - [x] All new jobs, commands, executors, parameters have descriptions - [ ] Examples have been added for any significant new features - [ ] README has been updated, if necessary ### Motivation, issues this helps mitigate the issue in #26 by adding redundant checks before allowing job to proceed ### Description add confidence threshold to repeat running job check. Co-authored-by: Eddie Webb <[email protected]>
2 parents 84f9ea7 + 507ee9f commit 56bf4a7

File tree

6 files changed

+92
-32
lines changed

6 files changed

+92
-32
lines changed

src/@orb.yml

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,15 @@ description: |
33
Allows jobs or entire workflows to be queued to ensure they run in serial.
44
This is ideal for deployments or other activities that must not run concurrently.
55
May optionaly consider branch-level isolation if unique branches should run concurrently.
6-
This orb requires the project to have an API to query build statuses.
6+
This orb requires the project to have an API key in order to query build states.
7+
8+
display:
9+
home_url: https://eddiewebb.github.io/circleci-queue/
10+
source_url: https://github.com/eddiewebb/circleci-queue
711

812
examples:
913
queue_workflow:
10-
description: Used typically as first job and will queue until no jobs from a previous workflow are running
14+
description: Used typically as first job and will queue until no previous workflows are running
1115
usage:
1216
version: 2.1
1317
orbs:
@@ -24,7 +28,10 @@ examples:
2428
- queue/block_workflow
2529

2630
single_concurrency_job:
27-
description: Used to ensure that a single job (deploy) is not run concurrently
31+
description: |
32+
Used to ensure that a only single job (deploy) is not run concurrently.
33+
By default will only queue if the same job from previous worfklows is running on the same branch.
34+
This allows safe jobs like build/test to overlap, minimizing overall queue times.
2835
usage:
2936
version: 2.1
3037
orbs:

src/commands/until_front_of_line.yml

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,6 @@ parameters:
33
type: boolean
44
default: true
55
description: "Should we only consider jobs running on the same branch?"
6-
consider-job:
7-
type: boolean
8-
# deprecated, do not use
9-
description: "Deprecated. Please see block-workflow."
10-
default: true
116
block-workflow:
127
type: boolean
138
# this is false at COMMAND level as intention is to only block CURRENT job.
@@ -29,6 +24,10 @@ parameters:
2924
type: string
3025
default: "github"
3126
description: "Override VCS to 'bitbucket' if needed."
27+
confidence:
28+
type: string
29+
default: "1"
30+
description: "Due to scarce API, we need to requery the recent jobs list to ensure we're not just in a pending state for previous jobs. This number indicates the threhold for API returning no previous pending jobs. Default is a single confirmation."
3231
steps:
3332
- run:
3433
name: Queue Until Front of Line
@@ -101,7 +100,7 @@ steps:
101100
load_current_workflow_values
102101
103102
# falsey parameters are empty strings, so always compare against 'true'
104-
if [ "<<parameters.consider-job>>" != "true" ] || [ "<<parameters.block-workflow>>" = "true" ] ;then
103+
if [ "<<parameters.block-workflow>>" = "true" ] ;then
105104
echo "Orb parameter block-workflow is true."
106105
echo "This job will block until no previous workflows have *any* jobs running."
107106
oldest_running_build_num=`jq 'sort_by(.workflows.created_at)| .[0].build_num' /tmp/augmented_jobstatus.json`
@@ -166,15 +165,23 @@ steps:
166165
#
167166
# Queue Loop
168167
#
168+
confidence=0
169169
while true; do
170170
update_comparables
171171
echo "This Workflow Timestamp: $my_commit_time"
172172
echo "Oldest Workflow Timestamp: $oldest_commit_time"
173173
if [[ "$oldest_commit_time" > "$my_commit_time" ]] || [[ "$oldest_commit_time" = "$my_commit_time" ]] ; then
174174
# API returns Y-M-D HH:MM (with 24 hour clock) so alphabetical string compare is accurate to timestamp compare as well
175-
# in event of race, everyone wins
176-
echo "Front of the line, WooHoo!, Build continuing"
177-
break
175+
# recent-jobs API does not include pending, so it is posisble we queried in between a workfow transition, and we;re NOT really front of line.
176+
if [ $confidence -lt <<parameters.confidence>> ];then
177+
# To grow confidence, we check again with a delay.
178+
confidence=$((confidence+1))
179+
echo "API shows no previous jobs/workflows, but it is possible a previous workflow has pending jobs not yet visible in API."
180+
echo "Rerunning check ${confidence}/<<parameters.confidence>>"
181+
else
182+
echo "Front of the line, WooHoo!, Build continuing"
183+
break
184+
fi
178185
else
179186
echo "This build (${CIRCLE_BUILD_NUM}) is queued, waiting for build number (${oldest_running_build_num}) to complete."
180187
echo "Total Queue time: ${wait_time} seconds."

src/jobs/block_workflow.yml

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,6 @@ parameters:
33
type: boolean
44
default: true
55
description: "Should we only consider jobs running on the same branch?"
6-
consider-job:
7-
type: boolean
8-
# deprecated, do not use.
9-
default: false
10-
description: "Deprecated. Please see block-workflow."
116
block-workflow:
127
type: boolean
138
# this is true at JOB level as intention is to block workflow
@@ -24,20 +19,24 @@ parameters:
2419
only-on-branch:
2520
type: string
2621
default: "*"
27-
description: "Only queue on specified branch"
22+
description: "Only queue on specified branch. Default is to enforce serialization on all branches."
2823
vcs-type:
2924
type: string
3025
default: "github"
3126
description: "Override VCS to 'bitbucket' if needed."
27+
confidence:
28+
type: string
29+
default: "1"
30+
description: "Due to scarce API, we need to requery the recent jobs list to ensure we're not just in a pending state for previous jobs. This number indicates the threhold for API returning no previous pending jobs. Default is a single confirmation."
3231

3332
docker:
34-
- image: circleci/node:10 #its just really popular, and therefore fast (cached)
33+
- image: cimg/base:stable
3534
resource_class: small
3635
steps:
3736
- until_front_of_line:
3837
consider-branch: <<parameters.consider-branch>>
39-
consider-job: <<parameters.consider-job>>
4038
time: <<parameters.time>>
4139
dont-quit: <<parameters.dont-quit>>
4240
only-on-branch: <<parameters.only-on-branch>>
4341
vcs-type: <<parameters.vcs-type>>
42+
confidence: <<parameters.confidence>>

test/bats_helper.bash

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,19 @@
11
#!/bin/bash
22

33

4+
function process_config_with {
5+
append_project_configuration $1 > $INPUT_PROJECT_CONFIG
6+
circleci config process $INPUT_PROJECT_CONFIG > ${PROCESSED_PROJECT_CONFIG}
7+
yq read -j ${PROCESSED_PROJECT_CONFIG} > ${JSON_PROJECT_CONFIG}
8+
9+
#assertions use output, tests can override outptu to test additional commands beyond parsing.
10+
output=`cat ${PROCESSED_PROJECT_CONFIG}`
11+
}
12+
413
function append_project_configuration {
514
if [ -z "$BATS_IMPORT_DEV_ORB" ]; then
6-
echo "#Using \`inline\` orb assembly, to test against published orb, set BATS_IMPORT_DEV_ORB to fully qualified path" >&3
715
assemble_inline $1
816
else
9-
echo "#BATS_IMPORT_DEV_ORB env var is set, all config will be tested against imported orb $BATS_IMPORT_DEV_ORB" >&3
1017
assemble_external $1
1118
fi
1219
}
@@ -39,14 +46,6 @@ function assemble_external {
3946
fi
4047
}
4148

42-
function process_config_with {
43-
append_project_configuration $1 > $INPUT_PROJECT_CONFIG
44-
circleci config process $INPUT_PROJECT_CONFIG > ${PROCESSED_PROJECT_CONFIG}
45-
yq read -j ${PROCESSED_PROJECT_CONFIG} > ${JSON_PROJECT_CONFIG}
46-
47-
#assertions use output, tests can override outptu to test additional commands beyond parsing.
48-
output=`cat ${PROCESSED_PROJECT_CONFIG}`
49-
}
5049

5150

5251
#

test/inputs/command-non-default.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,5 @@ jobs:
88
# non-defaults for each paramter
99
time: "1"
1010
consider-branch: false
11-
consider-job: false
1211
dont-quit: true
12+
block-workflow: true

test/test_expansion.bats

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,13 @@ function setup {
1313

1414
# the name used in example config files.
1515
INLINE_ORB_NAME="queue"
16+
17+
18+
if [ -z "$BATS_IMPORT_DEV_ORB" ]; then
19+
echo "#Using \`inline\` orb assembly, to test against published orb, set BATS_IMPORT_DEV_ORB to fully qualified path" >&3
20+
else
21+
echo "#BATS_IMPORT_DEV_ORB env var is set, all config will be tested against imported orb $BATS_IMPORT_DEV_ORB" >&3
22+
fi
1623
}
1724

1825

@@ -44,6 +51,46 @@ function setup {
4451
}
4552

4653

54+
# See https://github.com/eddiewebb/circleci-queue/issues/26 for explanation of race condition
55+
@test "Race condition on previous workflow does not fool us" {
56+
# given
57+
process_config_with test/inputs/command-defaults.yml
58+
export TESTING_MOCK_WORKFLOW_RESPONSES=test/api/workflows
59+
60+
# when
61+
assert_jq_match '.jobs | length' 1 #only 1 job
62+
assert_jq_match '.jobs["build"].steps | length' 1 #only 1 steps
63+
64+
jq -r '.jobs["build"].steps[0].run.command' $JSON_PROJECT_CONFIG > ${BATS_TMPDIR}/script-${BATS_TEST_NUMBER}.bash
65+
66+
export CIRCLECI_API_KEY="madethisup"
67+
export CIRCLE_BUILD_NUM="2"
68+
export CIRCLE_JOB="singlejob"
69+
export CIRCLE_PROJECT_USERNAME="madethisup"
70+
export CIRCLE_PROJECT_REPONAME="madethisup"
71+
export CIRCLE_REPOSITORY_URL="madethisup"
72+
export CIRCLE_BRANCH="madethisup"
73+
export CIRCLE_PR_REPONAME=""
74+
75+
# set API Payload to temp location
76+
export TESTING_MOCK_RESPONSE=/tmp/dynamic_response.json
77+
# set initial response to mimic in-btween race condition, no running jobs
78+
cp test/api/jobs/nopreviousjobs.json /tmp/dynamic_response.json
79+
# in 11 seconds (> 10) switch to return the running job BACKGROUND PROCESS
80+
(sleep 11 && cp test/api/jobs/onepreviousjobsamename.json /tmp/dynamic_response.json) &
81+
82+
run bash ${BATS_TMPDIR}/script-${BATS_TEST_NUMBER}.bash
83+
84+
85+
assert_contains_text "Max Queue Time: 1 minutes"
86+
assert_contains_text "Rerunning check 1/1"
87+
assert_contains_text "This build (${CIRCLE_BUILD_NUM}) is queued, waiting for build number (3) to complete."
88+
assert_contains_text "Max wait time exceeded"
89+
assert_contains_text "Cancelleing build 2"
90+
[[ "$status" == "1" ]]
91+
}
92+
93+
4794
@test "Command: script will proceed with no previous jobs" {
4895
# given
4996
process_config_with test/inputs/command-defaults.yml
@@ -130,6 +177,7 @@ function setup {
130177
[[ "$status" == "1" ]]
131178
}
132179

180+
133181
@test "Command: script with dont-quit will not fail current job" {
134182
# given
135183
process_config_with test/inputs/command-non-default.yml
@@ -254,7 +302,7 @@ function setup {
254302

255303

256304

257-
@test "Command: script will queue on different job when consider-job is false" {
305+
@test "Command: script will queue on different job when block-workflow is true" {
258306
# given
259307
process_config_with test/inputs/command-non-default.yml
260308
export TESTING_MOCK_RESPONSE=test/api/jobs/onepreviousjob-differentname.json

0 commit comments

Comments
 (0)