Skip to content

Commit 32e4fde

Browse files
Add retry mechanism for scan enqueue failures(AST-0000) (#1374)
1 parent 5597316 commit 32e4fde

File tree

6 files changed

+94
-20
lines changed

6 files changed

+94
-20
lines changed

internal/commands/scan.go

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -124,11 +124,12 @@ const (
124124
"--scs-repo-url your_repo_url --scs-repo-token your_repo_token"
125125
ScsScorecardUnsupportedHostWarningMsg = "SCS scan warning: Unable to run Scorecard scanner due to unsupported repo host. Currently, Scorecard can only run on GitHub Cloud repos."
126126

127-
jsonExt = ".json"
128-
xmlExt = ".xml"
129-
sbomScanTypeErrMsg = "The --sbom-only flag can only be used when the scan type is sca"
130-
BranchPrimaryPrefix = "--branch-primary="
131-
OverridePolicyManagement = "override-policy-management"
127+
jsonExt = ".json"
128+
xmlExt = ".xml"
129+
sbomScanTypeErrMsg = "The --sbom-only flag can only be used when the scan type is sca"
130+
BranchPrimaryPrefix = "--branch-primary="
131+
OverridePolicyManagement = "override-policy-management"
132+
defaultScanEnqueueRetryDelay = 5
132133
)
133134

134135
var (
@@ -708,6 +709,16 @@ func scanCreateSubCommand(
708709
0,
709710
"Cancel the scan and fail after the timeout in minutes",
710711
)
712+
createScanCmd.PersistentFlags().Int(
713+
commonParams.ScanEnqueueRetriesFlag,
714+
0,
715+
"Number of retry attempts for scan enqueue failures due to queue capacity (default: 0, no retries)",
716+
)
717+
createScanCmd.PersistentFlags().Int(
718+
commonParams.ScanEnqueueRetryDelayFlag,
719+
defaultScanEnqueueRetryDelay,
720+
"Base delay in seconds between scan enqueue retry attempts with exponential backoff (default: 5)",
721+
)
711722
createScanCmd.PersistentFlags().StringP(
712723
commonParams.SourcesFlag,
713724
commonParams.SourcesFlagSh,
@@ -859,6 +870,14 @@ func scanCreateSubCommand(
859870
if err != nil {
860871
log.Fatal(err)
861872
}
873+
err = viper.BindPFlag(commonParams.ScanEnqueueRetriesKey, createScanCmd.PersistentFlags().Lookup(commonParams.ScanEnqueueRetriesFlag))
874+
if err != nil {
875+
log.Fatal(err)
876+
}
877+
err = viper.BindPFlag(commonParams.ScanEnqueueRetryDelayKey, createScanCmd.PersistentFlags().Lookup(commonParams.ScanEnqueueRetryDelayFlag))
878+
if err != nil {
879+
log.Fatal(err)
880+
}
862881

863882
createScanCmd.PersistentFlags().String(commonParams.SSHKeyFlag, "", "Path to ssh private key")
864883

internal/params/envs.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ const (
44
CustomStatesAPIPathEnv = "CX_CUSTOM_STATES_PATH"
55
TenantEnv = "CX_TENANT"
66
BranchEnv = "CX_BRANCH"
7+
ScanEnqueueRetriesEnv = "CX_SCAN_ENQUEUE_RETRIES"
8+
ScanEnqueueRetryDelayEnv = "CX_SCAN_ENQUEUE_RETRY_DELAY"
79
BaseURIEnv = "CX_BASE_URI"
810
ClientTimeoutEnv = "CX_TIMEOUT"
911
ProxyEnv = "HTTP_PROXY"

internal/params/flags.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ const (
3333
AsyncFlag = "async"
3434
WaitDelayFlag = "wait-delay"
3535
ScanTimeoutFlag = "scan-timeout"
36+
ScanEnqueueRetriesFlag = "scan-enqueue-retries"
37+
ScanEnqueueRetryDelayFlag = "scan-enqueue-retry-delay"
3638
PolicyTimeoutFlag = "policy-timeout"
3739
IgnorePolicyFlag = "ignore-policy"
3840
SourceDirFilterFlag = "file-filter"

internal/params/keys.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ var (
66
CustomStatesAPIPathKey = strings.ToLower(CustomStatesAPIPathEnv)
77
TenantKey = strings.ToLower(TenantEnv)
88
BranchKey = strings.ToLower(BranchEnv)
9+
ScanEnqueueRetriesKey = strings.ToLower(ScanEnqueueRetriesEnv)
10+
ScanEnqueueRetryDelayKey = strings.ToLower(ScanEnqueueRetryDelayEnv)
911
BaseURIKey = strings.ToLower(BaseURIEnv)
1012
ProxyKey = strings.ToLower(ProxyEnv)
1113
ProxyTypeKey = strings.ToLower(ProxyTypeEnv)

internal/wrappers/mock/scans-mock.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import (
99
"github.com/pkg/errors"
1010
)
1111

12+
const ErrCodeMaxQueuedReached = 142
13+
1214
var HasScs bool
1315

1416
type ScansMockWrapper struct {
@@ -21,6 +23,13 @@ func (m *ScansMockWrapper) GetWorkflowByID(_ string) ([]*wrappers.ScanTaskRespon
2123

2224
func (m *ScansMockWrapper) Create(scanModel *wrappers.Scan) (*wrappers.ScanResponseModel, *wrappers.ErrorModel, error) {
2325
fmt.Println("Called Create in ScansMockWrapper")
26+
if scanModel.Project.ID == "fake-queue-capacity-error-id" {
27+
return nil, &wrappers.ErrorModel{
28+
Code: ErrCodeMaxQueuedReached,
29+
Message: "Failed to enqueue scan. Max Queued reached",
30+
Type: "ERROR",
31+
}, nil
32+
}
2433
if scanModel.Project.ID == "fake-kics-scanner-fail-id" {
2534
return &wrappers.ScanResponseModel{
2635
ID: "fake-scan-id-kics-scanner-fail",

internal/wrappers/scans-http.go

Lines changed: 55 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,19 @@ import (
55
"encoding/json"
66
"fmt"
77
"net/http"
8+
"time"
89

10+
"github.com/checkmarx/ast-cli/internal/logger"
911
commonParams "github.com/checkmarx/ast-cli/internal/params"
1012
"github.com/pkg/errors"
1113
"github.com/spf13/viper"
1214
)
1315

1416
const (
15-
failedToParseGetAll = "Failed to parse list response"
16-
failedToParseTags = "Failed to parse tags response"
17-
failedToParseBranches = "Failed to parse branches response"
17+
failedToParseGetAll = "Failed to parse list response"
18+
failedToParseTags = "Failed to parse tags response"
19+
failedToParseBranches = "Failed to parse branches response"
20+
queueCapacityErrorCode = 142
1821
)
1922

2023
type ScansHTTPWrapper struct {
@@ -29,26 +32,63 @@ func NewHTTPScansWrapper(path string) ScansWrapper {
2932
}
3033
}
3134

35+
// isQueueCapacityError checks if the error is due to queue capacity limits (error code 142)
36+
func isQueueCapacityError(errorModel *ErrorModel) bool {
37+
return errorModel != nil && errorModel.Code == queueCapacityErrorCode
38+
}
39+
3240
func (s *ScansHTTPWrapper) Create(model *Scan) (*ScanResponseModel, *ErrorModel, error) {
3341
clientTimeout := viper.GetUint(commonParams.ClientTimeoutKey)
3442
jsonBytes, err := json.Marshal(model)
3543
if err != nil {
3644
return nil, nil, err
3745
}
3846

39-
fn := func() (*http.Response, error) {
40-
return SendHTTPRequest(http.MethodPost, s.path, bytes.NewBuffer(jsonBytes), true, clientTimeout)
41-
}
42-
resp, err := retryHTTPRequest(fn, retryAttempts, retryDelay)
43-
if err != nil {
44-
return nil, nil, err
45-
}
46-
defer func() {
47-
if err == nil {
48-
_ = resp.Body.Close()
47+
// Get scan enqueue retry configuration
48+
scanEnqueueRetries := viper.GetInt(commonParams.ScanEnqueueRetriesKey)
49+
scanEnqueueRetryDelay := viper.GetInt(commonParams.ScanEnqueueRetryDelayKey)
50+
51+
var scanResp *ScanResponseModel
52+
var errorModel *ErrorModel
53+
54+
// Retry loop for scan creation (queue capacity errors)
55+
for attempt := 0; attempt <= scanEnqueueRetries; attempt++ {
56+
// Standard HTTP retry (for 502, 401)
57+
fn := func() (*http.Response, error) {
58+
return SendHTTPRequest(http.MethodPost, s.path, bytes.NewBuffer(jsonBytes), true, clientTimeout)
4959
}
50-
}()
51-
return handleScanResponseWithBody(resp, err, http.StatusCreated)
60+
resp, err := retryHTTPRequest(fn, retryAttempts, retryDelay)
61+
if err != nil {
62+
return nil, nil, err
63+
}
64+
65+
// Parse response
66+
scanResp, errorModel, err = handleScanResponseWithBody(resp, err, http.StatusCreated)
67+
// Close response body explicitly after parsing
68+
_ = resp.Body.Close()
69+
if err != nil {
70+
return nil, nil, err
71+
}
72+
73+
// Check if it's a queue capacity error and we have retries left
74+
if isQueueCapacityError(errorModel) && attempt < scanEnqueueRetries {
75+
// Calculate exponential backoff delay
76+
waitDuration := time.Duration(scanEnqueueRetryDelay) * time.Second * (1 << attempt)
77+
logger.PrintIfVerbose(fmt.Sprintf(
78+
"Scan creation failed due to queue capacity (attempt %d/%d). Waiting %v before retry...",
79+
attempt+1,
80+
scanEnqueueRetries,
81+
waitDuration,
82+
))
83+
time.Sleep(waitDuration)
84+
continue
85+
}
86+
87+
// Success or non-retryable error - break out of loop
88+
break
89+
}
90+
91+
return scanResp, errorModel, err
5292
}
5393

5494
func (s *ScansHTTPWrapper) Get(params map[string]string) (*ScansCollectionResponseModel, *ErrorModel, error) {

0 commit comments

Comments
 (0)