Skip to content

Conversation

weilerN
Copy link
Collaborator

@weilerN weilerN commented Jul 13, 2025

Description

This PR integrates the cache and watcher components into the DLX to enable scale-from-zero functionality based on ingress resources rather than relying on request headers.

Changes Made

  • Refactored initialization logic in both the ingress cache and ingress watcher operator to support external usage from the DLX.
  • Integrated ingresscache.IngressHostCache and kube.IngressWatcher into the DLX.
  • Optimize the watcher's UpdateHandler by adding a fast exit when the old and new objects share the same ResourceVersion

References

Additional Notes

  • This task will be ready for review once manual testing is complete, including the Nuclio side.
  • A separate ticket will handle integrating the cache into the requestHandler as part of completing the full solution

@weilerN weilerN changed the title Nuc 510 integrate cache and watcher into dlx [DLX] Integrate cache and watcher into dlx init Jul 13, 2025
@weilerN weilerN changed the title [DLX] Integrate cache and watcher into dlx init [DLX] Integrate cache and watcher into dlx handler Jul 13, 2025
@weilerN weilerN force-pushed the nuc-510-integrate-cache-and-watcher-into-dlx branch 2 times, most recently from b167f5c to 38a06d3 Compare July 15, 2025 14:27
@weilerN weilerN force-pushed the nuc-510-integrate-cache-and-watcher-into-dlx branch 2 times, most recently from f2f3ca0 to 6163d52 Compare July 15, 2025 15:22
@weilerN weilerN changed the title [DLX] Integrate cache and watcher into dlx handler [DLX] Integrate cache and watcher into dlx struct Jul 15, 2025
@weilerN weilerN force-pushed the nuc-510-integrate-cache-and-watcher-into-dlx branch from 6163d52 to 316dd12 Compare July 15, 2025 15:33
@weilerN weilerN marked this pull request as ready for review July 15, 2025 15:42
@weilerN weilerN requested review from rokatyy and TomerShor and removed request for rokatyy July 15, 2025 15:42
Copy link
Collaborator

@rokatyy rokatyy left a comment

Choose a reason for hiding this comment

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

Nice! we are getting closer and closer to the final :)

Some questions and suggestions

