diff --git a/CHANGELOG.md b/CHANGELOG.md index 32f6c393aae..b7be6e0189f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -63,6 +63,7 @@ To learn more about active deprecations, we recommend checking [GitHub Discussio - **CloudEventSource**: Provide ClusterCloudEventSource around the management of TriggerAuthentication/ClusterTriggerAuthentication resources ([#3524](https://github.com/kedacore/keda/issues/3524)) - **Github Action**: Fix panic when env for runnerScopeFromEnv or ownerFromEnv is empty ([#6156](https://github.com/kedacore/keda/issues/6156)) - **RabbitMQ Scaler**: provide separate paremeters for user and password ([#2513](https://github.com/kedacore/keda/issues/2513)) +- **Azure Pipelines Scalar**: Print warning to log when Azure DevOps API Rate Limits are (nearly) reached ([#6284](https://github.com/kedacore/keda/issues/6284)) #### Experimental diff --git a/pkg/scalers/azure_pipelines_scaler.go b/pkg/scalers/azure_pipelines_scaler.go index 99e6de0dff4..1c526570d4f 100644 --- a/pkg/scalers/azure_pipelines_scaler.go +++ b/pkg/scalers/azure_pipelines_scaler.go @@ -412,6 +412,16 @@ func getAzurePipelineRequest(ctx context.Context, logger logr.Logger, urlString return []byte{}, fmt.Errorf("the Azure DevOps REST API returned error. urlString: %s status: %d response: %s", urlString, r.StatusCode, string(b)) } + // Log when API Rate Limits are reached + rateLimitRemaining := r.Header[http.CanonicalHeaderKey("X-RateLimit-Remaining")] + if rateLimitRemaining != nil { + logger.V(1).Info(fmt.Sprintf("Warning: ADO TSTUs Left %s. When reaching zero requests are delayed, lower the polling interval. See https://learn.microsoft.com/en-us/azure/devops/integrate/concepts/rate-limits?view=azure-devops", rateLimitRemaining)) + } + rateLimitDelay := r.Header[http.CanonicalHeaderKey("X-RateLimit-Delay")] + if rateLimitDelay != nil { + logger.V(1).Info(fmt.Sprintf("Warning: Request to ADO API is delayed by %s seconds. Sending additional requests will increase delay until results are being blocked entirely", rateLimitDelay)) + } + return b, nil } diff --git a/pkg/scalers/azure_pipelines_scaler_test.go b/pkg/scalers/azure_pipelines_scaler_test.go index f65460ac3b4..b246c431ed2 100644 --- a/pkg/scalers/azure_pipelines_scaler_test.go +++ b/pkg/scalers/azure_pipelines_scaler_test.go @@ -222,6 +222,32 @@ func TestAzurePipelinesMatchedAgent(t *testing.T) { } } +func TestAzurePipelinesDelayed(t *testing.T) { + var apiStub = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Add("X-RateLimit-Limit", "0") + w.Header().Add("X-RateLimit-Delay", "42") + w.WriteHeader(http.StatusOK) + _, _ = w.Write(buildLoadJSON()) + })) + + meta := getMatchedAgentMetaData(apiStub.URL) + + mockAzurePipelinesScaler := azurePipelinesScaler{ + metadata: meta, + httpClient: http.DefaultClient, + } + + queueLen, err := mockAzurePipelinesScaler.GetAzurePipelinesQueueLength(context.TODO()) + + if err != nil { + t.Fail() + } + + if queueLen < 1 { + t.Fail() + } +} + func getDemandJobMetaData(url string) *azurePipelinesMetadata { meta := getMatchedAgentMetaData(url) meta.parent = ""