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
feat: introduce features/settings client #2377
Conversation
473cbbe
to
247b934
Compare
Codecov ReportBase: 38.62% // Head: 38.56% // Decreases project coverage by
Additional details and impacted files@@ Coverage Diff @@
## master #2377 +/- ##
==========================================
- Coverage 38.62% 38.56% -0.06%
==========================================
Files 166 168 +2
Lines 36696 36740 +44
==========================================
- Hits 14173 14169 -4
- Misses 21608 21660 +52
+ Partials 915 911 -4
Help us with your feedback. Take ten seconds to tell us how you rate us. Have a feature suggestion? Share it here. ☔ View full report at Codecov. |
86fecdd
to
3291658
Compare
app/apphandlers/setup.go
Outdated
@@ -107,6 +108,15 @@ func rudderCoreBaseSetup() { | |||
|
|||
processor.RegisterAdminHandlers(&readonlyProcErrorDB) | |||
router.RegisterAdminHandlers(&readonlyRouterDB, &readonlyBatchRouterDB) | |||
|
|||
go func() { | |||
backendconfig.DefaultBackendConfig.WaitForConfig(context.Background()) |
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.
Waiting for config to send is a workaround since we don't have the workspaceID, until config is fetched.
There is a plan to remove this need in the v2
API.
6af7d0a
to
75dd590
Compare
75dd590
to
9c4c889
Compare
defer resp.Body.Close() | ||
|
||
b, err := io.ReadAll(resp.Body) |
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.
defer resp.Body.Close() | |
b, err := io.ReadAll(resp.Body) | |
b, err := io.ReadAll(resp.Body) | |
_ = resp.Body.Close() |
Optional of course but I wanted to share to gather your thoughts given that in this case it seems very easy to avoid deferring.
package features
import (
"testing"
)
func doNoDefer(t *int) {
func() {
*t++
}()
}
func doDefer(t *int) {
defer func() {
*t++
}()
}
func BenchmarkDefer(b *testing.B) {
b.Run("yes", func(b *testing.B) {
t := 0
for i := 0; i < b.N; i++ {
doDefer(&t)
}
})
b.Run("no", func(b *testing.B) {
t := 0
for i := 0; i < b.N; i++ {
doNoDefer(&t)
}
})
}
With go 1.19 it gives me:
goos: linux
goarch: amd64
pkg: github.com/rudderlabs/rudder-server/services/controlplane/features
cpu: 12th Gen Intel(R) Core(TM) i9-12900HK
BenchmarkDefer
BenchmarkDefer/yes
BenchmarkDefer/yes-20 588573512 2.031 ns/op
BenchmarkDefer/no
BenchmarkDefer/no-20 1000000000 0.2673 ns/op
PASS
Perhaps I'm not measuring it appropriately but it seems like some overhead is there 🤔
(I'm not saying do not use defer
, just think twice if it's not really needed. If you have multiple if/else
with returns then paying the overhead makes sense for the sake of keeping the complexity under control, in any case again totally optional).
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.
Interesting, I would not expect this to be that big.
However, the performance overhead of using defer in this case should be minor. Indeed, in this case, it can be easily avoided, but I like it as a consistent pattern across the code. I expect a defer close as soon as a successful request has returned.
For me, the readability/predictability ways the performance when IO is involved.
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.
Yes I agree with you. I'd say that having the defer makes it easier to follow if you're coming from a Go background.
It is a micro optimization on a piece of code that is not really giving us problems. Like yourself I think that readability should take precedence for the sake of code maintenability.
I wanted to share this anyway because sometimes I avoid using defer
myself for the performance penalty (when the code is quite simple and doesn't have returns). Plus sharing is caring so 😄
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.
LGTM 👍
I've added a comment to fix a typo and another one as a reminder, I see that you reacted with 👍 but forgot to add the error context.
a7f8bc8
to
555b82e
Compare
var ServerComponent = Component{ | ||
Name: "server", | ||
Features: []string{ | ||
"features", |
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.
how and when is this going to be populated?
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.
They are going to be populated manually for now.
When a new feature that the control plane needs to be aware of is developed, it will be added here.
Following your comment here #2377 (comment), I found this approach to be the most straightforward and cleanest for now.
The dynamic enabled/disable of features based on config is not supported yet, but it can be easily implemented if required by something like this:
func ServerComponentFromConfig(conf config.Config) Component {
comp := Component{
Name: "server",
Features: []string{},
}
if config.IsSet("feature.enabled") {
comp.Features = append(comp.Features, "features")
}
return comp
}
info/build.go
Outdated
var ( | ||
Version = "Not an official release. Get the latest release from the github repo." | ||
Major, Minor, Commit, BuildDate, BuiltBy, GitURL, Patch, EnterpriseToken string | ||
) |
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.
nobody seems to be using these
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 will remove the file and introduce it in another PR.
555b82e
to
3cf38ae
Compare
3cf38ae
to
e514477
Compare
c90401b
to
29b8b3c
Compare
Description
This PR is meant to introduce functionality that will inform the control plane about the features of rudder-server. Control-plane will make decisions based on what rudder-server supports or not.
More about this can be read in notion.
Registry
I was puzzled about the best way to capture features of multiple components as they are added in rudder-server, global vs injected
One approach is to DI the functionality in every component, which seemed overkill for two reasons:
I end up using a global registry approach, for now, similar to what we do for configs, stats and logs. This should be revised in the future, especially for logs and stats DI will be handy for testing.
Client
I wanted to decouple the client for Registry. Both for testing and extensibility. Registry in a simple in-memory store. While client just takes the Registry and sends the payload to the control plane.
Registry assumes all features will be added before a client call is made. We might want to revisit the decision if a feature should register based on configuration (feature flags). However, a system that waits until all registrations have occurred would require more complexity, so it is deferred until needed.
With the current implementations, features need to be registered in an
init
method.With this approach, I wanted to avoid:
Some of the rejected approaches above might need to revisit as our requirements, code pollution, and usage patterns become more advanced.
Identity
I have also introduced an abstraction called identity. I wanted this abstraction to be under control-plane-specific libraries and address the two different ways a rudder-server can be identified by control-plane workspace and namespace.
The differences between workspace and namespace are minor, with a slightly different endpoint and a different authorisation mechanism. Instead of having two different clients for workspace and namespace, I've delegated/abstracted the differences to the identity interface.
I also want to introduce identity in backend config and Oauth client, and remove the
AccessToken
method, which doesn't encapsulate the auth mechanism and the type of identity we are using. For now, I've marked as deprecated as replacing AccessToken would be beyond the scope of this task.Removed legacy multitenant code
Backend config had an additional mechanism two support multitenant without namespaces. This mechanism didn't support
/settings
as it was deprecated. I didn't want to perplex things anymore with this legacy code, so I've removed it.Notion Ticket
https://www.notion.so/rudderstacks/Reporting-features-to-control-plane-dd4976cb288544cf979f1d1e85913c1b
Security