-
Notifications
You must be signed in to change notification settings - Fork 239
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Refactor of the value cache #2151
base: main
Are you sure you want to change the base?
Conversation
e64d648
to
924df73
Compare
1a1f1a9
to
0a65e49
Compare
0a65e49
to
00bd528
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you for this change! I suppose the simplification is a step in the right direct. I find both the new and the old code hard to understand though... Maybe it's just me but it feels too convoluted. I tried to review and to leave useful comments, but I'll need to review it again and hopefully I'll understand it better.
continue | ||
// Remove the component exports from the scope. | ||
for _, id := range cleanupIds { | ||
err := cleanup(vc.scope.Variables, id, 0) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What does the 0 mean? It might be nice to have a comment above func cleanup(
to mention it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this is the index that refers to the first part of the componentID (see the answer of the comment below). I will add some comments
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can you instead have
func cleanup(m map[string]any, id ComponentID) error {
return cleanup_from_index(m, id, 0)
}
that could make it easier to read. Both me and Paulin seem to have found this somewhat confusing.
Thanks for the review, it's definitely not just you, it's hard to simplify it because it must build the scope tree which is quite a complex object to make + the fact that it must handle the args for the modules and the "parent" context. I also don't want to start a massive refactoring of everything. I will work on the review feedback and see whether I can simplify it a bit more. Maybe further refactoring PRs can follow to simplify other areas + add doc + add tests to consolidate the logic and simplify the code |
@mattdurham @ptodev Thank you both for the review, I answered your comments and made some modifications. Please take another look when you have time and feel free to add more comments or unresolved existing ones. We can also discuss it over a call if you have concerns about the approach or find some parts confusing and would like to talk about alternatives |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have a few questions
// TODO: Refactor this function to avoid using an error as input solely for shadowing purposes. | ||
// This design choice limits extensibility and makes it harder to modify or add functionality. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What do you mean that this function is using error "solely for shadowing purposes"?
I can see that it just logs the error as part of post evaluation steps, doesn't seem too odd for a function called postEvaluate
.
err2 := l.cache.CacheExports(c.ID(), c.Exports()) | ||
if err2 != nil { | ||
level.Error(logger).Log("msg", "failed to cache exports after evaluation", "err", err2) | ||
// Don't mask the previous evaluation error. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we join errors here so we don't lose anything?
keyMap := make(map[string]any) | ||
keyMap["value"] = value | ||
vc.moduleArguments[key] = keyMap |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm, why do we have a map of maps if the second layer of maps always has only one key "value"? Why not a map of keys to values?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
because to refer to an argument value in the config file you need to write "argument.LABEL.value". To resolve it, it must have the path ["argument"][LABEL]["value"]
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah gotcha. Yeah now I remember this syntax. Would things break if we made both foo.bar.value
and foo.bar
work? It could be UX improvement 🤔
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
that would be nice but it's tricky to do because you cant have two values for the same key in the map + you would need to handle the weird cases where the user names its argument "value".
v, exist := vc.moduleArguments[key] | ||
return v, exist |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
v, exist := vc.moduleArguments[key] | |
return v, exist | |
return vc.moduleArguments[key] |
// Remove the component exports from the scope. | ||
for _, id := range cleanupIds { | ||
// Start with the index "0". It refers to the first part of the componentID and it's used for recursion. | ||
err := cleanup(vc.scope.Variables, id, 0) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we try to make this a bit more readable - I find the index
argument set to 0
here pretty hard to follow and I think we can probably find a way to simplify.
continue | ||
// Remove the component exports from the scope. | ||
for _, id := range cleanupIds { | ||
err := cleanup(vc.scope.Variables, id, 0) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can you instead have
func cleanup(m map[string]any, id ComponentID) error {
return cleanup_from_index(m, id, 0)
}
that could make it easier to read. Both me and Paulin seem to have found this somewhat confusing.
nodeID := id.String() | ||
vc.components[nodeID] = id | ||
variables := vc.scope.Variables | ||
// Build nested maps. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you help me understand why do we choose to take on the complexity of the nested maps instead of using a flattened map with a concatenated component id parts, like "foo.bar.baz" instead of {foo: { bar: baz}}
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
because that's the structure that we need to give to the Alloy syntax to evaluate the expression. It's also the structure that we use for modules to propagate the context. The current implementation builds it on every evaluation whereas this new implementation stores the values directly in it
What is the "value cache"
The value cache (value_cache.go) stores arguments and exports of the components in the graph to build scope. The scope is used by the Alloy syntax on evaluation to resolve the expressions on the arguments of the components.
For example, when you have
forward_to = [prometheus.remote_write.default.receiver]
, the Alloy syntax will check in the scope to find the receiver which corresponds to "prometheus.remote_write.default.receiver".Why we should refactor this part of the code
About the solution
I first considered merging the parent scope with the built scope but merging the nested maps brings more complexity to the value cache which is already hard to follow and this feels very hacky to merge the parent scope with the child just because the Alloy syntax does not resolve it the needed way.
To avoid this I came up with a different approach:
Tests
I added more unit tests + error handling. I also manually tested it with the foreach feature that's currently in progress + did some manual tests.