Skip to content

perf: filter workflows on K8s API during infractl list API calls#1882

Merged
tommartensen merged 13 commits into
masterfrom
tm/improve-performance
Jul 14, 2026
Merged

perf: filter workflows on K8s API during infractl list API calls#1882
tommartensen merged 13 commits into
masterfrom
tm/improve-performance

Conversation

@tommartensen

@tommartensen tommartensen commented Jul 13, 2026

Copy link
Copy Markdown
Contributor
  • adding label based filtering for owner and flavor only showed performance improvements if the clusters were owned by someone else (that was expected) -- besides the inefficient retrieval of workflows, we also inefficiently get artifacts from GCP for each of the selected workflows.
  • adding deleted label to filter deleted or naturally expired workflows from List() and background loops.

--> Reduces time spent for List() operations by 10-15% (in realistic scenario, where most clusters belong to other users. In extreme scenario, where current user owns all clusters, no noticable change).

  • introduce LRU cache with 1000 entries (100 workflows x 10 artifacts) for workflow artifacts

--> Reduces time spent for List() operations by 75% (across different scenarios). Probably other methods like Info(), Get() are also much quicker.

Before deploying this change, all existing workflows need to get the new labels, otherwise they won't show up correctly when requesting specific user or flavors

TODO

  • add script for setting the owner and flavor labels in old workflows for backwards compatibility

@tommartensen tommartensen self-assigned this Jul 13, 2026
@rhacs-bot

Copy link
Copy Markdown
Contributor

A single node development cluster (infra-pr-1882) was allocated in production infra for this PR.

CI will attempt to deploy quay.io/rhacs-eng/infra-server: to it.

🔌 You can connect to this cluster with:

gcloud container clusters get-credentials infra-pr-1882 --zone us-central1-a --project acs-team-temp-dev

🛠️ And pull infractl from the deployed dev infra-server with:

nohup kubectl -n infra port-forward svc/infra-server-service 8443:8443 &
make pull-infractl-from-dev-server

🔓 You must go to to export an . Your token from the prod infra instance will not work with dev environments.

🚲 You can then use the dev infra instance e.g.:

bin/infractl -k -e localhost:8443 whoami

⚠️ Any clusters that you start using your dev infra instance should have a lifespan shorter then the development cluster instance. Otherwise they will not be destroyed when the dev infra instance ceases to exist when the development cluster is deleted. ⚠️

Further Development

☕ If you make changes, you can commit and push and CI will take care of updating the development cluster.

🚀 If you only modify configuration (chart/infra-server/configuration) or templates (chart/infra-server/{static,templates}), you can get a faster update with:

make helm-deploy

Logs

Logs for the development infra depending on your @redhat.com authuser:

Or:

kubectl -n infra logs -l app=infra-server --tail=1 -f

@coderabbitai

coderabbitai Bot commented Jul 13, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Cluster workflows now use normalized owner and flavor labels, server-side selectors, and deleted-state labels. Artifact retrieval now uses a thread-safe LRU cache with Prometheus metrics and tests covering cache behavior.

Changes

Cluster workflow filtering

Layer / File(s) Summary
Label contracts and selector construction
pkg/service/cluster/labels.go, pkg/service/cluster/helpers.go, pkg/service/cluster/helpers_test.go
Adds owner, flavor, and deleted labels, normalizes email values, builds Kubernetes selectors, removes obsolete helpers, and tests selector construction.
Workflow label emission
pkg/service/cluster/cluster.go
Cluster creation stores normalized owner and flavor identifiers in workflow labels.
Workflow listing and deleted-state lifecycle
pkg/service/cluster/cluster.go
Listing and background workflows use label selectors, deletion patches labelDeleted=true, and lookup excludes deleted workflows.

Artifact cache

Layer / File(s) Summary
Cache implementation and service wiring
pkg/service/cluster/artifact_cache.go, pkg/service/cluster/cluster.go, pkg/service/cluster/helpers.go, go.mod
Adds the LRU cache, initializes it in the cluster service, and consults it before signer retrieval.
Cache validation and metrics
pkg/service/cluster/artifact_cache_test.go, pkg/service/metrics/metrics.go
Tests cache hits, multiple entries, eviction, and registers cache hit, miss, and size metrics.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant getClusterDetailsFromArtifacts
  participant artifactCache
  participant signer.Contents
  getClusterDetailsFromArtifacts->>artifactCache: Get(bucket, key)
  artifactCache-->>getClusterDetailsFromArtifacts: cached content or miss
  getClusterDetailsFromArtifacts->>signer.Contents: fetch on cache miss
  signer.Contents-->>getClusterDetailsFromArtifacts: artifact content
  getClusterDetailsFromArtifacts->>artifactCache: Set(bucket, key, content)
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly summarizes the main performance change: server-side workflow filtering during list calls.
Description check ✅ Passed The description matches the changeset, covering label-based filtering, deleted-workflow handling, and the artifact cache.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch tm/improve-performance

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
pkg/service/cluster/helpers_test.go (1)

