Skip to content

Replace SQS singleton with per-pipeline client cache with TTL eviction - #291

Open
gshikhar2021 wants to merge 2 commits into
redhat-data-and-ai:mainfrom
gshikhar2021:separate-sqsClient
Open

Replace SQS singleton with per-pipeline client cache with TTL eviction#291
gshikhar2021 wants to merge 2 commits into
redhat-data-and-ai:mainfrom
gshikhar2021:separate-sqsClient

Conversation

@gshikhar2021

@gshikhar2021 gshikhar2021 commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Summary by CodeRabbit

  • Bug Fixes

    • Improved SQS connectivity and reliability for pipelines using queue-based wake-ups.
    • Prevented stale SQS connections from remaining after a pipeline is removed.
    • Ensured separate pipelines can use their own AWS queue configurations without interference.
  • Performance

    • Added automatic cleanup of inactive SQS connections to reduce resource usage.
  • Tests

    • Improved end-to-end coverage for SQS client initialization and queue access.

@gemini-code-assist

Copy link
Copy Markdown

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request refactors the SQS client management strategy to improve scalability and resource management. By moving from a single global client to a per-pipeline cache, the system can now effectively handle multiple concurrent pipelines. The addition of TTL-based eviction ensures that inactive clients are cleaned up, preventing memory leaks and stale connections.

Highlights

  • SQS Client Caching: Replaced the global SQS singleton with a thread-safe per-pipeline client cache to support multiple pipelines simultaneously.
  • TTL Eviction: Implemented a background cleanup process that evicts stale SQS clients from the cache based on a configurable time-to-live (TTL).
  • Controller Lifecycle Integration: Updated the controller to handle SQS client cleanup when a SourceCrawler resource is deleted and integrated cache initialization into the main application startup.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize the Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counterproductive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@gemini-code-assist gemini-code-assist 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.

Code Review

