jdash.client.request.GDRouterImpl Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of jdash-client Show documentation
Show all versions of jdash-client Show documentation
The HTTP client used to pull data from Geometry Dash servers
The newest version!
package jdash.client.request;
import jdash.client.exception.HttpResponseException;
import org.reactivestreams.Subscription;
import reactor.core.Exceptions;
import reactor.core.publisher.*;
import reactor.core.scheduler.Scheduler;
import reactor.netty.ByteBufFlux;
import reactor.netty.http.client.HttpClient;
import reactor.util.Logger;
import reactor.util.Loggers;
import reactor.util.retry.Retry;
import java.io.IOException;
import java.time.Duration;
import java.util.UUID;
import static reactor.core.publisher.Sinks.EmitFailureHandler.FAIL_FAST;
class GDRouterImpl extends BaseSubscriber implements GDRouter {
private static final Logger LOGGER = Loggers.getLogger(GDRouterImpl.class);
private final RequestLimiter limiter;
private final Duration timeout;
private final Scheduler scheduler;
private final HttpClient httpClient;
private final Sinks.Many requestQueue = Sinks.many().multicast().onBackpressureBuffer();
private final Sinks.Many nextRequestScheduler = Sinks.many().multicast().onBackpressureBuffer();
public GDRouterImpl(RequestLimiter limiter, Duration timeout, String baseUrl, Scheduler scheduler) {
this.limiter = limiter;
this.timeout = timeout;
this.scheduler = scheduler;
var httpClient = HttpClient.create()
.baseUrl(baseUrl)
.headers(h -> {
h.add("Content-Type", "application/x-www-form-urlencoded");
h.add("User-Agent", "");
h.add("Cookie", "gd=1;");
});
if (baseUrl.startsWith("https://")) {
httpClient = httpClient.secure();
}
this.httpClient = httpClient;
requestQueue.asFlux().subscribe(this);
nextRequestScheduler.asFlux()
.flatMap(Mono::delay)
.subscribe(next -> request(1));
}
@Override
public Mono send(GDRequest request) {
final var callback = Sinks.one();
final var requestWithCallback = new RequestWithCallback(request, callback);
final var result = requestQueue.tryEmitNext(requestWithCallback);
if (result.isSuccess()) {
final var mono = callback.asMono().publishOn(scheduler);
if (timeout != null) {
return mono.timeout(timeout);
}
return mono;
}
return Mono.error(new Sinks.EmissionException(result));
}
@Override
protected void hookOnSubscribe(Subscription subscription) {
subscription.request(1);
}
@Override
protected void hookOnNext(RequestWithCallback value) {
limiter.fire();
final var request = value.request();
final var callback = value.callback();
final var requestId = UUID.randomUUID().toString();
httpClient.doAfterRequest((httpClientRequest, connection) -> LOGGER
.debug("[requestId: {}] Request sent: {}", requestId, request))
.post()
.uri(request.getUri())
.send(ByteBufFlux.fromString(Flux.just(request.toRequestString())))
.responseSingle(((httpClientResponse, byteBufMono) -> {
if (httpClientResponse.status().code() / 100 != 2) {
return Mono.error(new HttpResponseException(httpClientResponse.status()));
}
return byteBufMono.asString().defaultIfEmpty("");
}))
.retryWhen(Retry.backoff(10, Duration.ofMillis(100))
.filter(IOException.class::isInstance)
.doAfterRetry(retrySignal -> LOGGER.warn("Retried attempt {}/{} for failed request {} [{}]",
retrySignal.totalRetries(), 10, request, retrySignal.failure())))
.onErrorMap(IOException.class, e -> Exceptions.retryExhausted("Giving up after 10 I/O failures", e))
.doFinally(signalType -> {
final var remaining = limiter.remaining();
if (remaining.remainingPermits() > 0) {
request(1);
} else {
nextRequestScheduler.emitNext(remaining.timeLeftBeforeNextPermit(),
(result, emission) -> emission == Sinks.EmitResult.FAIL_NON_SERIALIZED);
}
})
.subscribe(response -> {
LOGGER.trace("[requestId: {}] Received response: {}", requestId, response);
callback.emitValue(response, FAIL_FAST);
},
error -> callback.emitError(error, FAIL_FAST),
() -> callback.emitEmpty(FAIL_FAST));
}
@Override
protected void hookFinally(SignalType type) {
LOGGER.error("Request queue subscription has been terminated with signal {}", type);
}
}
record RequestWithCallback(GDRequest request, Sinks.One callback) {}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy