Skip to content

[Framework] Add ingress cache#70

Merged
TomerShor merged 18 commits intov3io:developmentfrom
weilerN:nuc-420-add-ingress-cache
Jun 22, 2025
Merged

[Framework] Add ingress cache#70
TomerShor merged 18 commits intov3io:developmentfrom
weilerN:nuc-420-add-ingress-cache

Conversation

@weilerN
Copy link
Collaborator

@weilerN weilerN commented Jun 11, 2025

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:

  • Initial cache DB structure
  • Thread-safe trie (safeTrie) for efficient path resolution
  • Unit tests validating DB operations and trie logic

Affected Areas

This is a standalone implementation with no impact on existing functionality.
It introduces isolated, internal components to support upcoming features.

Testing

  • Unit tests: cover all core cache logic and pathTrie interface implementations
  • Integration tests: to be addressed in a dedicated follow-up task

Changes Made

  • Added ingressCache.go - implements IngressHostCache.
  • Added safeTrie.go - implements IngressHostsTree.

Additional Notes

  • Additional in-memory optimizations for safeTrie are planned separately to keep this PR focused and scoped.

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.

Initial review, mostly related to our conventions.
Main things are:

  • File name are all lowercase, even for multiple words.
  • We use the errors package over fmt.Errorf etc.
  • When returning errors, think about future Noam trying to debug this code - what will be his experience?
  • Utilities have a dedicated space in the common package.
  • 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.

@@ -0,0 +1,7 @@
package ingressCache

type ingressHostsTree 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 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.

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 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.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Changed into a public interface - here

Comment on lines 8 to 14
const (
emptyPathError = "path is empty"
emptyFunctionNameError = "function name is empty"
walkPathResultError = "value is not a []string"
functionNotExistsError = "function does not exist in path"
)
Copy link
Collaborator

Choose a reason for hiding this comment

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

define error type consts instead of strings:

const (
    emptyPathError error = errors.New("Path is empty")
    ...
)

Copy link
Collaborator

Choose a reason for hiding this comment

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

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>).

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Thanks for the explanation. I was using const in tests to check negative flows. I’ll update my code to follow the existing convention.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Fixed- no const errors in any file

Comment on lines 16 to 18
func newPathTree() *pathTree {
return &pathTree{*trie.NewPathTrie()}
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

The constructor should be after the struct definition (convention, not error).

// Definition
type MyStruct struct {
    ...
}

// Constructor
func NewMyStruct() *MyStruct {
    ...
}

// Methods
func (*MyStruct) Get() {
    ...
}

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Fixed here

}

if function == "" {
return fmt.Errorf(emptyFunctionNameError)
Copy link
Collaborator

Choose a reason for hiding this comment

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

Same for the other errors.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Fixed here

@@ -0,0 +1,7 @@
package ingressCache
Copy link
Collaborator

Choose a reason for hiding this comment

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

as the action says - rename package (and folder) to ingresscache

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

fixed here

return fmt.Errorf(emptyFunctionNameError)
}

// get the exact path value in order to avoid creating a new path if it does not exist
Copy link
Collaborator

Choose a reason for hiding this comment

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

avoid creating a new path if it does not exist

?
You do create it if it does not exist.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

right, bad typo

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Fixed here