7-51: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add edge-case tests for label value validity.

The test suite verifies length ≤ 63 but doesn't assert that the result starts and ends with an alphanumeric character (the K8s label value regex requirement). Missing cases:

  1. Email starting with + (e.g., +tag@example.com) — verifies leading-dot trimming after the fix.
  2. Long email where truncation lands on a dash or underscore — verifies trailing non-dot trimming.
  3. A validity assertion in the test loop: result must match ^[A-Za-z0-9]([A-Za-z0-9._-]*[A-Za-z0-9])?$ or be empty.
♻️ Suggested additional test cases and validation
 		{
 			name:     "complex email",
 			email:    "user.name+test@subdomain.example.com",
 			expected: "user.name.plus.test.at.subdomain.example.com",
 		},
+		{
+			name:     "email starting with plus",
+			email:    "+tag@example.com",
+			expected: "plus.tag.at.example.com",
+		},
+		{
+			name:     "long email truncating on dash",
+			email:    "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa-tag@example.com",
+			expected: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
+		},
 	}
 
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
 			result := emailToLabelValue(tt.email)
 			if result != tt.expected {
 				t.Errorf("emailToLabelValue(%q) = %q, want %q", tt.email, result, tt.expected)
 			}
 			if len(result) > 63 {
 				t.Errorf("emailToLabelValue(%q) returned %q with length %d, exceeds 63 chars", tt.email, result, len(result))
 			}
+			if result != "" && !regexp.MustCompile(`^[A-Za-z0-9]([A-Za-z0-9._-]*[A-Za-z0-9])?$`).MatchString(result) {
+				t.Errorf("emailToLabelValue(%q) returned %q, not a valid K8s label value", tt.email, result)
+			}
 		})
 	}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/service/cluster/helpers_test.go` around lines 7 - 51, Extend
TestEmailToLabelValue with cases for an email beginning with “+” and a long
email whose truncation ends on a dash or underscore, asserting the expected
trimmed label values. In the test loop, validate each result against the
Kubernetes label-value pattern ^[A-Za-z0-9]([A-Za-z0-9._-]*[A-Za-z0-9])?$ or
allow an empty result, while retaining the existing length check.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@pkg/service/cluster/helpers.go`:
- Around line 292-315: Update buildLabelSelector so non-all requests with an
empty email are rejected before constructing the selector, returning an
appropriate error instead of allowing an owner-less or flavor-only selector.
Preserve the existing owner and allowed-flavor filtering for valid requests and
the unrestricted behavior for req.All.
- Around line 274-288: Update emailToLabelValue to guarantee Kubernetes label
values start and end with alphanumeric characters: after replacing @ and +, trim
leading and trailing non-alphanumeric separators, including dots, dashes, and
underscores, while retaining the 63-character limit and ensuring truncation
cannot leave an invalid trailing character.

---

Nitpick comments:
In `@pkg/service/cluster/helpers_test.go`:
- Around line 7-51: Extend TestEmailToLabelValue with cases for an email
beginning with “+” and a long email whose truncation ends on a dash or
underscore, asserting the expected trimmed label values. In the test loop,
validate each result against the Kubernetes label-value pattern
^[A-Za-z0-9]([A-Za-z0-9._-]*[A-Za-z0-9])?$ or allow an empty result, while
retaining the existing length check.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Central YAML (base), Organization UI (inherited)

Review profile: CHILL

Plan: Enterprise

Run ID: 9d21b494-77e2-4d06-bbe5-dd3873f52d40

📥 Commits

Reviewing files that changed from the base of the PR and between a7a8b93 and f04270d.

📒 Files selected for processing (4)
  • pkg/service/cluster/cluster.go
  • pkg/service/cluster/helpers.go
  • pkg/service/cluster/helpers_test.go
  • pkg/service/cluster/labels.go

Comment thread pkg/service/cluster/helpers.go
Comment thread pkg/service/cluster/helpers.go

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
pkg/service/cluster/artifact_cache.go (1)

19-22: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Redundant external sync.RWMutexlru.Cache is already thread-safe.

hashicorp/golang-lru/v2's lru.Cache uses an internal sync.RWMutex; all methods (Get, Add, Len) are safe for concurrent use. The external mu doubles lock contention on every cache operation with no correctness benefit — Prometheus counters and gauges are individually atomic, so the compound Get+Inc and Add+Len+Set sequences don't need external atomicity.