func createDLX(
resourceScaler scalertypes.ResourceScaler,
options scalertypes.DLXOptions,
kubeClientSet kubernetes.Interface,
Copy link
Collaborator

Choose a reason for hiding this comment

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

why not adding kubeClientSet into options?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I initially assumed that options was meant only for static variables or configuration values.
From your comment, I understand that including runtime dependencies like the Kubernetes client in options is acceptable here—and likely the better practice in this case.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

type ingressValue struct {
name string
host string
path string
version string
Copy link
Collaborator

Choose a reason for hiding this comment

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

It's better to avoid changing the actual code for testing purposes, please remove this

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

During development, I initially planned to use this in the actual code (e.g., for logging), but it turned out to be redundant—removing it.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

}

ctx, cancel := context.WithCancel(dlxCtx)
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
ctx, cancel := context.WithCancel(dlxCtx)
contextWithCancel, cancel := context.WithCancel(dlxCtx)

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Comment on lines 274 to 275
testOldObj := suite.createDummyIngress(testCase.testOldObj.host, testCase.testOldObj.path, testCase.testOldObj.version, testCase.testOldObj.targets)
testNewObj := suite.createDummyIngress(testCase.testNewObj.host, testCase.testNewObj.path, testCase.testNewObj.version, testCase.testNewObj.targets)
Copy link
Collaborator

Choose a reason for hiding this comment

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

version can be passed as a parameter here, but let's not change the code

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

pkg/dlx/dlx.go Outdated
return &DLX{
logger: childLogger,
handler: handler,
server: &http.Server{
Addr: options.ListenAddress,
},
cache: cache,
Copy link
Collaborator

Choose a reason for hiding this comment

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

Why passing cache here if we already have it in watcher?

Copy link
Collaborator

Choose a reason for hiding this comment

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

The watcher updates the cache, but the DLX itself will need to read from it when receiving a request.

Copy link
Collaborator

Choose a reason for hiding this comment

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

@TomerShor but we can get it by dlx.watcher.cache, I think here it's better to stick with a single ownership and single source of truth (can also be done via encapsulation like watcher.Cache() or something if we need to only read it)

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Technically, we could access the cache via watcher.cache, but I think it's cleaner to initialize the cache in the main function and pass it explicitly to both the watcher and (in the upcoming PR) to the request handler.

This approach promotes loose coupling — the request handler and the watcher both interact with the cache, but neither should own it. More importantly, it’s preferable that the request handler remains unaware of the watcher operator to maintain a clear separation of concerns. This approach helps minimize unnecessary dependencies between components.

Since the cache is fully wired during initialization, having it stored on the DLX struct may be redundant — I’ll consider removing it if it’s not directly needed there.

Let me know what you think =]

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I removed the cache from the DLX struct - CR comments - remove the cache from the DLX struct

Copy link
Collaborator

@rokatyy rokatyy Jul 16, 2025

Choose a reason for hiding this comment

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

@weilerN Watcher writes to the cache, and DLX reads it. Since watcher is a part of DLX, the idea of initializing and passing the cache separately to both contradicts the Single Responsibility Principle. Only the watcher should have the ability to modify the cache; DLX should not — it merely reads from it. Allowing both components to be independently wired with the same cache introduces unnecessary risk.

This approach helps minimize unnecessary dependencies between components.

The question here is: why do we want to minimize dependencies in this specific case? In fact, the dependency between DLX and watcher isn't unnecessary — it's inherent. DLX already owns the watcher; reading data from a cache managed by its child is a natural and clear dependency.

Trying to artificially decouple them reduce "visible" coupling, it weakens architectural clarity. It also opens the door to subtle bugs — for example, if someone mistakenly passes a copy or separate instance of the cache, we’ll end up with divergence that’s hard to track. This kind of split ownership violates the principle of having a single source of truth.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

@rokatyy Just to clarify — it’s the request handler, not the DLX itself, that reads from the cache. So either we pass the cache directly to the handler, or we pass the watcher and have the handler access the cache through the watcher.

If the concern is about restricting write access, exposing the full cache still leaves that possibility open. We’d need to either wrap it in the watcher (i.e. - watcher.ResolveFromCache() or something like that).

Since your main concern is ownership and making that responsibility explicit within the handler’s boundaries, I’ll follow your direction.
Let me know which approach you prefer ( watcher.cache.Get() or watcher.ResolveFromCache()), and I’ll update the code accordingly.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

@rokatyy @TomerShor I did some changes here (CR comment - move the cache into the watcher and expose only the cach…) to address all the points discussed in this thread:

  • Exposing only the cache.Get to the handler
  • Single Responsibility Principle- the watcher is the responsible for the cache
  • Single source of truth- the cache inside the watcher

Copy link
Collaborator

@TomerShor TomerShor left a comment

Choose a reason for hiding this comment

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

Minor comment. Overall looks great!

Comment on lines 169 to 171
iw.logger.DebugWith("No changes in resource, skipping",
"resourceVersion", oldIngressResource.ResourceVersion,
"ingressName", oldIngressResource.Name)
Copy link
Collaborator

Choose a reason for hiding this comment

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

This will be super spammy - most of the time we won't have changes in the ingress, and with a small resync interval will get 1 log line for every nuclio ingress - could be a very large number.
No need for log here at all.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

That's why this log is DEBUG =]
But ok I will remove

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

@weilerN weilerN requested review from TomerShor and rokatyy July 16, 2025 08:09
Copy link
Collaborator

@rokatyy rokatyy left a comment

Choose a reason for hiding this comment

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

Very well!

Just a minor comment, but anyway approved ✅

@@ -82,12 +82,12 @@ func Run(kubeconfigPath string,
return errors.Wrap(err, "Failed to get client configuration")
}

kubeClientSet, err := kubernetes.NewForConfig(restConfig)
dlxOptions.KubeClientSet, err = kubernetes.NewForConfig(restConfig)
Copy link
Collaborator

Choose a reason for hiding this comment

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

if dlxOptions.KubeClientSet, err = kubernetes.NewForConfig(restConfig); err != nil {}

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Copy link
Collaborator

@rokatyy rokatyy left a comment

Choose a reason for hiding this comment

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

Great idea with a nested reader interface!

@weilerN weilerN force-pushed the nuc-510-integrate-cache-and-watcher-into-dlx branch from 44134be to 659d388 Compare July 16, 2025 14:30
Comment on lines +90 to +92
ResolveTargetsFromIngressCallback ResolveTargetsFromIngressCallback `json:"-"`
ResyncInterval Duration
KubeClientSet kubernetes.Interface `json:"-"`
Copy link
Collaborator

Choose a reason for hiding this comment

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

Why is the json:"-" needed?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

When starting the DLX, an error log appears indicating that the kubeClient failed to be marshaled.
Adding this annotation resolves this false-positive error.

@weilerN weilerN requested a review from TomerShor July 17, 2025 09:00
@TomerShor TomerShor merged commit 59f2bc3 into v3io:development Jul 17, 2025
3 checks passed
TomerShor pushed a commit that referenced this pull request Jul 21, 2025
### Description  
This PR integrates the cache into the handler in order to resolve the
request target based on the new cache.

### Changes Made  
- Add the `IngressHostCacheReader` (the cache read-only access) to the
handler
- Add the logic for the new resolving (i.e. - try to resolve based on
the cache, and if failed - resolve by the existing headers' logic)

### Testing  
- Added a flow test - for the entire request handling  
- Added unit tests for the resolving per case (i.e. - based on headers
and based on ingress)

### References  
- Jira ticket link: https://iguazio.atlassian.net/browse/NUC-523  
- External link: this PR is based on this branch -
#76

### Additional Notes  
- I left a **TODO** question inside the test file — it seems that when
parsing the `targetURL`, the handler appends the path twice. This fix is
relatively minor and has been tested manually, but since the current
code still works, it's worth deciding whether we want to address it now.
rokatyy added a commit to nuclio/nuclio that referenced this pull request Jul 21, 2025
### Description  
This PR integrates the new DLX from the Scaler package, which introduces
a revised internal flow.
The changes align the DLX initialization with the new Scaler
initialization and configuration patterns by updating the `dlxOptions`
and related logic.

### Changes Made  
- Extracted hardcoded configuration variables
- Implemented the `scalertype.ResolveTargetsFromIngressCallback` for
extracting Nuclio targets
- Added changes to the DLX init (as described in the Jira)

### Testing  
- Unit tests for the new `resolveCallback`
- Manual integration tests to verify the functionalities
 
### References  
- Jira ticket link  - https://iguazio.atlassian.net/browse/NUC-509
- External links - the related scaler PRs
-- v3io/scaler#76 
-- v3io/scaler#78

### Additional Notes  
- Before merging this PR, the `go.mod` must be modified since it
currently relies on a feature branch

---------

Co-authored-by: Katerina Molchanova <[email protected]>
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