Comment on lines 113 to 124
func excludeElemFromSlice(slice []string, elem string) []string {
var output []string
for _, v := range slice {
if v == elem {
continue
}
output = append(output, v)
}
return output
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

  1. Move to common/helpers.go
  2. another suggestion here: https://stackoverflow.com/a/37335777

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

  1. I will keep the function in this file because it's gonna be removed anyway as part of this improvement
  2. I refactored the function to be as efficient as possible here

Comment on lines 72 to 73
// TODO - add log that function does not exist in the path
return fmt.Errorf("%s, function:%s, path:%s", functionNotExistsError, function, path)
Copy link
Collaborator

Choose a reason for hiding this comment

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

is this really an error?
I would say the "Deletion" succeeded (idempotency - remember)?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Well, this won’t affect idempotency, but on second thought, I agree that this could be logged instead and shouldn’t return an error.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Fixed here

// 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 {
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
err := p.WalkPath(path, func(path string, value interface{}) error {
if err := p.WalkPath(path, func(path string, value interface{}) error {
...
}); 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.

Fixed here

Comment on lines 104 to 103
func existsInSlice(slice []string, elem string) bool {
for _, v := range slice {
if v == elem {
return true
}
}
return false
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

no need if you use slices.contains

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Removed

@weilerN weilerN marked this pull request as ready for review June 12, 2025 08:06
@weilerN weilerN marked this pull request as draft June 12, 2025 08:07
@weilerN weilerN force-pushed the nuc-420-add-ingress-cache branch 4 times, most recently from e79e841 to 08adfdb Compare June 15, 2025 11:42
@weilerN weilerN force-pushed the nuc-420-add-ingress-cache branch from b9d7f34 to a356561 Compare June 15, 2025 14:11
@weilerN weilerN force-pushed the nuc-420-add-ingress-cache branch from 1de4cab to b4eaa16 Compare June 16, 2025 09:53
@weilerN weilerN force-pushed the nuc-420-add-ingress-cache branch from 1c930b0 to d1741bc Compare June 16, 2025 14:36
@weilerN weilerN requested a review from TomerShor June 16, 2025 15:03
@weilerN weilerN marked this pull request as ready for review June 16, 2025 15:03
@TomerShor TomerShor changed the title Nuc 420 add ingress cache [Framework] Add ingress cache Jun 16, 2025
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.

General Comments and Suggestions Regarding Tests:

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

  2. 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.

  3. 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.

@@ -0,0 +1,124 @@
/*
Copyright 2019 Iguazio Systems Ltd.
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
Copyright 2019 Iguazio Systems Ltd.
Copyright 2025 Iguazio Systems Ltd.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Fixed here - CR comment - Copyright

Comment on lines 56 to 61
urlTree, exists := ic.syncMap.Load(host)
if !exists {
urlTree = NewSafeTrie()
ic.syncMap.Store(host, urlTree)
}

Copy link
Collaborator

Choose a reason for hiding this comment

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

use ic.syncMap.LoadOrStore() instead

Copy link
Collaborator Author

@weilerN weilerN Jun 17, 2025

Choose a reason for hiding this comment

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

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

Comment on lines 29 to 41

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)
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

Top lvl entity up

Suggested change
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
}

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 99 to 101
ic.logger.DebugWith("cache delete: host removed as it is empty",
"host", host)
ic.syncMap.Delete(host)
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
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)

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Fixed here

Comment on lines 31 to 33
SetFunctionName(path string, function string) error // will overwrite existing values if exists
DeleteFunctionName(path string, function string) error
GetFunctionName(path string) ([]string, error)
Copy link
Collaborator

Choose a reason for hiding this comment

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

maybe remove name from SetFunctionName and others? So when we move to typed trie values (not [string]), it will return either canaryFunction or singleFunction

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 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.

Copy link
Collaborator

Choose a reason for hiding this comment

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

@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
Copy link
Collaborator

Choose a reason for hiding this comment

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

what is the reason of using mock here? Won't proper object creation be the better fit here?

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

Choose a reason for hiding this comment

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

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.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

"github.com/stretchr/testify/suite"
)

type SafeTrieTest struct {
Copy link
Collaborator

@rokatyy rokatyy Jun 16, 2025

Choose a reason for hiding this comment

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

Suggested change
type SafeTrieTest struct {
type SafeTrieTestSuite struct {

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Done here


type SafeTrieTest struct {
suite.Suite
safeTrie *SafeTrie
Copy link
Collaborator

Choose a reason for hiding this comment

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

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

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 I'm aware that the unit tests run in parallel, and I've verified it prior to committing the code:
image
Each test creates is own safeTrie before starting - link.

But I agree with you that the safeTrie should be part of the test itself and not part of the Suite so I will move it.

Copy link
Collaborator

Choose a reason for hiding this comment

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

You just was lucky :)
image

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 54 to 62
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"
Copy link
Collaborator

Choose a reason for hiding this comment

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

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.

Copy link
Collaborator Author

@weilerN weilerN Jun 17, 2025

Choose a reason for hiding this comment

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

@rokatyy I initially defined these variables within each test, but the linter raised the following error:
image
link

That leaves us with a few options:

  • Add //goconst comments 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?

Copy link
Collaborator

@rokatyy rokatyy Jun 17, 2025

Choose a reason for hiding this comment

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

@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.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

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

if !exists {
ic.syncMap.Delete(host)
}
return errors.Wrap(err, "cache set failed")
Copy link
Collaborator

Choose a reason for hiding this comment

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

Even more explicit:

Suggested change
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.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Fixed here

Comment on lines 39 to 43
func (suite *SafeTrieTest) SetupTest() {
suite.safeTrie = NewSafeTrie()
}

func (suite *SafeTrieTest) SetupSubTest(safeTrieState []safeTrieFunctionArgs) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

when you have a suite, you can have SetupSuite / TeardownSuite for things that relate to all tests, and SetupTest / TeardownTest for specific tests stuff.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Good point, I will take that in mind.
Fixed here - CR comment - refactor UTs (remove mocks, consts and setupTest)

@weilerN weilerN force-pushed the nuc-420-add-ingress-cache branch from b40b97f to e18346a Compare June 17, 2025 13:31
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.

Logic looks much better!
Added some comments regarding tests

return resultMap, nil
}

func (suite *SafeTrieTestSuite) generateLotsOfPathsAndFunctions(num int) []safeTrieFunctionArgs {
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
func (suite *SafeTrieTestSuite) generateLotsOfPathsAndFunctions(num int) []safeTrieFunctionArgs {
func (suite *SafeTrieTestSuite) generatePathsAndFunctions(num int) []safeTrieFunctionArgs {

Copy link
Collaborator Author

@weilerN weilerN Jun 19, 2025

Choose a reason for hiding this comment

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

Fixed here

},
} {
suite.Run(testCase.name, func() {
suite.SetupSubTest(nil)
Copy link
Collaborator

Choose a reason for hiding this comment

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

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.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

As suggested, I removed the SetupSubTestand moved into a privategetTestSafeTrie` function.
CR comment - refactor UTs (remove mocks, consts and setupTest)

Comment on lines 267 to 275
suite.SetupSubTest([]safeTrieFunctionArgs{
{testPathRoot, testFunctionName},
{testPath1, testFunctionName1},
{testPath2, testFunctionName2},
{testFunctionPathWithDots, testFunctionName1},
{testFunctionPathWithDoubleSlash, testFunctionName1},
{testPathWithMultipleFunctions, testFunctionName1},
{testPathWithMultipleFunctions, testFunctionName2},
})
Copy link
Collaborator

Choose a reason for hiding this comment

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

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.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

As suggested, I removed the SetupSubTest and refactored to privategetTestSafeTrie function.
CR comment - refactor UTs (remove mocks, consts and setupTest)

Comment on lines 64 to 71
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)
}
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

See the comments in safetrie_test.go, same applies here

Copy link
Collaborator Author

Choose a reason for hiding this comment

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


resultFunctionNames, err := suite.ingressCache.Get(testCase.args.host, testCase.args.path)
if testCase.shouldFail {
suite.Require().NotNil(err)
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
suite.Require().NotNil(err)
suite.Require().Error(err)

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

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)
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
suite.Require().Contains(err.Error(), testCase.errorMessage)
suite.Require().ErrorContains(err, testCase.errorMessage)

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 165 to 166
suite.Require().NotNil(err)
suite.Require().Contains(err.Error(), testCase.errorMessage)
Copy link
Collaborator

Choose a reason for hiding this comment

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

same here

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 253 to 254
suite.Require().NotNil(err)
suite.Require().Contains(err.Error(), testCase.errorMessage)
Copy link
Collaborator

Choose a reason for hiding this comment

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

and here

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 June 19, 2025 08:05
Comment on lines 67 to 73
}, {
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"},
Copy link
Collaborator

Choose a reason for hiding this comment

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

how is this different from Get two functionName?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Good catch, both test are the same. It was a leftover before the refactoring. Removing duplicate.

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 259 to 260
testLogger, err := nucliozap.NewNuclioZapTest("test")
suite.Require().NoError(err)
Copy link
Collaborator

Choose a reason for hiding this comment

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

This IS something that can be in the SetupSuite, because the logger can be shared between tests.

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.

Much better now, have couple of comments and suggestions

Comment on lines 85 to 86
shouldFail: true,
errorMessage: "host does not exist",
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 having just errorMessage param? If it's not empty, then shouldFail

Copy link
Collaborator Author

@weilerN weilerN Jun 19, 2025

Choose a reason for hiding this comment

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

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.

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 127 to 142
{
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"}},
},
}, {
Copy link
Collaborator

Choose a reason for hiding this comment

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

so test case's order matters here? If somebody adds a test in between of those, it will break them?

Copy link
Collaborator Author

@weilerN weilerN Jun 19, 2025

Choose a reason for hiding this comment

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

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.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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


pathFunctionNames, ok := pathValue.([]string)
if !ok {
return errors.Errorf("value is not a []string, got: %T", pathValue)
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
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

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 +95 to +104
}, {
name: "set different paths and different functions",
args: []safeTrieFunctionArgs{
{
path: "/path/to/function",
function: "test-function",
}, {
path: "/another/path/to/function/",
function: "test-function2",
},
Copy link
Collaborator

Choose a reason for hiding this comment

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

what will happen in this case? and how it aligns with ingress behaviour in this case

Suggested change
}, {
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",
},

Copy link
Collaborator Author

@weilerN weilerN Jun 19, 2025

Choose a reason for hiding this comment

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

There are two separate points here:

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

  2. 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.

Copy link
Collaborator

Choose a reason for hiding this comment

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

yeah, I didn't mean to modify the test, just was wondering how it works in this case. Then we are safe 👍

Comment on lines +135 to +138
{
path: "///path/to/function",
function: "test-function",
},
Copy link
Collaborator

Choose a reason for hiding this comment

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

is it something that's possible to have in our setup with ingresses?

Copy link
Collaborator Author

@weilerN weilerN Jun 19, 2025

Choose a reason for hiding this comment

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

Similar to regex — it's configurable, but not actually usable.
From the dashboard:
image

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
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
arg string
path string

Copy link
Collaborator

Choose a reason for hiding this comment

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

still here

Comment on lines 32 to 41
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
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

nitpicking - add new strings between methods for better readability

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 What do you mean new strings ?

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 June 19, 2025 14:02
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.

LGTM

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.

Great job 👏

@TomerShor TomerShor merged commit b73025e into v3io:development Jun 22, 2025
3 checks passed
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