private Optional<Integer> indexFor(HttpUrl url) { HttpUrl canonicalUrl = canonicalize(url); for (int i = 0; i < baseUrls.size(); ++i) { if (isBaseUrlFor(baseUrls.get(i), canonicalUrl)) { return Optional.of(i); } } return Optional.empty(); }
@Override public Optional<HttpUrl> redirectToNext(HttpUrl existingUrl) { // if possible, determine the index of the passed in url (so we can be sure to return a url which is different) Optional<Integer> existingUrlIndex = indexFor(existingUrl); int potentialNextIndex = existingUrlIndex.orElse(currentUrl.get()); Optional<HttpUrl> nextUrl = getNext(potentialNextIndex); if (nextUrl.isPresent()) { return redirectTo(existingUrl, nextUrl.get()); } // No healthy URLs remain; re-balance across any specified nodes return redirectTo(existingUrl, baseUrls.get((existingUrlIndex.orElse(currentUrl.get()) + 1) % baseUrls.size())); }
static UrlSelectorImpl create(Collection<String> baseUrls, boolean randomizeOrder) { return createWithFailedUrlCooldown(baseUrls, randomizeOrder, Duration.ZERO); }
@Override public Optional<HttpUrl> redirectToNextRoundRobin(HttpUrl current) { Optional<HttpUrl> nextUrl = getNext(currentUrl.get()); if (nextUrl.isPresent()) { return redirectTo(current, nextUrl.get()); } return redirectTo(current, baseUrls.get((currentUrl.get() + 1) % baseUrls.size())); }
/** * Creates a new {@link UrlSelector} with the supplied URLs. The order of the URLs may be randomized by setting * {@code randomizeOrder} to true. If a {@code failedUrlCooldown} is specified, URLs that are marked as failed * using {@link #markAsFailed(HttpUrl)} will be removed from the pool of prioritized, healthy URLs for that period * of time. */ static UrlSelectorImpl createWithFailedUrlCooldown(Collection<String> baseUrls, boolean randomizeOrder, Duration failedUrlCooldown) { List<String> orderedUrls = new ArrayList<>(baseUrls); if (randomizeOrder) { Collections.shuffle(orderedUrls); } ImmutableSet.Builder<HttpUrl> canonicalUrls = ImmutableSet.builder(); // ImmutableSet maintains insert order orderedUrls.forEach(url -> { HttpUrl httpUrl = HttpUrl.parse(switchWsToHttp(url)); Preconditions.checkArgument(httpUrl != null, "Not a valid URL: %s", url); HttpUrl canonicalUrl = canonicalize(httpUrl); Preconditions.checkArgument(canonicalUrl.equals(httpUrl), "Base URLs must be 'canonical' and consist of schema, host, port, and path only: %s", url); canonicalUrls.add(canonicalUrl); }); return new UrlSelectorImpl(ImmutableList.copyOf(canonicalUrls.build()), failedUrlCooldown); }
@Test public void testRedirectTo_doesNotFindMatchesForCaseSentitivePaths() throws Exception { String baseUrl = "http://foo/a"; UrlSelectorImpl selector = UrlSelectorImpl.create(list(baseUrl), false); assertThat(selector.redirectTo(parse(baseUrl), "http://foo/A")).isEmpty(); }
@Test public void testRedirectTo_updatesCurrentPointer() throws Exception { UrlSelectorImpl selector = UrlSelectorImpl.create(list("http://foo/a", "http://bar/a"), false); HttpUrl current = HttpUrl.parse("http://baz/a/b/path"); String redirectTo = "http://bar/a"; assertThat(selector.redirectTo(current, redirectTo)).contains(HttpUrl.parse("http://bar/a/b/path")); assertThat(selector.redirectToCurrent(current)).contains(HttpUrl.parse("http://bar/a/b/path")); }
@Test public void testRedirectToNextRoundRobin() { UrlSelectorImpl selector = UrlSelectorImpl.create(list("http://foo/a", "http://bar/a"), false); HttpUrl current = HttpUrl.parse("http://baz/a/b/path"); String redirectTo = "http://bar/a"; assertThat(selector.redirectTo(current, redirectTo)).contains(HttpUrl.parse("http://bar/a/b/path")); assertThat(selector.redirectToNextRoundRobin(current)).contains(HttpUrl.parse("http://foo/a/b/path")); assertThat(selector.redirectToNextRoundRobin(current)).contains(HttpUrl.parse("http://bar/a/b/path")); }
@Override public Optional<HttpUrl> redirectTo(HttpUrl current, String redirectBaseUrl) { return redirectTo(current, HttpUrl.parse(redirectBaseUrl)); }
private Optional<HttpUrl> redirectTo(HttpUrl current, HttpUrl redirectBaseUrl) { Optional<Integer> baseUrlIndex = indexFor(redirectBaseUrl); baseUrlIndex.ifPresent(currentUrl::set); return baseUrlIndex .map(baseUrls::get) .flatMap(baseUrl -> { if (!isPathPrefixFor(baseUrl, current)) { // The requested redirectBaseUrl has a path that is not compatible with // the path of the current URL return Optional.empty(); } else { return Optional.of(current.newBuilder() .scheme(baseUrl.scheme()) .host(baseUrl.host()) .port(baseUrl.port()) .encodedPath( baseUrl.encodedPath() // matching prefix + current.encodedPath().substring(baseUrl.encodedPath().length())) .build()); } }); }
@Test public void testIsBaseUrlFor() throws Exception { // Negative cases assertThat(UrlSelectorImpl.isBaseUrlFor(parse("http://foo/a"), parse("https://foo/a"))).isFalse(); assertThat(UrlSelectorImpl.isBaseUrlFor(parse("http://foo/a"), parse("http://bar/a"))).isFalse(); assertThat(UrlSelectorImpl.isBaseUrlFor(parse("http://foo/a"), parse("http://foo:8080/a"))).isFalse(); assertThat(UrlSelectorImpl.isBaseUrlFor(parse("http://foo/b"), parse("http://foo/a"))).isFalse(); // Positive cases: schema, host, port must be equal, path must be a prefix assertThat(UrlSelectorImpl.isBaseUrlFor(parse("http://foo/a"), parse("http://foo/a"))).isTrue(); assertThat(UrlSelectorImpl.isBaseUrlFor(parse("http://foo/a"), parse("http://foo/a/"))).isTrue(); assertThat(UrlSelectorImpl.isBaseUrlFor(parse("http://foo/a"), parse("http://foo/a/b"))).isTrue(); assertThat(UrlSelectorImpl.isBaseUrlFor(parse("http://foo"), parse("http://foo/a"))).isTrue(); }
@Override public void markAsFailed(HttpUrl failedUrl) { if (useFailedUrlCache) { Optional<Integer> indexForFailedUrl = indexFor(failedUrl); indexForFailedUrl.ifPresent(index -> failedUrls.put(baseUrls.get(index), UrlAvailability.FAILED) ); } }
/** * Returns true if the canonicalized base URLs are equal and if the path of the {@code prefixUrl} is a prefix (in * the string sense) of the path of the given {@code fullUrl}. */ @VisibleForTesting static boolean isBaseUrlFor(HttpUrl baseUrl, HttpUrl fullUrl) { return fullUrl.scheme().equals(baseUrl.scheme()) && fullUrl.host().equals(baseUrl.host()) && fullUrl.port() == baseUrl.port() && isPathPrefixFor(baseUrl, fullUrl); }
/** * Creates a new {@link UrlSelector} with the supplied URLs. The order of the URLs may be randomized by setting * {@code randomizeOrder} to true. If a {@code failedUrlCooldown} is specified, URLs that are marked as failed * using {@link #markAsFailed(HttpUrl)} will be removed from the pool of prioritized, healthy URLs for that period * of time. */ static UrlSelectorImpl createWithFailedUrlCooldown(Collection<String> baseUrls, boolean randomizeOrder, Duration failedUrlCooldown) { List<String> orderedUrls = new ArrayList<>(baseUrls); if (randomizeOrder) { Collections.shuffle(orderedUrls); } ImmutableSet.Builder<HttpUrl> canonicalUrls = ImmutableSet.builder(); // ImmutableSet maintains insert order orderedUrls.forEach(url -> { HttpUrl httpUrl = HttpUrl.parse(switchWsToHttp(url)); Preconditions.checkArgument(httpUrl != null, "Not a valid URL: %s", url); HttpUrl canonicalUrl = canonicalize(httpUrl); Preconditions.checkArgument(canonicalUrl.equals(httpUrl), "Base URLs must be 'canonical' and consist of schema, host, port, and path only: %s", url); canonicalUrls.add(canonicalUrl); }); return new UrlSelectorImpl(ImmutableList.copyOf(canonicalUrls.build()), failedUrlCooldown); }
@Test public void testRedirectTo_failsWhenRequestedBaseUrlPathIsNotPrefixOfCurrentPath() throws Exception { String url1 = "http://foo/a"; String url2 = "https://bar:8080/a/b/c"; UrlSelectorImpl selector = UrlSelectorImpl.create(list(url1, url2), false); assertThat(selector.redirectTo(parse(url1), url2)).isEmpty(); }
@Override public Optional<HttpUrl> redirectToNextRoundRobin(HttpUrl current) { Optional<HttpUrl> nextUrl = getNext(currentUrl.get()); if (nextUrl.isPresent()) { return redirectTo(current, nextUrl.get()); } return redirectTo(current, baseUrls.get((currentUrl.get() + 1) % baseUrls.size())); }
@Override public Optional<HttpUrl> redirectTo(HttpUrl current, String redirectBaseUrl) { return redirectTo(current, HttpUrl.parse(redirectBaseUrl)); }
private Optional<HttpUrl> redirectTo(HttpUrl current, HttpUrl redirectBaseUrl) { Optional<Integer> baseUrlIndex = indexFor(redirectBaseUrl); baseUrlIndex.ifPresent(currentUrl::set); return baseUrlIndex .map(baseUrls::get) .flatMap(baseUrl -> { if (!isPathPrefixFor(baseUrl, current)) { // The requested redirectBaseUrl has a path that is not compatible with // the path of the current URL return Optional.empty(); } else { return Optional.of(current.newBuilder() .scheme(baseUrl.scheme()) .host(baseUrl.host()) .port(baseUrl.port()) .encodedPath( baseUrl.encodedPath() // matching prefix + current.encodedPath().substring(baseUrl.encodedPath().length())) .build()); } }); }
@Override public void markAsFailed(HttpUrl failedUrl) { if (useFailedUrlCache) { Optional<Integer> indexForFailedUrl = indexFor(failedUrl); indexForFailedUrl.ifPresent(index -> failedUrls.put(baseUrls.get(index), UrlAvailability.FAILED) ); } }
/** * Returns true if the canonicalized base URLs are equal and if the path of the {@code prefixUrl} is a prefix (in * the string sense) of the path of the given {@code fullUrl}. */ @VisibleForTesting static boolean isBaseUrlFor(HttpUrl baseUrl, HttpUrl fullUrl) { return fullUrl.scheme().equals(baseUrl.scheme()) && fullUrl.host().equals(baseUrl.host()) && fullUrl.port() == baseUrl.port() && isPathPrefixFor(baseUrl, fullUrl); }