-
Notifications
You must be signed in to change notification settings - Fork 4k
core: pass Subchannel state updates to SubchannelStateListener rather than LoadBalancer #5503
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
Conversation
…channel is created; All existing implementations pass despite deprecation warnings
|
Quick thoughts before diving in:
|
I thought about that option, but didn't go that route. The Subchannel arg from the listener actually fits naturally. A ...
subchannel.start((state) -> handleState(subchannel, state));
...
private void handleState(Subchannel subchannel, ConnectivityStateInfo state) {
...
}For the sake of API style consistency, and a relatively smaller churn on the API (which mostly affects unit tests), it may be the right thing to do. |
|
Another reason that may justify passing the listener to The interfaces that has
|
As suggested by @carl-mastrangelo, using a first-class object to pass arguments may avoid breakages when we add new arguments to createSubchannel(). For example, a LoadBalancer may wrap Helper and intercept createSubchannel() in a hierarchical case. It may not be interested in all arguments. Passing a single CreateSubchannelArgs will not break the parent LoadBalancer if new fields are added. This also reduces the eventual size of Helper interface, as the convenience createSubchannel() that accepts one EAG instead of a List is no longer necessary. That convenience is moved into CreateSubchannelArgs.
carl-mastrangelo
left a comment
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.
Mostly LGTM. A few nits first
| private CreateSubchannelArgs( | ||
| List<EquivalentAddressGroup> addrs, Attributes attrs, | ||
| SubchannelStateListener stateListener) { | ||
| this.addrs = checkNotNull(addrs, "addresses are not set"); |
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 needs to be a copy
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 made Builder copy it.
| this.addrs = Collections.singletonList(addrs); | ||
| return this; | ||
| } | ||
|
|
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.
make Builders ctor package private
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.
Done.
| if (entry == null) { | ||
| subchannel = helper.createSubchannel(eag, defaultAttributes); | ||
| final Attributes attrs = defaultAttributes.toBuilder() | ||
| .set(STATE_LISTENER, new AtomicReference<>(listener)).build(); |
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.
nit: .build() should be on the next line
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.
Done.
| private RoundRobinPicker currentPicker = | ||
| new RoundRobinPicker(Collections.<DropEntry>emptyList(), Arrays.asList(BUFFER_ENTRY)); | ||
|
|
||
| private final SubchannelStateListener subchannelStateListener = new SubchannelStateListener() { |
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.
One thing you should consider: Make these listeners a named private class. It makes debugging much easier in a stack trace. Also, the named classes can be made final, which avoids letting Mockito.spy extend them.
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 changed SubchannelStateListener per @ejona86's request. Some LoadBalancers now implement it directly. In other cases I have made named classes for the listeners.
| */ | ||
| public Builder setAddresses(List<EquivalentAddressGroup> addrs) { | ||
| checkArgument(!addrs.isEmpty(), "addrs is empty"); | ||
| this.addrs = Collections.unmodifiableList(addrs); |
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.
An immutable copy or doc that the given arg should not be mutated afterwards?
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.
Done.
| } | ||
|
|
||
| @Test | ||
| public void helper_createSubchannel_delegates() { |
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 think this does not test anything useful. (It only tests the ArgsBuilder creates equal instances for the same input though, but that test could be simpler.)
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.
Good catch. Deleting this test.
| new NoopHelper().createSubchannel(Arrays.asList(eag), attrs); | ||
| } | ||
|
|
||
| @Test(expected = UnsupportedOperationException.class) |
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.
nit: this will be an errorprone when import.
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.
Got it. TestExceptionChecker discourages use of @Test(expected) because "the test will pass if any statement in the test method throws the expected exception". Switched away from it.
| .setStateListener(subchannelStateListener) | ||
| .build()); | ||
| fail("Should throw"); | ||
| } catch (IllegalStateException e) { |
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.
nit: This will also be an errorprone when import.
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.
Does errorprone prefer assertThrows? It's not used in grpc.
| @SuppressWarnings({"deprecation", "unchecked"}) | ||
| public void tearDown() throws Exception { | ||
| verifyNoMoreInteractions(mockArgs); | ||
| verify(mockHelper, never()).createSubchannel(any(List.class), any(Attributes.class)); |
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.
nit: ArgumentMatchers.<List<EAG>>any() does not need suppress unchecked.
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.
Done.
| // in the cache. | ||
| subchannel.getAttributes().get(STATE_LISTENER).set(listener); | ||
| // Make the listener up-to-date with the latest state in case it has changed while it's in the | ||
| // cache. |
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.
Is the whole method in syncContext? Why not run listener.onSubchannelState(subchannel, entry.state) directly, so that there will be no state change?
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.
onSubchannelState() is not re-entrant with the other logic from GrpclbState when called by channel impl. Although reentrancy may not cause a problem, it's better to keep the baheavior closer to with channel impl.
That said, I think there is a problem with the current code. If channel impl schedules an update before the task on the next line while this method is running, the update from the channel, which is newer, would be overwritten with the older value by the latter task. I went with a different approach of the implementation that always track Subchannels and their states, whether in or out of the pool, which ends up simpler.
dapengzhang0
left a comment
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
when a subchannel is returned and channel impl sends an update at the same time, channel impl may schedule the state update BEFORE the fake update call for the previously saved state, thus the newer state would be overwritten by the older state.
ejona86
left a comment
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 spoke with Kun about the API change yesterday. In the end I wasn't really convinced and close to being on the fence, but was willing to defer to Kun.
Without this API change, parent policies with multiple subpolicies concurrently need to track which subchannels go to which subpolicy. They can do that with ~2ish lines via the per-Subchannel Attributes, so I didn't find it that big of a deal. So I was sort of leaning toward the conservative approach and let sleeping dogs lie; changes to the API cause instability that can lead to more changes. But I also agreed there weren't too many ways this could cause additional issues.
…java into handle_subchannel_state
zhangkun83
left a comment
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.
@ejona86 PTAL
| private RoundRobinPicker currentPicker = | ||
| new RoundRobinPicker(Collections.<DropEntry>emptyList(), Arrays.asList(BUFFER_ENTRY)); | ||
|
|
||
| private final SubchannelStateListener subchannelStateListener = new SubchannelStateListener() { |
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 changed SubchannelStateListener per @ejona86's request. Some LoadBalancers now implement it directly. In other cases I have made named classes for the listeners.
| } | ||
|
|
||
| @Test | ||
| public void helper_createSubchannel_delegates() { |
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.
Good catch. Deleting this test.
| new NoopHelper().createSubchannel(Arrays.asList(eag), attrs); | ||
| } | ||
|
|
||
| @Test(expected = UnsupportedOperationException.class) |
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.
Got it. TestExceptionChecker discourages use of @Test(expected) because "the test will pass if any statement in the test method throws the expected exception". Switched away from it.
| .setStateListener(subchannelStateListener) | ||
| .build()); | ||
| fail("Should throw"); | ||
| } catch (IllegalStateException e) { |
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.
Does errorprone prefer assertThrows? It's not used in grpc.
| @SuppressWarnings({"deprecation", "unchecked"}) | ||
| public void tearDown() throws Exception { | ||
| verifyNoMoreInteractions(mockArgs); | ||
| verify(mockHelper, never()).createSubchannel(any(List.class), any(Attributes.class)); |
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.
Done.
ejona86
left a comment
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.
LoadBalancer and CachedSubchannelPool look good. I didn't look much at anything else.
…than LoadBalancer (take 2) (#5722) This is a revised version of #5503 (62b03fd), which was rolled back in f8d0868. The newer version passes SubchannelStateListener to Subchannel.start() instead of SubchannelCreationArgs, which allows us to remove the Subchannel argument from the listener, which works as a solution for #5676. LoadBalancers that call the old createSubchannel() will get start() implicitly called with a listener that passes updates to the deprecated LoadBalancer.handleSubchannelState(). Those who call the new createSubchannel() will have to call start() explicitly. GRPCLB code is still using the old API, because it's a pain to migrate the SubchannelPool to the new API. Since CachedSubchannelHelper is on the way, it's easier to switch to it when it's ready. Keeping GRPCLB with the old API would also confirm the backward compatibility.
Resolves #5497
Notes for reviewers
The API and the needed changes in
LoadBalancerimplementations are relatively simple. The large changes are mostly in tests. Please focus on non-test files first.Motivation
In hierarchical
LoadBalancers (e.g.,XdsLoadBalancer) or wrappedLoadBalancers (e.g.,HealthCheckingLoadBalancerFactory, the top-levelLoadBalancerreceivesSubchannelstate updates from the Channel impl, and they almost always pass it down to its childrenLoadBalancers.Sometimes the children
LoadBalancers are not directly created by the parent, thus requires whatever API in the middle to also pass Subchannel state updates, complicating that API. For example, the proposedRequestDirectorincludeshandleSubchannelState()solely to plumb state updates to where they are used. We also see this pattern inHealthCheckingLoadBalancerFactory,GrpclbStateandSubchannelPool.Another minor issue is, the parent
LoadBalancerwould need to intercept theHelperpassed to its children to map Subchannels to the childrenLoadBalancers, so that it pass updates about relevant Subchannels to the children. Otherwise, a childLoadBalancermay be surprised by seeing Subchannel not created by it, and it's not efficient to broadcast Subchannel updates to all children.API Proposal
We will pass a
SubchannelStateListenerwhen creating aSubchannelto accept state updates, those updates could be directly passed to where theSubchannelis created, skipping the explicit chaining in the middle.Also define a first-class object
CreateSubchannelArgsto pass arguments for the reasons below:createSubchannel(). For example, aLoadBalancermay wrapHelperand interceptcreateSubchannel()in a hierarchical case. It may not be interested in all arguments. Passing a singleCreateSubchannelArgswill not break the parentLoadBalancerif we add new fields later.createSubchannel()that accepts one EAG instead of a List is no longer necessary, since that convenience is moved intoCreateSubchannelArgs.The new
createSubchannel()must be called from synchronization context, as a step towards #5015.How the new API helps
Most hierarchical
LoadBalancers would just let the listener from the childLoadBalancers directly pass through to gRPC core, which is less boilerplate than before.Without any effort by the parent, each child will only see updates for the Subchannels it has created, which is clearer and more efficient.
If a parent
LoadBalancerdoes want to look at or alter the Subchannel state updates for its delegate (like inHealthCheckingLoadBalancerFactory), it can still do so in the wrappingLoadBalancer.Helperpassed to the delegate by intercepting theSubchannelStateListener.Migration implications
Existing
LoadBalancerimplementations will continue to work, while they will see deprecation warnings when compiled:LoadBalancer.Helper#createSubchannelvariants are now deprecated, but will work until they are deleted. They create aSubchannelStateListenerthat delegates toLoadBalancer#handleSubchannelState.LoadBalancer#handleSubchannelStateis now deprecated, and will throw if called and the implementation doesn't override it. It will be deleted in a future release.The migration for most
LoadBalancerimplementation would be moving the logic fromLoadBalancer#handleSubchannelStateinto aSubchannelStateListener.