♻️ Remove the redundant mutex
 type artifactCache struct {
 	cache *lru.Cache[string, []byte]
-	mu    sync.RWMutex
 }
 
 // newArtifactCache creates a new artifact cache with the specified size.
 func newArtifactCache(size int) (*artifactCache, error) {
 	cache, err := lru.New[string, []byte](size)
 	if err != nil {
 		return nil, fmt.Errorf("failed to create LRU cache: %w", err)
 	}
 
 	return &artifactCache{
 		cache: cache,
 	}, nil
 }
 
 // Get retrieves cached artifact content if present.
 // Returns the content and true if found, nil and false otherwise.
 func (c *artifactCache) Get(bucket, key string) ([]byte, bool) {
-	c.mu.RLock()
-	defer c.mu.RUnlock()
-
 	cacheKey := makeCacheKey(bucket, key)
 	content, found := c.cache.Get(cacheKey)
 	if !found {
 		metrics.ArtifactCacheMissesCounter.Inc()
 		return nil, false
 	}
 
 	metrics.ArtifactCacheHitsCounter.Inc()
 	return content, true
 }
 
 // Set stores artifact content in the cache.
 func (c *artifactCache) Set(bucket, key string, content []byte) {
-	c.mu.Lock()
-	defer c.mu.Unlock()
-
 	cacheKey := makeCacheKey(bucket, key)
 	c.cache.Add(cacheKey, content)
 	metrics.ArtifactCacheSizeGauge.Set(float64(c.cache.Len()))
 }

Also applies to: 38-51, 54-61

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/service/cluster/artifact_cache.go` around lines 19 - 22, Remove the
redundant sync.RWMutex field from artifactCache and eliminate its associated
locking and unlock calls around cache operations. Keep using lru.Cache methods
directly in the affected Get, Add, Len, and metric-update paths, preserving the
existing cache behavior and metrics.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@pkg/service/cluster/artifact_cache.go`:
- Around line 19-22: Remove the redundant sync.RWMutex field from artifactCache
and eliminate its associated locking and unlock calls around cache operations.
Keep using lru.Cache methods directly in the affected Get, Add, Len, and
metric-update paths, preserving the existing cache behavior and metrics.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Central YAML (base), Organization UI (inherited)

Review profile: CHILL

Plan: Enterprise

Run ID: 42e9461f-cb39-4be1-9b6c-905b4476355f

📥 Commits

Reviewing files that changed from the base of the PR and between f04270d and e88fe34.

⛔ Files ignored due to path filters (1)
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (9)
  • chart/infra-server/argo-values.yaml
  • go.mod
  • pkg/service/cluster/artifact_cache.go
  • pkg/service/cluster/artifact_cache_test.go
  • pkg/service/cluster/cluster.go
  • pkg/service/cluster/helpers.go
  • pkg/service/cluster/helpers_test.go
  • pkg/service/cluster/labels.go
  • pkg/service/metrics/metrics.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • pkg/service/cluster/labels.go

@davdhacs davdhacs left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some questions/nits, but the core changes look good: Moving the filter server-side looks like a good optimization for handling lists of hundreds of clusters. And caching the cluster details instead of the GCS api calls 100% makes sense.

Comment thread pkg/service/cluster/cluster.go Outdated
Comment thread pkg/service/cluster/artifact_cache.go Outdated
Comment thread pkg/service/cluster/cluster.go
Comment thread chart/infra-server/argo-values.yaml Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@pkg/service/cluster/cluster.go`:
- Around line 670-671: Handle the error returned by labels.NewRequirement in the
cluster selector construction near clusterIDRequirement instead of discarding
it. If requirement creation fails, return or propagate the error through the
surrounding function’s existing error-handling path before dereferencing
clusterIDRequirement; only add the requirement to labelSelector after successful
creation.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Central YAML (base), Organization UI (inherited)

Review profile: CHILL

Plan: Enterprise

Run ID: 3d65d8b0-9452-4364-aae4-48616bb26f5f

📥 Commits

Reviewing files that changed from the base of the PR and between d6a21fb and b964845.

📒 Files selected for processing (4)
  • pkg/service/cluster/cluster.go
  • pkg/service/cluster/helpers.go
  • pkg/service/cluster/helpers_test.go
  • pkg/service/cluster/labels.go
🚧 Files skipped from review as they are similar to previous changes (2)
  • pkg/service/cluster/helpers_test.go
  • pkg/service/cluster/helpers.go

Comment thread pkg/service/cluster/cluster.go Outdated
…when invalid IDs are used as Kubernetes labels
@tommartensen tommartensen marked this pull request as ready for review July 14, 2026 11:59
@tommartensen tommartensen requested review from a team and rhacs-bot as code owners July 14, 2026 11:59
@tommartensen tommartensen merged commit 88b13b1 into master Jul 14, 2026
12 checks passed
@tommartensen tommartensen deleted the tm/improve-performance branch July 14, 2026 12:55
@davdhacs

Copy link
Copy Markdown
Contributor

excellent! 🥳

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants