-
Notifications
You must be signed in to change notification settings - Fork 96
Better behaviour in the presence of 429s #786
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
Changes from 3 commits
2dcd409
c583b33
d51d3ac
19a8173
d7164de
39388b7
f2927d0
8f57de6
8f02b8b
fbbcc41
27a2153
e548996
a4c9e68
c68bc89
91dd6d2
2229a81
ee8e539
036d45b
1e18435
fbdeab1
951dfdd
ed18c48
fda4d64
8a088d7
a9721e4
1ce7b82
bc38b76
addbdca
c97925a
baaa142
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,177 @@ | ||
| /* | ||
| * Copyright 2018 Palantir Technologies, Inc. All rights reserved. | ||
| * | ||
| * Licensed under the Apache License, Version 2.0 (the "License"); | ||
| * you may not use this file except in compliance with the License. | ||
| * You may obtain a copy of the License at | ||
| * | ||
| * http://www.apache.org/licenses/LICENSE-2.0 | ||
| * | ||
| * Unless required by applicable law or agreed to in writing, software | ||
| * distributed under the License is distributed on an "AS IS" BASIS, | ||
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| * See the License for the specific language governing permissions and | ||
| * limitations under the License. | ||
| */ | ||
|
|
||
| package com.palantir.remoting3.okhttp; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @iamdanfox what's the deal with remoting-vs-conjure in PRs? |
||
|
|
||
| import com.google.common.annotations.VisibleForTesting; | ||
| import com.google.common.cache.CacheBuilder; | ||
| import com.google.common.cache.RemovalCause; | ||
| import com.google.common.util.concurrent.Futures; | ||
| import com.google.common.util.concurrent.ListenableFuture; | ||
| import com.google.common.util.concurrent.SettableFuture; | ||
| import com.netflix.concurrency.limits.Limiter; | ||
| import com.netflix.concurrency.limits.limit.AIMDLimit; | ||
| import com.netflix.concurrency.limits.limit.TracingLimitDecorator; | ||
| import com.netflix.concurrency.limits.limiter.DefaultLimiter; | ||
| import com.netflix.concurrency.limits.strategy.SimpleStrategy; | ||
| import com.palantir.remoting3.tracing.okhttp3.OkhttpTraceInterceptor; | ||
| import java.util.Map; | ||
| import java.util.Optional; | ||
| import java.util.Queue; | ||
| import java.util.concurrent.ConcurrentHashMap; | ||
| import java.util.concurrent.ConcurrentMap; | ||
| import java.util.concurrent.LinkedBlockingQueue; | ||
| import okhttp3.Request; | ||
| import org.slf4j.Logger; | ||
| import org.slf4j.LoggerFactory; | ||
|
|
||
| /** | ||
| * Remoting calls may observe 429 or 503 responses from the server, at which point they back off in order to | ||
| * reduce excess load. Unfortunately this state on backing off is stored per-call, so 429s or 503s in one call do not | ||
|
||
| * cause any request rate slowdown in subsequent calls. This class affects this by adjusting the number of requests | ||
| * that might be dispatched to a given endpoint. | ||
| * <p> | ||
| * This is based on Netflix's <a href="https://github.com/Netflix/concurrency-limits/">Concurrency Limits</a> library, | ||
| * which provides a number of primitives for this. | ||
| * <p> | ||
| * In order to use this class, one should get a Limiter for their request, which returns a future. once the Future is | ||
| * done, the caller can assume that the request is schedulable. After the request completes, the caller <b>must</b> | ||
| * call one of the methods on {@link Limiter.Listener} in order to provide feedback about the request's success. | ||
| * If this is not done, throughput will be negatively affected. We attempt to eventually recover to avoid a total | ||
| * deadlock, but this is not guaranteed. | ||
| */ | ||
| final class ConcurrencyLimiters { | ||
| private static final Logger log = LoggerFactory.getLogger(ConcurrencyLimiters.class); | ||
| private static final String FALLBACK = ""; | ||
|
|
||
| // If a request is never marked as complete and is thrown away, recover on the next GC instead of deadlocking | ||
| private static final Map<Limiter.Listener, Runnable> activeListeners = CacheBuilder.newBuilder() | ||
| .weakKeys() | ||
| .<Limiter.Listener, Runnable>removalListener(notification -> { | ||
| if (notification.getCause().equals(RemovalCause.COLLECTED)) { | ||
| log.warn("Concurrency limiter was leaked." | ||
| + " This implies a remoting bug or classpath issue, and may cause degraded performance"); | ||
| notification.getValue().run(); | ||
| } | ||
| }) | ||
| .build() | ||
| .asMap(); | ||
|
|
||
| private final ConcurrentMap<String, ConcurrencyLimiter> limiters = new ConcurrentHashMap<>(); | ||
|
||
|
|
||
| private static Limiter<Void> newLimiter() { | ||
|
||
| return DefaultLimiter.newBuilder() | ||
| .limit(TracingLimitDecorator.wrap(AIMDLimit.newBuilder().initialLimit(1).build())) | ||
| .build(new SimpleStrategy<>()); | ||
| } | ||
|
|
||
| @VisibleForTesting | ||
| ConcurrencyLimiter limiter(String name) { | ||
| return limiters.computeIfAbsent(name, key -> new ConcurrencyLimiter(activeListeners, newLimiter())); | ||
| } | ||
|
|
||
| ConcurrencyLimiter limiter(Request request) { | ||
| final String limiterKey; | ||
|
||
| String pathTemplate = request.header(OkhttpTraceInterceptor.PATH_TEMPLATE_HEADER); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this is a bit dodgy
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this is still dodgy
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't really see a way of avoiding this. It seems reasonable to do this by endpoint, and if you do that you end up with this.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. could also see this being something that uses a dynamic proxy which makes it much easier to limit per method or per some annotation. think the only sad thing about this is relying on the tracing header which is only every passed around internally (never sent across the wire)
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. OK. But then I'd rename the code bits so that they're no longer "trace"-specific. Probably also need to stop deleting the header in the trace-specific code path |
||
| if (pathTemplate == null) { | ||
| limiterKey = FALLBACK; | ||
| } else { | ||
| limiterKey = request.method() + " " + pathTemplate; | ||
| } | ||
| return limiter(limiterKey); | ||
| } | ||
|
|
||
| /** | ||
| * The Netflix library provides either a blocking approach or a non-blocking approach which might say | ||
| * you can't be scheduled at this time. All of our HTTP calls are asynchronous, so we really want to get | ||
| * a {@link ListenableFuture} that we can add a callback to. This class then is a translation of | ||
| * {@link com.netflix.concurrency.limits.limiter.BlockingLimiter} to be asynchronous, maintaining a queue | ||
| * of currently waiting requests. | ||
| * <p> | ||
| * Upon a request finishing, we check if there are any waiting requests, and if there are we attempt to trigger | ||
| * some more. | ||
| */ | ||
| static final class ConcurrencyLimiter { | ||
j-baker marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| private final Map<Limiter.Listener, Runnable> activeListeners; | ||
| private final Queue<SettableFuture<Limiter.Listener>> waitingRequests = new LinkedBlockingQueue<>(); | ||
| private final Limiter<Void> limiter; | ||
|
|
||
| public ConcurrencyLimiter( | ||
| Map<Limiter.Listener, Runnable> activeListeners, | ||
| Limiter<Void> limiter) { | ||
| this.activeListeners = activeListeners; | ||
| this.limiter = limiter; | ||
| } | ||
|
|
||
| public ListenableFuture<Limiter.Listener> acquire() { | ||
| Optional<Limiter.Listener> maybeListener = limiter.acquire(null); | ||
| if (maybeListener.isPresent()) { | ||
| return Futures.immediateFuture(wrap(activeListeners, maybeListener.get())); | ||
| } | ||
| SettableFuture<Limiter.Listener> future = SettableFuture.create(); | ||
| waitingRequests.add(future); | ||
| return future; | ||
| } | ||
|
|
||
| private void processQueue() { | ||
| while (!waitingRequests.isEmpty()) { | ||
| Optional<Limiter.Listener> maybeAcquired = limiter.acquire(null); | ||
|
||
| if (!maybeAcquired.isPresent()) { | ||
| return; | ||
| } | ||
| Limiter.Listener acquired = maybeAcquired.get(); | ||
| SettableFuture<Limiter.Listener> head = waitingRequests.poll(); | ||
|
||
| if (head == null) { | ||
| acquired.onIgnore(); | ||
| } else { | ||
| head.set(acquired); | ||
|
||
| } | ||
| } | ||
| } | ||
|
|
||
| private Limiter.Listener wrap( | ||
| Map<Limiter.Listener, Runnable> activeListeners, Limiter.Listener listener) { | ||
|
||
| Limiter.Listener res = new Limiter.Listener() { | ||
| @Override | ||
| public void onSuccess() { | ||
| listener.onSuccess(); | ||
| activeListeners.remove(this); | ||
| processQueue(); | ||
| } | ||
|
|
||
| @Override | ||
| public void onIgnore() { | ||
| listener.onIgnore(); | ||
| activeListeners.remove(this); | ||
| processQueue(); | ||
| } | ||
|
|
||
| @Override | ||
| public void onDropped() { | ||
| listener.onDropped(); | ||
| activeListeners.remove(this); | ||
| processQueue(); | ||
| } | ||
| }; | ||
| activeListeners.put(res, () -> { | ||
| listener.onIgnore(); | ||
| processQueue(); | ||
| }); | ||
| return res; | ||
| } | ||
|
|
||
| } | ||
| } | ||
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.
@samrogerson fysa
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.
uhm, I thought we had merged such a change already?
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, but we did it in only one of the two places (see above in this class)