All Downloads are FREE. Search and download functionalities are using the official Maven repository.

discord4j.rest.request.RequestStream Maven / Gradle / Ivy

/*
 * This file is part of Discord4J.
 *
 * Discord4J is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * Discord4J is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with Discord4J.  If not, see .
 */
package discord4j.rest.request;

import discord4j.common.LogUtil;
import discord4j.rest.http.client.ClientException;
import discord4j.rest.http.client.ClientRequest;
import discord4j.rest.http.client.ClientResponse;
import discord4j.rest.http.client.DiscordWebClient;
import discord4j.rest.response.ResponseFunction;
import org.reactivestreams.Subscription;
import reactor.core.publisher.BaseSubscriber;
import reactor.core.publisher.Mono;
import reactor.core.publisher.MonoProcessor;
import reactor.core.publisher.SignalType;
import reactor.core.scheduler.Scheduler;
import reactor.netty.http.client.HttpClientResponse;
import reactor.retry.Retry;
import reactor.util.Logger;
import reactor.util.Loggers;

import java.time.Duration;
import java.time.Instant;
import java.util.List;
import java.util.function.Function;

import static discord4j.common.LogUtil.format;

/**
 * A stream of {@link DiscordWebRequest DiscordRequests}. Any number of items may be {@link #push(RequestCorrelation)}
 * written to the stream. However, the {@link RequestSubscriber} ensures that only one is read at a time. This
 * serialization ensures proper rate limit handling.
 * 

* The flow of a request through the stream is as follows: * * */ class RequestStream { private static final Logger log = Loggers.getLogger(RequestStream.class); private final BucketKey id; private final RequestQueue> requestQueue; private final GlobalRateLimiter globalRateLimiter; private final Scheduler timedTaskScheduler; private final List responseFunctions; private final DiscordWebClient httpClient; private final RateLimitStrategy rateLimitStrategy; private final RateLimitRetryOperator rateLimitRetryOperator; RequestStream(BucketKey id, RouterOptions routerOptions, DiscordWebClient httpClient, RateLimitStrategy rateLimitStrategy) { this.id = id; this.requestQueue = routerOptions.getRequestQueueFactory().create(); this.globalRateLimiter = routerOptions.getGlobalRateLimiter(); this.timedTaskScheduler = routerOptions.getReactorResources().getTimerTaskScheduler(); this.responseFunctions = routerOptions.getResponseTransformers(); this.httpClient = httpClient; this.rateLimitStrategy = rateLimitStrategy; this.rateLimitRetryOperator = new RateLimitRetryOperator(timedTaskScheduler); } /** * This retry function is used for reading and completing HTTP requests in the event of a server error (codes * 500, 502, 503 and 504). The delay is calculated using exponential backoff with jitter. */ private Retry serverErrorRetryFactory() { return Retry.onlyIf(ClientException.isRetryContextStatusCode(500, 502, 503, 504)) .withBackoffScheduler(timedTaskScheduler) .exponentialBackoffWithJitter(Duration.ofSeconds(2), Duration.ofSeconds(30)) .doOnRetry(ctx -> { if (log.isDebugEnabled()) { log.debug("Retry {} in bucket {} due to {} for {}", ctx.iteration(), id.toString(), ctx.exception().toString(), ctx.backoff()); } }); } void push(RequestCorrelation request) { requestQueue.push(request); } void start() { requestQueue.requests() .doOnDiscard(RequestCorrelation.class, this::onDiscard) .subscribe(new RequestSubscriber(rateLimitStrategy)); } private void onDiscard(RequestCorrelation requestCorrelation) { requestCorrelation.getResponse().onError(new DiscardedRequestException(requestCorrelation.getRequest())); } /** * Reads and completes one request from the stream at a time. If a request fails, it is retried according a retry * strategy. The reader may wait in between each request if preemptive rate limiting is necessary according to the * response headers. */ private class RequestSubscriber extends BaseSubscriber> { private volatile Duration sleepTime = Duration.ZERO; private final Function> responseFunction; public RequestSubscriber(RateLimitStrategy strategy) { this.responseFunction = response -> { HttpClientResponse httpResponse = response.getHttpResponse(); if (log.isDebugEnabled()) { Instant requestTimestamp = Instant.ofEpochMilli(httpResponse.currentContext().get(DiscordWebClient.KEY_REQUEST_TIMESTAMP)); Duration responseTime = Duration.between(requestTimestamp, Instant.now()); LogUtil.traceDebug(log, trace -> format(httpResponse.currentContext(), "Read " + httpResponse.status() + " in " + responseTime + (!trace ? "" : " with headers: " + httpResponse.responseHeaders()))); } Duration nextReset = strategy.apply(httpResponse); if (!nextReset.isZero()) { if (log.isDebugEnabled()) { log.debug(format(httpResponse.currentContext(), "Delaying next request by {}"), nextReset); } sleepTime = nextReset; } boolean global = Boolean.parseBoolean(httpResponse.responseHeaders().get("X-RateLimit-Global")); Mono action = Mono.empty(); if (global) { long retryAfter = Long.parseLong(httpResponse.responseHeaders().get("Retry-After")); Duration fixedBackoff = Duration.ofMillis(retryAfter); action = globalRateLimiter.rateLimitFor(fixedBackoff) .doOnTerminate(() -> log.debug(format(httpResponse.currentContext(), "Globally rate limited for {}"), fixedBackoff)); } if (httpResponse.status().code() >= 400) { return action.then(response.createException().flatMap(Mono::error)); } else { return action.thenReturn(response); } }; } @Override protected void hookOnSubscribe(Subscription subscription) { request(1); } @Override protected void hookOnNext(RequestCorrelation correlation) { DiscordWebRequest request = correlation.getRequest(); ClientRequest clientRequest = new ClientRequest(request); MonoProcessor callback = correlation.getResponse(); if (log.isDebugEnabled()) { log.debug("[B:{}, R:{}] {}", id.toString(), clientRequest.getId(), clientRequest.getDescription()); } Mono.just(clientRequest) .doOnEach(s -> log.trace(format(s.getContext(), ">> {}"), s)) .flatMap(req -> globalRateLimiter.withLimiter(httpClient.exchange(req) .flatMap(responseFunction)) .next()) .doOnEach(s -> log.trace(format(s.getContext(), "<< {}"), s)) .subscriberContext(ctx -> ctx.putAll(correlation.getContext()) .put(LogUtil.KEY_REQUEST_ID, clientRequest.getId()) .put(LogUtil.KEY_BUCKET_ID, id.toString())) .retryWhen(rateLimitRetryOperator::apply) .transform(getResponseTransformers(request)) .retryWhen(serverErrorRetryFactory()) .doFinally(this::next) .checkpoint("Request to " + clientRequest.getDescription() + " [RequestStream]") .subscribeWith(callback) .subscribe(null, t -> log.trace("Error while processing {}: {}", request, t)); } private Function, Mono> getResponseTransformers(DiscordWebRequest discordRequest) { return responseFunctions.stream() .map(rt -> rt.transform(discordRequest) .andThen(mono -> mono.checkpoint("Apply " + rt + " to " + discordRequest.getDescription() + " [RequestStream]"))) .reduce(Function::andThen) .orElse(mono -> mono); } private void next(SignalType signal) { Mono timer = sleepTime.isZero() ? Mono.just(0L) : Mono.delay(sleepTime, timedTaskScheduler); timer.subscribe(l -> { if (log.isDebugEnabled()) { log.debug("[B:{}] Ready to consume next request after {}", id.toString(), signal); } sleepTime = Duration.ZERO; request(1); }, t -> log.error("[B:{}] Error while scheduling next request", id.toString(), t)); } } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy