-
Notifications
You must be signed in to change notification settings - Fork 16
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
reuse concurrency limiter state when reloading host channels #2413
Conversation
Generate changelog in
|
} | ||
} | ||
|
||
static final class StateHolder { |
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.
disclaimer: naming is hard, and I'm not very good at it. Take my comments with a grain of salt!
Thoughts on extracting this to its own file, and renaming to ChannelState
? "Holder" makes me think it's using the old initialize-on-demand holder pattern.
If we extract to its own file, the key type could make sense as a static nested class, since the outer class provides context as to what it's a key for e.g. ChannelState.Key
.
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.
yep done
@Value.Immutable | ||
interface ConcurrencyLimitedChannelState { | ||
CautiousIncreaseAggressiveDecreaseConcurrencyLimiter hostLimiter(); | ||
} |
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 we can put the CautiousIncreaseAggressiveDecreaseConcurrencyLimiter
directly into the state, since it's the only piece of data we're tracking
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 static final StateHolderKey<ConcurrencyLimitedChannelState> STATE_HOLDER_KEY = | ||
new StateHolderKey<>(ConcurrencyLimitedChannelState.class, ConcurrencyLimitedChannel::createState); |
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.
II think we should include HOST
in the name, since this is creating a host-specific limiter
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
T cast(final Object value) { | ||
return valueClass.cast(value); | ||
} | ||
|
||
Supplier<T> getFactory() { | ||
return factory; | ||
} |
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.
These can be private
} | ||
} | ||
|
||
@SuppressWarnings("DangerousIdentityKey") |
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.
We could implement equals
and hashCode
on Key
, delegating to super
to make this explicit (and avoid the suppression)
if (state.containsKey(key)) { | ||
return key.cast(state.get(key)); | ||
} else { | ||
T value = key.getFactory().get(); | ||
Preconditions.checkNotNull(value, "state factory cannot produce a null value"); | ||
state.put(key, value); | ||
return 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.
I don't think we expect this to be a hot path, so we can use something like:
return key.cast(Preconditions.checkNotNull(
state.computeIfAbsent(key, keyValue -> keyValue.getFactory().get()),
"state factory cannot produce a null value"));
})); | ||
LimitedChannel nodeSelectionChannel = | ||
new SupplierChannel(cf.uris().map(new Function<List<TargetUri>, LimitedChannel>() { | ||
private final Map<TargetUri, ChannelState> state = new HashMap<>(); |
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.
Let's make this a ConcurrentHashMap for safety, that way we don't need to think about it
Set<TargetUri> toRemove = state.keySet().stream() | ||
.filter(uri -> !targetUris.contains(uri)) | ||
.collect(Collectors.toSet()); | ||
toRemove.forEach(state::remove); |
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 we can replace this with state.keySet().retainAll(targetUris);
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 voodoo that i didn't know existed
.filter(uri -> !targetUris.contains(uri)) | ||
.collect(Collectors.toSet()); | ||
toRemove.forEach(state::remove); | ||
targetUris.forEach(uri -> state.putIfAbsent(uri, new ChannelState())); |
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.
Perhaps computeIfAbsent(uri, _uri -> new ChannelState())
to avoid creating ChannelState
instances for keys that are already in the map?
@@ -39,13 +40,20 @@ | |||
final class ConcurrencyLimitedChannel implements LimitedChannel { | |||
private static final SafeLogger log = SafeLoggerFactory.get(ConcurrencyLimitedChannel.class); | |||
|
|||
@VisibleForTesting | |||
static final ChannelState.Key<CautiousIncreaseAggressiveDecreaseConcurrencyLimiter> HOST_SPECIFIC_STATE_KEY = |
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 visible for the purposes of ConcurrencyLimitedChannelTest#testReuseCachedLimiterState_host
, which may not be a particularly useful test. I'm somewhat indifferent, we could delete that test and make this private.
3d820e4
to
7cb1e69
Compare
Before this PR
The recent changes to implement DNS node discovery came with the drawback that when domain name resolution changes are detected, we reload channels causing all internal channel state to be discarded and recreated anew. This means that concurrency limiter state (i.e. the current limit values encapsulated within a
CautiousIncreaseAggressiveDecreaseConcurrencyLimiter
instance) for a per-host channel are lost and we need to restart the congestion control algorithm.This can have some implications in both directions: if the limiter state has reduced the concurrency significantly because the server asked us to slow down, we may end up bombarding the host with more requests than it can handle when a reload happens; if the limiter state has increased concurrency significantly, we will artificially start limiting request concurrency and have to slowly work our way back up to better throughput.
After this PR
This change introduces a
ChannelState
capable of holding internal state to be retained across channel reloads. In this change, we add a cache mappingTargetUri
to aChannelState
instance, where eachChannelState
holds an instance of aCautiousIncreaseAggressiveDecreaseConcurrencyLimiter
. This allows aConcurrencyLimitedChannel
for a specificTargetUri
to be created anew but re-use existing limiter state for that particular host.Note that this change only affects per-host channels, not per-endpoint channels (which are also concurrency limited, but interact with a
QueuedChannel
in a much different way). A follow-on PR will address retaining limiter state for per-endpoint channels upon reload.==COMMIT_MSG==
reuse concurrency limiter state when reloading host channels
==COMMIT_MSG==
Possible downsides?