This pull request replaces the single global SQS client with a multi-tenant, TTL-evicted client cache (SQSClientCache) to support multiple pipelines. The review feedback highlights critical issues that need to be addressed: using only the resource name as a cache key can lead to cross-namespace cache collisions, so namespaced names should be used instead. Additionally, the cache currently lacks support for credential rotation because it does not compare incoming AWS configurations with cached ones, and the cleanup routine is susceptible to a panic if initialized with an invalid TTL.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines +81 to +84
if apierrors.IsNotFound(err) {
awsclienthandler.DeleteSQSClient(req.Name)
return ctrl.Result{}, nil
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

security-critical critical

Using only req.Name as the cache key can lead to cache collisions if multiple SourceCrawler resources with the same name exist in different namespaces. Since this operator can watch multiple namespaces, we should use the namespaced name (e.g., req.NamespacedName.String()) as the cache key to prevent cross-namespace client leakage.

Suggested change
if apierrors.IsNotFound(err) {
awsclienthandler.DeleteSQSClient(req.Name)
return ctrl.Result{}, nil
}
if apierrors.IsNotFound(err) {
awsclienthandler.DeleteSQSClient(req.NamespacedName.String())
return ctrl.Result{}, nil
}

Comment on lines +169 to +172
if _, err := awsclienthandler.NewSQSClientFromConfig(ctx, sourceAWSConfig, sourceCrawlerCR.Name); err != nil {
return ctrl.Result{}, r.handleError(ctx, sourceCrawlerCR, fmt.Errorf("failed to create SQS client: %w", err))
}
return handleSQSWakeUp(ctx, sqsQueueURL, sourceCrawlerConfig.S3Config.Bucket, sourceCrawlerConfig.S3Config.Prefix), nil
return handleSQSWakeUp(ctx, sqsQueueURL, sourceCrawlerConfig.S3Config.Bucket, sourceCrawlerConfig.S3Config.Prefix, sourceCrawlerCR.Name), nil

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

security-critical critical

Using sourceCrawlerCR.Name as the cache key can cause cache collisions across different namespaces. We should use the namespaced name string of the resource to ensure uniqueness.

			namespacedName := types.NamespacedName{Namespace: sourceCrawlerCR.Namespace, Name: sourceCrawlerCR.Name}.String()
			if _, err := awsclienthandler.NewSQSClientFromConfig(ctx, sourceAWSConfig, namespacedName); err != nil {
				return ctrl.Result{}, r.handleError(ctx, sourceCrawlerCR, fmt.Errorf("failed to create SQS client: %w", err))
			}
			return handleSQSWakeUp(ctx, sqsQueueURL, sourceCrawlerConfig.S3Config.Bucket, sourceCrawlerConfig.S3Config.Prefix, namespacedName), nil

Comment on lines +36 to +39
type sqsEntry struct {
client *sqs.Client
lastUsed time.Time
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

To support credential rotation and updates, we should store the AWSConfig in the sqsEntry so that we can detect when the configuration has changed and recreate the client accordingly.

Suggested change
type sqsEntry struct {
client *sqs.Client
lastUsed time.Time
}
type sqsEntry struct {
client *sqs.Client
awsConfig AWSConfig
lastUsed time.Time
}

Comment thread pkg/awsclienthandler/sqs_client.go Outdated
Comment on lines 98 to 102
entry, ok := sqsClientCache.GetClient(pipelineName)
if ok {
logger.Info("Using existing SQS client for the pipeline", "pipelineName", pipelineName)
return entry.client, nil
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

If the AWS credentials are rotated or updated in the Secret, the controller will continue using the cached client with the old/stale credentials because it ignores the incoming awsConfig when a cached client exists. We should compare the incoming configuration with the cached one and recreate the client if they differ.

Suggested change
entry, ok := sqsClientCache.GetClient(pipelineName)
if ok {
logger.Info("Using existing SQS client for the pipeline", "pipelineName", pipelineName)
return entry.client, nil
}
entry, ok := sqsClientCache.GetClient(pipelineName)
if ok && awsConfig != nil && entry.awsConfig == *awsConfig {
logger.Info("Using existing SQS client for the pipeline", "pipelineName", pipelineName)
return entry.client, nil
}

Comment thread pkg/awsclienthandler/sqs_client.go Outdated
Comment on lines +119 to +120
newEntry := &sqsEntry{client: sqsClient, lastUsed: time.Now()}
sqsClientCache.SetClient(pipelineName, newEntry)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

Store the AWSConfig in the new cache entry to enable configuration comparison on subsequent calls.

Suggested change
newEntry := &sqsEntry{client: sqsClient, lastUsed: time.Now()}
sqsClientCache.SetClient(pipelineName, newEntry)
var cfgVal AWSConfig
if awsConfig != nil {
cfgVal = *awsConfig
}
newEntry := &sqsEntry{
client: sqsClient,
awsConfig: cfgVal,
lastUsed: time.Now(),
}
sqsClientCache.SetClient(pipelineName, newEntry)

Comment on lines +68 to +69
func (c *SQSClientCache) startCleanup(ctx context.Context, ttl time.Duration) {
ticker := time.NewTicker(ttl / 2)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

If ttl is less than or equal to 0, time.NewTicker(ttl / 2) will panic. We should add a defensive check to prevent panics if an invalid TTL is passed.

func (c *SQSClientCache) startCleanup(ctx context.Context, ttl time.Duration) {
	if ttl <= 0 {
		return
	}
	ticker := time.NewTicker(ttl / 2)

@gshikhar2021
gshikhar2021 force-pushed the separate-sqsClient branch 2 times, most recently from 05bc52d to fd51748 Compare July 21, 2026 13:43
@coderabbitai

coderabbitai Bot commented Jul 29, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The application now initializes a TTL-based, pipeline-scoped SQS client cache. SourceCrawler reconciliation creates, retrieves, and deletes clients by pipeline identifier, and e2e setup uses named client lookup.

Changes

SQS client cache lifecycle

Layer / File(s) Summary
SQS cache and startup initialization
pkg/awsclienthandler/sqs_client.go, cmd/main.go
SQS clients are cached per pipeline with configuration matching, TTL eviction, deletion, and presence-based lookup; startup initializes the cache with a 30-minute TTL.
SourceCrawler SQS lifecycle integration
internal/controller/sourcecrawler_controller.go
Reconciliation uses namespaced pipeline identifiers, handles missing resources by deleting cached clients, and passes named clients into SQS wake-up handling.
E2E client lookup validation
test/e2e/unstructured_test.go
The e2e test initializes and retrieves the SQS client using the e2e-test identifier.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant SourceCrawlerReconciler
  participant SQSClientCache
  participant SQSClient
  SourceCrawlerReconciler->>SQSClientCache: Create or retrieve client by namespace/name
  SourceCrawlerReconciler->>SQSClientCache: Look up pipeline client
  SQSClientCache-->>SourceCrawlerReconciler: Return client and presence status
  SourceCrawlerReconciler->>SQSClient: Handle SQS wake-up
  SourceCrawlerReconciler->>SQSClientCache: Delete client when resource is not found
Loading

Suggested reviewers: concaf

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: moving from a singleton SQS client to a per-pipeline cache with TTL eviction.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
test/e2e/unstructured_test.go (1)

65-65: 🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win

Stale call will not compile: NewSQSClientFromConfig now requires a pipelineName argument.

sqs_client.go changed the signature to NewSQSClientFromConfig(ctx, awsConfig, pipelineName), but line 101 still calls it with only 2 args — this is a build-breaking compile error. This call (and the outer sqsClient declared at line 65) also appear to be dead leftovers: the test's actual SQS admin operations exclusively use the named "e2e-test" cached client fetched via GetSQSClient at line 141, which currently shadows this outer variable. The simplest fix is to remove the stale block entirely rather than just adding the missing argument.

🐛 Proposed fix
-	var sqsClient *sqs.Client
-			sqsClient, err = awsclienthandler.NewSQSClientFromConfig(ctx, e2eAWS)
-			if err != nil {
-				t.Fatal(err)
-			}
-

Removing these also eliminates the local-vs-outer sqsClient shadowing at line 141.

Also applies to: 101-104, 141-144

🤖 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 `@test/e2e/unstructured_test.go` at line 65, Remove the unused outer sqsClient
declaration and the stale NewSQSClientFromConfig initialization block in the
test; retain the named “e2e-test” client obtained via GetSQSClient for all SQS
admin operations, eliminating the local shadowing.
🤖 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 `@test/e2e/unstructured_test.go`:
- Around line 89-99: Preserve and validate the error returned by
NewDestinationS3ClientFromConfig before initializing the SQS client, so it
cannot be overwritten by the subsequent NewSQSClientFromConfig assignment. Keep
the existing fatal behavior for either client initialization failure.

---

Outside diff comments:
In `@test/e2e/unstructured_test.go`:
- Line 65: Remove the unused outer sqsClient declaration and the stale
NewSQSClientFromConfig initialization block in the test; retain the named
“e2e-test” client obtained via GetSQSClient for all SQS admin operations,
eliminating the local shadowing.
🪄 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: defaults

Review profile: CHILL

Plan: Enterprise

Run ID: 6d2898ae-c045-49c6-bc75-cb266c1a7849

📥 Commits

Reviewing files that changed from the base of the PR and between 7abca92 and 6f74e1e.

📒 Files selected for processing (4)
  • cmd/main.go
  • internal/controller/sourcecrawler_controller.go
  • pkg/awsclienthandler/sqs_client.go
  • test/e2e/unstructured_test.go

Comment on lines 89 to 99
err = awsclienthandler.NewDestinationS3ClientFromConfig(ctx, e2eAWS)
// create SQS client
_, err = awsclienthandler.NewSQSClientFromConfig(ctx, &awsclienthandler.AWSConfig{
Region: "us-east-1",
AccessKeyID: "test",
SecretAccessKey: "test",
Endpoint: localstackURL,
}, "e2e-test")
if err != nil {
t.Fatal(err)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win

Newly inserted code swallows the error from NewDestinationS3ClientFromConfig.

Line 89 assigns err from NewDestinationS3ClientFromConfig, but the inserted SQS-client-init block (lines 90-96) immediately reassigns err before it's checked — the original error is discarded and only the new call's error is validated at line 97.

🐛 Proposed fix
 			err = awsclienthandler.NewDestinationS3ClientFromConfig(ctx, e2eAWS)
+			if err != nil {
+				t.Fatal(err)
+			}
+
 			// create SQS client
 			_, err = awsclienthandler.NewSQSClientFromConfig(ctx, &awsclienthandler.AWSConfig{
 				Region:          "us-east-1",
 				AccessKeyID:     "test",
 				SecretAccessKey: "test",
 				Endpoint:        localstackURL,
 			}, "e2e-test")
 			if err != nil {
 				t.Fatal(err)
 			}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
err = awsclienthandler.NewDestinationS3ClientFromConfig(ctx, e2eAWS)
// create SQS client
_, err = awsclienthandler.NewSQSClientFromConfig(ctx, &awsclienthandler.AWSConfig{
Region: "us-east-1",
AccessKeyID: "test",
SecretAccessKey: "test",
Endpoint: localstackURL,
}, "e2e-test")
if err != nil {
t.Fatal(err)
}
err = awsclienthandler.NewDestinationS3ClientFromConfig(ctx, e2eAWS)
if err != nil {
t.Fatal(err)
}
// create SQS client
_, err = awsclienthandler.NewSQSClientFromConfig(ctx, &awsclienthandler.AWSConfig{
Region: "us-east-1",
AccessKeyID: "test",
SecretAccessKey: "test",
Endpoint: localstackURL,
}, "e2e-test")
if err != nil {
t.Fatal(err)
}
🤖 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 `@test/e2e/unstructured_test.go` around lines 89 - 99, Preserve and validate
the error returned by NewDestinationS3ClientFromConfig before initializing the
SQS client, so it cannot be overwritten by the subsequent NewSQSClientFromConfig
assignment. Keep the existing fatal behavior for either client initialization
failure.

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.

1 participant