-
Notifications
You must be signed in to change notification settings - Fork 5
[Experimental] Optional gRPC connection sharing among VUs #31
[Experimental] Optional gRPC connection sharing among VUs #31
Conversation
…nSharing` field of options parameter to `grpc.Client.connect` function
Hi @chrismoran-mica ! Thanks for the contribution 👍 Sorry for the long reply, it was vacation season, and after that, some should be done for the upcoming k6 release. I want to let you know that reviewing this PR is on my list, and I will do that next week. Cheers! |
|
||
client, ok := v.ToObject(rt).Export().(*Client) | ||
if !ok { | ||
return false, errors.New("parameter must a 'Client'") |
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.
return false, errors.New("parameter must a 'Client'") | |
return false, errors.New("parameter must be a gRPC 'Client'") |
func parseConnectConnectionSharingParam(params *connectParams, v interface{}) error { | ||
var ( | ||
ok bool | ||
connectionSharingBool bool |
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.
Out of curiosity, why do you think mixing the param type is beneficial? Why not stick with the number?
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.
Same question 👍🏻
I would intuitively tend to prefer having two separate options: one boolean to activate/deactivate connection sharing and one number to control the maximum sharing.
But I'm happy to be convinced otherwise 🙇🏻
} | ||
|
||
var ( | ||
// Is there any other way? |
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.
it seems like yes
it seems like we need a new struct like connectionPool
(a better name). This struct could be initialized and live in the RootModule
https://github.com/grafana/xk6-grpc/blob/main/grpc/grpc.go#L15-L26 This struct could also take the big part of the management of the connections probably
And pass it to the client via the NewClient
https://github.com/grafana/xk6-grpc/blob/main/grpc/grpc.go#L59-L63
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.
👍🏻 on my side for a RootModule
connection pool.
By the way, and to be we're all on the same page as to why we suggest the RootModule
, that's because it will be instantiated only once throughout the execution and will be spawning new instances for VUs (hence, one can easily pass a pointer to all instances of the said pool from there).
func parseConnectConnectionSharingParam(params *connectParams, v interface{}) error { | ||
var ( | ||
ok bool | ||
connectionSharingBool bool |
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.
Same question 👍🏻
I would intuitively tend to prefer having two separate options: one boolean to activate/deactivate connection sharing and one number to control the maximum sharing.
But I'm happy to be convinced otherwise 🙇🏻
@@ -58,9 +61,22 @@ type clientConnCloser interface { | |||
|
|||
// Conn is a gRPC client connection. | |||
type Conn struct { | |||
raw clientConnCloser | |||
addr string | |||
shares atomic.Uint64 |
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.
My understanding is that shares
represents "how many concurrent clients are using this connection". Is that correct?
I would suggest at the very least, adding a godoc comment on this field to clarify what the atomic number it holds represents, and I can't think of a more explicit name, but if one of us was to come up with one, I think it could be good. When I see "shares", I think "shares of what?" which, I believe, doesn't do justice to what the attribute actually holds, if that makes sense 🤓
} | ||
|
||
var ( | ||
// Is there any other way? |
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.
👍🏻 on my side for a RootModule
connection pool.
By the way, and to be we're all on the same page as to why we suggest the RootModule
, that's because it will be instantiated only once throughout the execution and will be spawning new instances for VUs (hence, one can easily pass a pointer to all instances of the said pool from there).
// Is there any other way? | ||
|
||
//nolint:golint,gochecknoglobals | ||
connections = &sync.Map{} |
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.
To raise awareness on this, the sync.Map
documentation mentions:
The Map type is optimized for two common use cases: (1) when the entry for a given key is only ever written once but read many times, as in caches that only grow, or (2) when multiple goroutines read, write, and overwrite entries for disjoint sets of keys.
Does the use of it we make here fit those use cases? Otherwise I would suggest a good old map with explicit locking instead 👍🏻
//nolint:golint,gochecknoglobals | ||
connections = &sync.Map{} | ||
//nolint:golint,gochecknoglobals | ||
connectionsAddrMu = &sync.Map{} |
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.
Same comment about sync.Map
here 🙇🏻
err error | ||
) | ||
|
||
// Lock for mutating connectionsAddrMu map |
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'm somewhat confused as to why we lock here, based on the fact that connectionsAddrMu
and connections
are sync.Map
at the moment, which is designed to be safe for concurrent use without locking.
Could you expand the comment to reflect that information? 🙇🏻
Chris Moran seems not to be a GitHub user. You need a GitHub account to be able to sign the CLA. If you have already a GitHub account, please add the email address used for this commit to your account. You have signed the CLA already but the status is still pending? Let us recheck it. |
As the CLA wasn't signed, we can't merge this PR even if the comments will be addressed. So we are closing it for now. If, in the future, there will be a need to continue to work on that, we could return to it. |
@chrismoran-mica I have a similar requirement where instead of creating new grpc connection per VU, I want to create a pool of connections and share it with VUs. Can you please help me how to use this in a K6 js file? |
What?
Add a parameter to
connectParams
that would allowgrpc.ClientConn
s to be shared across VUs. In order for connections to be shared (within a VU or across VUs) the following must be true:Client
isconnect
ed with{ connectionSharing: boolean|integer }
Client
(within a VU or across VUs) if theaddr
(address) parameter toconnect
is the same (case-insensitive)Client
s connection to be shared is made whenconnect
is calledLogically, it would only make sense to share a connection (within a VU or across VUs) if the two
Client
s in question target an endpoint that either:Client
sAside from comparing
protoreflect.MethodDescriptor
s, I cannot think of a way to ensure theClient
s should share a connection prior to making the connection and callinginvoke
(to generate theprotoreflect.MethodDescriptor
mapping). So it would be left to the test writer to decide if theClient
s should share connections.Why?
This feature has the potential to substantially reduce the resources required for multiple VUs connecting to the same service(s) or endpoint(s). This gives greater flexibility for testing scenarios.
On the other hand, this does blur the line among VUs with
connectionSharing
enabled ongrpc.Client
s. There could potentially be unforeseen consequences to sharing connections among VUs. This situation, however, is mitigated by the fact that this feature is opt-in and disabled by default.TODO / In-process:
Connection sharing limiting
ConnectionSharing
parameter can be either a boolean value OR an integer > 1true
value will mean ConnectionSharing is enabled for the Client and the maximum sharing is set to a sane default (100)false
value will mean ConnectionSharing is disabled entirely for that ClientconnectionSharing - 1
number of times before a new connection is made.connectionSharing
parameter exactly when the actual connection was dialed.Limiting the number of connections can be a complicated concept. Since each
client.connect
can have aconnectionSharing
parameter with any arbitrary value forconnectionSharing
it can quickly become confusing which connections are possibly being shared.Checklist
Limit the connection sharing amount using a maximum connection sharing amount parameter and/or a sane default
Limit complexity of the connection sharing logic
I have performed a self-review of my code.
I have added tests for my changes.
I have run linter locally (
make lint
) and all checks pass.I have run tests locally (
make test
) and all tests pass.I have commented on my code, particularly in hard-to-understand areas.