[Framework] Add ingress cache#70
Conversation
TomerShor
left a comment
There was a problem hiding this comment.
Initial review, mostly related to our conventions.
Main things are:
- File name are all lowercase, even for multiple words.
- We use the
errorspackage overfmt.Errorfetc. - When returning errors, think about future Noam trying to debug this code - what will be his experience?
- Utilities have a dedicated space in the
commonpackage. - Private over Public - consider who's gonna use the interfaces and structs, and if they should be indeed private.
I'll focus more on the logic the next round.
pkg/ingressCache/ingressHostTree.go
Outdated
| @@ -0,0 +1,7 @@ | |||
| package ingressCache | |||
|
|
|||
| type ingressHostsTree interface { | |||
There was a problem hiding this comment.
Why is this private? will this only be used internally in the ingresscache package?
Usually interfaces are meant to be implemented by structs from different packages. If there is only one object that will implement these functions - maybe it shouldn't be an interface.
There was a problem hiding this comment.
I agree that this interface could be public, but I prefer to keep it. The data structure implementing the interface is based on a third-party open-source package, and keeping the interface gives us flexibility to change or modify the package if needed.
pkg/ingressCache/pathTrie.go
Outdated
| const ( | ||
| emptyPathError = "path is empty" | ||
| emptyFunctionNameError = "function name is empty" | ||
| walkPathResultError = "value is not a []string" | ||
| functionNotExistsError = "function does not exist in path" | ||
| ) |
There was a problem hiding this comment.
define error type consts instead of strings:
const (
emptyPathError error = errors.New("Path is empty")
...
)There was a problem hiding this comment.
secondly - I prefer to avoid having errors as constants, and much prefer having the explicit string where the errors are returned.
It makes the code a lot more readable, and the messages easy for find in the code if you encounter them.
thirdly, we use the errors package (github.com/nuclio/errors) which provides both New and better Wrapping of errors, using errors.Wrap(err, <message>).
There was a problem hiding this comment.
Thanks for the explanation. I was using const in tests to check negative flows. I’ll update my code to follow the existing convention.
There was a problem hiding this comment.
Fixed- no const errors in any file
pkg/ingressCache/pathTrie.go
Outdated
| func newPathTree() *pathTree { | ||
| return &pathTree{*trie.NewPathTrie()} | ||
| } |
There was a problem hiding this comment.
The constructor should be after the struct definition (convention, not error).
// Definition
type MyStruct struct {
...
}
// Constructor
func NewMyStruct() *MyStruct {
...
}
// Methods
func (*MyStruct) Get() {
...
}
pkg/ingressCache/pathTrie.go
Outdated
| } | ||
|
|
||
| if function == "" { | ||
| return fmt.Errorf(emptyFunctionNameError) |
There was a problem hiding this comment.
Same for the other errors.
pkg/ingressCache/ingressHostTree.go
Outdated
| @@ -0,0 +1,7 @@ | |||
| package ingressCache | |||
There was a problem hiding this comment.
as the action says - rename package (and folder) to ingresscache
pkg/ingressCache/pathTrie.go
Outdated
| return fmt.Errorf(emptyFunctionNameError) | ||
| } | ||
|
|
||
| // get the exact path value in order to avoid creating a new path if it does not exist |
There was a problem hiding this comment.
avoid creating a new path if it does not exist
?
You do create it if it does not exist.
pkg/ingressCache/pathTrie.go
Outdated
| func excludeElemFromSlice(slice []string, elem string) []string { | ||
| var output []string | ||
| for _, v := range slice { | ||
| if v == elem { | ||
| continue | ||
| } | ||
| output = append(output, v) | ||
| } | ||
| return output | ||
| } |
There was a problem hiding this comment.
- Move to
common/helpers.go - another suggestion here: https://stackoverflow.com/a/37335777
There was a problem hiding this comment.
- I will keep the function in this file because it's gonna be removed anyway as part of this improvement
- I refactored the function to be as efficient as possible here
pkg/ingressCache/pathTrie.go
Outdated
| // TODO - add log that function does not exist in the path | ||
| return fmt.Errorf("%s, function:%s, path:%s", functionNotExistsError, function, path) |
There was a problem hiding this comment.
is this really an error?
I would say the "Deletion" succeeded (idempotency - remember)?
There was a problem hiding this comment.
Well, this won’t affect idempotency, but on second thought, I agree that this could be logged instead and shouldn’t return an error.
pkg/ingressCache/pathTrie.go
Outdated
| // get retrieve the closest prefix matching the path and returns the associated functions | ||
| func (p *pathTree) get(path string) ([]string, error) { | ||
| var walkPathResult interface{} | ||
| err := p.WalkPath(path, func(path string, value interface{}) error { |
There was a problem hiding this comment.
| err := p.WalkPath(path, func(path string, value interface{}) error { | |
| if err := p.WalkPath(path, func(path string, value interface{}) error { | |
| ... | |
| }); err != nil { | |
| ... | |
| } |
pkg/ingressCache/pathTrie.go
Outdated
| func existsInSlice(slice []string, elem string) bool { | ||
| for _, v := range slice { | ||
| if v == elem { | ||
| return true | ||
| } | ||
| } | ||
| return false | ||
| } |
There was a problem hiding this comment.
no need if you use slices.contains
e79e841 to
08adfdb
Compare
b9d7f34 to
a356561
Compare
1de4cab to
b4eaa16
Compare
1c930b0 to
d1741bc
Compare
rokatyy
left a comment
There was a problem hiding this comment.
General Comments and Suggestions Regarding Tests:
-
Avoid using constants for dynamic values:
It’s generally better to avoid hardcoding constants for things like function names or other values that are logically variable. Using variables instead makes tests clearer and easier to maintain, especially if those values might change or differ between test cases. -
Avoid mocking if possible:
In this context, since the object being tested is abstract or a simple interface, the need for mocking is limited. Using real or simple concrete instances can improve test clarity and reduce complexity. Mocking should be reserved for dependencies that are expensive, have side effects, or are external. -
ensure tests are isolated and independent:
Tests should not rely on shared state or cross dependencies. This is important to guarantee that tests can run reliably in parallel or in any order without interference. Proper setup and teardown are key to avoiding flakiness and making tests robust.
pkg/ingressCache/ingressCache.go
Outdated
| @@ -0,0 +1,124 @@ | |||
| /* | |||
| Copyright 2019 Iguazio Systems Ltd. | |||
There was a problem hiding this comment.
| Copyright 2019 Iguazio Systems Ltd. | |
| Copyright 2025 Iguazio Systems Ltd. |
| urlTree, exists := ic.syncMap.Load(host) | ||
| if !exists { | ||
| urlTree = NewSafeTrie() | ||
| ic.syncMap.Store(host, urlTree) | ||
| } | ||
|
|
There was a problem hiding this comment.
use ic.syncMap.LoadOrStore() instead
There was a problem hiding this comment.
To avoid using syncMap.Delete on every failure, I chose to keep the result from syncMap.Load and only store it at the end of the positive flow.
Fixed here - CR comment - improve usage of syncMap and slice
pkg/ingressCache/ingressCache.go
Outdated
|
|
||
| type IngressHostsTree interface { | ||
| SetFunctionName(path string, function string) error // will overwrite existing values if exists | ||
| DeleteFunctionName(path string, function string) error | ||
| GetFunctionName(path string) ([]string, error) | ||
| IsEmpty() bool | ||
| } | ||
|
|
||
| type IngressHostCache interface { | ||
| Set(host string, path string, function string) error // will overwrite existing values if exists | ||
| Delete(host string, path string, function string) error | ||
| Get(host string, path string) ([]string, error) | ||
| } |
There was a problem hiding this comment.
Top lvl entity up
| type IngressHostsTree interface { | |
| SetFunctionName(path string, function string) error // will overwrite existing values if exists | |
| DeleteFunctionName(path string, function string) error | |
| GetFunctionName(path string) ([]string, error) | |
| IsEmpty() bool | |
| } | |
| type IngressHostCache interface { | |
| Set(host string, path string, function string) error // will overwrite existing values if exists | |
| Delete(host string, path string, function string) error | |
| Get(host string, path string) ([]string, error) | |
| } | |
| type IngressHostCache interface { | |
| Set(host string, path string, function string) error // will overwrite existing values if exists | |
| Delete(host string, path string, function string) error | |
| Get(host string, path string) ([]string, error) | |
| } | |
| type IngressHostsTree interface { | |
| SetFunctionName(path string, function string) error // will overwrite existing values if exists | |
| DeleteFunctionName(path string, function string) error | |
| GetFunctionName(path string) ([]string, error) | |
| IsEmpty() bool | |
| } |
There was a problem hiding this comment.
pkg/ingressCache/ingressCache.go
Outdated
| ic.logger.DebugWith("cache delete: host removed as it is empty", | ||
| "host", host) | ||
| ic.syncMap.Delete(host) |
There was a problem hiding this comment.
| ic.logger.DebugWith("cache delete: host removed as it is empty", | |
| "host", host) | |
| ic.syncMap.Delete(host) | |
| ic.syncMap.Delete(host) | |
| ic.logger.DebugWith("cache delete: host removed as it is empty", | |
| "host", host) |
pkg/ingressCache/ingressCache.go
Outdated
| SetFunctionName(path string, function string) error // will overwrite existing values if exists | ||
| DeleteFunctionName(path string, function string) error | ||
| GetFunctionName(path string) ([]string, error) |
There was a problem hiding this comment.
maybe remove name from SetFunctionName and others? So when we move to typed trie values (not [string]), it will return either canaryFunction or singleFunction
There was a problem hiding this comment.
I believe we should keep using functionName — it's a commonly used convention and clearly describes the value the trie holds.
But as @TomerShor recommended here, I will change the GetFunctionName specifically.
There was a problem hiding this comment.
@weilerN I think we will need to rename when code is moved from using []string to structs, which will be with function prefix in the name (e.g canaryFunction, singleFunction). Though naming can be reconsidered in the following PR
| expectedResult []string | ||
| shouldFail bool | ||
| errorMessage string | ||
| testMocks mockFunction |
There was a problem hiding this comment.
what is the reason of using mock here? Won't proper object creation be the better fit here?
There was a problem hiding this comment.
Discuses F2F, fixed here- CR comment - refactor UTs (remove mocks, consts and setupTest)
pkg/ingressCache/mock/safeTrie.go
Outdated
There was a problem hiding this comment.
I don't really understand why mock for safeTrie is needed in general as it's in-memory data structure with no external dependencies, so using it directly in tests is fast, reliable, and easy to control.
There was a problem hiding this comment.
Discussed F2F, removed here - CR comment - refactor UTs (remove mocks, consts and setupTest)
pkg/ingressCache/safeTrie_test.go
Outdated
| "github.com/stretchr/testify/suite" | ||
| ) | ||
|
|
||
| type SafeTrieTest struct { |
There was a problem hiding this comment.
| type SafeTrieTest struct { | |
| type SafeTrieTestSuite struct { |
pkg/ingressCache/safeTrie_test.go
Outdated
|
|
||
| type SafeTrieTest struct { | ||
| suite.Suite | ||
| safeTrie *SafeTrie |
There was a problem hiding this comment.
1stly, That's not a best practice to have a shared object, which is used across all the tests, in suite. 2ndly, it fully blocks tests from parallel execution even though these are unit tests and can be executed in parallel. Please create a new safeTrie in every tests to avoid unnecessary complexity
There was a problem hiding this comment.
There was a problem hiding this comment.
pkg/ingressCache/safeTrie_test.go
Outdated
| testFunctionName := "test-function" | ||
| testFunctionName2 := "test-function-2" | ||
| testFunctionPath := "/path/to/function" | ||
| testFunctionPathNested := "/path/to/function/nested" | ||
| testFunctionPathEndsWithSlash := "/path/to/function/" | ||
| testFunctionPathWithDots := "/path/./to/./function/" | ||
| testFunctionPathUpperCase := "/PATH/TO/function" | ||
| testFunctionNameUpperCase := "test-FUNCTION" | ||
| testAPIPath := "/api/v1/user-data/123" |
There was a problem hiding this comment.
please remove this and pass raw strings to test cases below. Also, testFunctionName1 and testFunctionName2 are consts which leads us to the reason why one should not use consts in tests especially for names if this is not really necessary.
There was a problem hiding this comment.
@rokatyy I initially defined these variables within each test, but the linter raised the following error:

link
That leaves us with a few options:
- Add
//goconstcomments to suppress the warning per test (which isn't ideal), or disable this check here - Use constants instead.
- Adjust the linter configuration to ignore this check altogether.
- In each test create a unique dummy variable names. This walk around the linter.
What approach do you prefer?
There was a problem hiding this comment.
@weilerN use local variables, no need in global once. And these variable are not required as they can be passed as strings to test cases.
There was a problem hiding this comment.
Fixed here - CR comment - refactor UTs (remove mocks, consts and setupTest)
@rokatyy As we discussed yesterday - the linter didn't fail the test when using strings inside the tests, so no need to adjust the goconst
pkg/ingressCache/ingressCache.go
Outdated
| if !exists { | ||
| ic.syncMap.Delete(host) | ||
| } | ||
| return errors.Wrap(err, "cache set failed") |
There was a problem hiding this comment.
Even more explicit:
| return errors.Wrap(err, "cache set failed") | |
| return errors.Wrap(err, "Failed to set function name in the ingress host tree") |
The caller of Set will return an error Failed to set function in cache.
pkg/ingressCache/safeTrie_test.go
Outdated
| func (suite *SafeTrieTest) SetupTest() { | ||
| suite.safeTrie = NewSafeTrie() | ||
| } | ||
|
|
||
| func (suite *SafeTrieTest) SetupSubTest(safeTrieState []safeTrieFunctionArgs) { |
There was a problem hiding this comment.
when you have a suite, you can have SetupSuite / TeardownSuite for things that relate to all tests, and SetupTest / TeardownTest for specific tests stuff.
There was a problem hiding this comment.
Good point, I will take that in mind.
Fixed here - CR comment - refactor UTs (remove mocks, consts and setupTest)
b40b97f to
e18346a
Compare
TomerShor
left a comment
There was a problem hiding this comment.
Logic looks much better!
Added some comments regarding tests
pkg/ingresscache/safetrie_test.go
Outdated
| return resultMap, nil | ||
| } | ||
|
|
||
| func (suite *SafeTrieTestSuite) generateLotsOfPathsAndFunctions(num int) []safeTrieFunctionArgs { |
There was a problem hiding this comment.
| func (suite *SafeTrieTestSuite) generateLotsOfPathsAndFunctions(num int) []safeTrieFunctionArgs { | |
| func (suite *SafeTrieTestSuite) generatePathsAndFunctions(num int) []safeTrieFunctionArgs { |
pkg/ingresscache/safetrie_test.go
Outdated
| }, | ||
| } { | ||
| suite.Run(testCase.name, func() { | ||
| suite.SetupSubTest(nil) |
There was a problem hiding this comment.
From testify's docs, you don't really need to call SetupSubTest manually, as suite.Run calls it anyway. However - it calls it without any arguments.
What you need here is a SetupTest function that only initializes suite.safeTrie.
There was a problem hiding this comment.
As suggested, I removed the SetupSubTestand moved into a privategetTestSafeTrie` function.
CR comment - refactor UTs (remove mocks, consts and setupTest)
pkg/ingresscache/safetrie_test.go
Outdated
| suite.SetupSubTest([]safeTrieFunctionArgs{ | ||
| {testPathRoot, testFunctionName}, | ||
| {testPath1, testFunctionName1}, | ||
| {testPath2, testFunctionName2}, | ||
| {testFunctionPathWithDots, testFunctionName1}, | ||
| {testFunctionPathWithDoubleSlash, testFunctionName1}, | ||
| {testPathWithMultipleFunctions, testFunctionName1}, | ||
| {testPathWithMultipleFunctions, testFunctionName2}, | ||
| }) |
There was a problem hiding this comment.
As my previous comment, this is not the correct usage of SetupSubTest.
You can create a helper private function that populates suite.safeTrie with data, and call it here.
There was a problem hiding this comment.
As suggested, I removed the SetupSubTest and refactored to privategetTestSafeTrie function.
CR comment - refactor UTs (remove mocks, consts and setupTest)
| func (suite *IngressCacheTestSuite) SetupSubTest(testHost string, testMocks mockFunction) { | ||
| suite.ingressCache = NewIngressCache(suite.logger) | ||
|
|
||
| if m := testMocks(); m != nil { | ||
| // mock==nil is used to check for non-existing host | ||
| suite.ingressCache.syncMap.Store(testHost, m) | ||
| } | ||
| } |
There was a problem hiding this comment.
See the comments in safetrie_test.go, same applies here
There was a problem hiding this comment.
|
|
||
| resultFunctionNames, err := suite.ingressCache.Get(testCase.args.host, testCase.args.path) | ||
| if testCase.shouldFail { | ||
| suite.Require().NotNil(err) |
There was a problem hiding this comment.
| suite.Require().NotNil(err) | |
| suite.Require().Error(err) |
There was a problem hiding this comment.
| resultFunctionNames, err := suite.ingressCache.Get(testCase.args.host, testCase.args.path) | ||
| if testCase.shouldFail { | ||
| suite.Require().NotNil(err) | ||
| suite.Require().Contains(err.Error(), testCase.errorMessage) |
There was a problem hiding this comment.
| suite.Require().Contains(err.Error(), testCase.errorMessage) | |
| suite.Require().ErrorContains(err, testCase.errorMessage) |
There was a problem hiding this comment.
| suite.Require().NotNil(err) | ||
| suite.Require().Contains(err.Error(), testCase.errorMessage) |
There was a problem hiding this comment.
| suite.Require().NotNil(err) | ||
| suite.Require().Contains(err.Error(), testCase.errorMessage) |
There was a problem hiding this comment.
| }, { | ||
| name: "Get multiple functionName", | ||
| args: testIngressCacheArgs{"example.com", "/test/path", ""}, | ||
| expectedResult: []string{"test-function-name-1", "test-function-name-2"}, | ||
| initialState: []ingressCacheTestInitialState{ | ||
| {"example.com", "/test/path", "test-function-name-1"}, | ||
| {"example.com", "/test/path", "test-function-name-2"}, |
There was a problem hiding this comment.
how is this different from Get two functionName?
There was a problem hiding this comment.
Good catch, both test are the same. It was a leftover before the refactoring. Removing duplicate.
There was a problem hiding this comment.
| testLogger, err := nucliozap.NewNuclioZapTest("test") | ||
| suite.Require().NoError(err) |
There was a problem hiding this comment.
This IS something that can be in the SetupSuite, because the logger can be shared between tests.
There was a problem hiding this comment.
rokatyy
left a comment
There was a problem hiding this comment.
Much better now, have couple of comments and suggestions
| shouldFail: true, | ||
| errorMessage: "host does not exist", |
There was a problem hiding this comment.
why not having just errorMessage param? If it's not empty, then shouldFail
There was a problem hiding this comment.
I followed the testing convention commonly used in many Nuclio tests — for example:
I'll rename shouldFail to expectError, but aside from that, I'd prefer to keep the test structure as is.
There was a problem hiding this comment.
| { | ||
| name: "Set new host", | ||
| args: testIngressCacheArgs{"example.com", "/test/path", "test-function-name-1"}, | ||
| expectedResult: map[string]map[string][]string{ | ||
| "example.com": {"/test/path": {"test-function-name-1"}}, | ||
| }, | ||
| }, { | ||
| name: "Set another functionName for existing host", | ||
| args: testIngressCacheArgs{"example.com", "/test/path", "test-function-name-2"}, | ||
| initialState: []ingressCacheTestInitialState{ | ||
| {"example.com", "/test/path", "test-function-name-1"}, | ||
| }, | ||
| expectedResult: map[string]map[string][]string{ | ||
| "example.com": {"/test/path": {"test-function-name-1", "test-function-name-2"}}, | ||
| }, | ||
| }, { |
There was a problem hiding this comment.
so test case's order matters here? If somebody adds a test in between of those, it will break them?
There was a problem hiding this comment.
Each test initializes its own testIngressCache based on testCase.initialState (link),
ensuring full test independence — execution order doesn't affect results.
In this specific case, the test name was a bit misleading, so I’m renaming it for clarity.
There was a problem hiding this comment.
pkg/ingresscache/safetrie.go
Outdated
|
|
||
| pathFunctionNames, ok := pathValue.([]string) | ||
| if !ok { | ||
| return errors.Errorf("value is not a []string, got: %T", pathValue) |
There was a problem hiding this comment.
| return errors.Errorf("value is not a []string, got: %T", pathValue) | |
| return errors.Errorf("path value should be []string, got %T", pathValue) |
to align with the error in Delete function
There was a problem hiding this comment.
| }, { | ||
| name: "set different paths and different functions", | ||
| args: []safeTrieFunctionArgs{ | ||
| { | ||
| path: "/path/to/function", | ||
| function: "test-function", | ||
| }, { | ||
| path: "/another/path/to/function/", | ||
| function: "test-function2", | ||
| }, |
There was a problem hiding this comment.
what will happen in this case? and how it aligns with ingress behaviour in this case
| }, { | |
| name: "set different paths and different functions", | |
| args: []safeTrieFunctionArgs{ | |
| { | |
| path: "/path/to/function", | |
| function: "test-function", | |
| }, { | |
| path: "/another/path/to/function/", | |
| function: "test-function2", | |
| }, | |
| }, { | |
| name: "set different paths and different functions", | |
| args: []safeTrieFunctionArgs{ | |
| { | |
| path: "/path/to/function", | |
| function: "test-function", | |
| }, { | |
| path: "/path/to/function/", | |
| function: "test-function2", | |
| }, |
There was a problem hiding this comment.
There are two separate points here:
-
It's not possible to define two ingresses that differ only by a trailing /. For example, if there's already an ingress with the path /path/to/function, trying to add another one with /path/to/function/ will fail during the Dashboard's ingress validation.
This happens because the validation logic normalizes the URL before checking for duplicates — as seen here. -
The goal of this test is to verify behavior with two distinct paths, not nested ones — so I’d prefer to keep it as is.
There was a problem hiding this comment.
yeah, I didn't mean to modify the test, just was wondering how it works in this case. Then we are safe 👍
| { | ||
| path: "///path/to/function", | ||
| function: "test-function", | ||
| }, |
There was a problem hiding this comment.
is it something that's possible to have in our setup with ingresses?
There was a problem hiding this comment.
Similar to regex — it's configurable, but not actually usable.
From the dashboard:

From the ingress yaml:
$ k -n default-tenant get ingress nuclio-test6 -oyaml | grep -A 10 rules
rules:
- host: my-multi-function.default-tenant.app.vmdev5.lab.iguazeng.com
http:
paths:
- backend:
service:
name: nuclio-test6
port:
name: http
path: ///path/to/function
pathType: ImplementationSpecific
But when trying to trigger this function from outside- it doesn't reach the DLX.
So even though configuring it is technically possible (despite being a bad practice and not a viable path), I'd prefer to keep the coverage in place.
| } | ||
| for _, testCase := range []struct { | ||
| name string | ||
| arg string |
There was a problem hiding this comment.
| arg string | |
| path string |
| type IngressHostsTree interface { | ||
| // SetFunctionName sets a function for a given path. Will overwrite existing values if the path already exists | ||
| SetFunctionName(path string, function string) error | ||
| // DeleteFunctionName removes the function from the given path and deletes the deepest suffix used only by that function; does nothing if the path or function doesn't exist. | ||
| DeleteFunctionName(path string, function string) error | ||
| // GetFunctionNames retrieves the best matching function names for a given path based on longest prefix match | ||
| GetFunctionNames(path string) ([]string, error) | ||
| // IsEmpty checks if the tree is empty | ||
| IsEmpty() bool | ||
| } |
There was a problem hiding this comment.
nitpicking - add new strings between methods for better readability
There was a problem hiding this comment.
@rokatyy What do you mean new strings ?
There was a problem hiding this comment.
…ewlines in types.go


Motivation
Introduce efficient host+path resolution for DLX with O(logN) complexity, where
N = len(strings.Split(path, "/")).For full technical context, see HLD
Description
This PR lays the foundation for future ingress cache functionality, as described in the JIRA- https://iguazio.atlassian.net/browse/NUC-423 .
Key deliverables include:
safeTrie) for efficient path resolutionAffected Areas
This is a standalone implementation with no impact on existing functionality.
It introduces isolated, internal components to support upcoming features.
Testing
pathTrieinterface implementationsChanges Made
ingressCache.go- implementsIngressHostCache.safeTrie.go- implementsIngressHostsTree.Additional Notes