Add action queuing and rate limit handling
This commit is contained in:
parent
93fde2621d
commit
5e99ef7fea
@ -25,10 +25,16 @@ package me.braydon.pelican.action;
|
||||
|
||||
import lombok.AccessLevel;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
import lombok.NonNull;
|
||||
import me.braydon.pelican.client.ClientConfig;
|
||||
import me.braydon.pelican.model.PanelModel;
|
||||
import me.braydon.pelican.request.JsonWebRequest;
|
||||
import me.braydon.pelican.request.WebRequestHandler;
|
||||
import me.braydon.pelican.request.RateLimitHandler;
|
||||
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.function.BiConsumer;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
/**
|
||||
* An action that can be executed on a panel.
|
||||
@ -39,14 +45,19 @@ import me.braydon.pelican.request.WebRequestHandler;
|
||||
@AllArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
public class PanelAction<T extends PanelModel<T>> {
|
||||
/**
|
||||
* The web handler to execute actions with.
|
||||
* The client config to use for executing actions.
|
||||
*/
|
||||
@NonNull private final WebRequestHandler requestHandler;
|
||||
@NonNull private final ClientConfig clientConfig;
|
||||
|
||||
/**
|
||||
* The rate limit handler to use for querying actions.
|
||||
*/
|
||||
@NonNull private final RateLimitHandler rateLimitHandler;
|
||||
|
||||
/**
|
||||
* The web request this action will execute.
|
||||
*/
|
||||
@NonNull private final JsonWebRequest webRequest;
|
||||
@Getter @NonNull private final JsonWebRequest webRequest;
|
||||
|
||||
/**
|
||||
* The type of response expected when
|
||||
@ -54,27 +65,56 @@ public class PanelAction<T extends PanelModel<T>> {
|
||||
*/
|
||||
private final Class<T> responseType;
|
||||
|
||||
/**
|
||||
* Queue this action and await its response.
|
||||
* The given callback will be invoked once
|
||||
* a response has been received from the panel.
|
||||
*
|
||||
* @param callback the callback to invoke
|
||||
*/
|
||||
public void queue(@NonNull Consumer<T> callback) {
|
||||
queue((res, ex) -> {
|
||||
if (ex != null) {
|
||||
ex.printStackTrace();
|
||||
}
|
||||
callback.accept(res);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Queue this action and await its response.
|
||||
* The given callback will be invoked once
|
||||
* a response has been received from the panel.
|
||||
*
|
||||
* @param callback the callback to invoke
|
||||
*/
|
||||
public void queue(@NonNull BiConsumer<T, Exception> callback) {
|
||||
CompletableFuture.runAsync(() -> rateLimitHandler.tryRequest(this, callback, false, true));
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute this action instantly.
|
||||
*
|
||||
* @return the response, null if none
|
||||
*/
|
||||
public T execute() {
|
||||
return requestHandler.handle(webRequest, responseType);
|
||||
return webRequest.execute(clientConfig, responseType);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new panel action.
|
||||
*
|
||||
* @param requestHandler the request handler to use
|
||||
* @param clientConfig the client config to use for executing actions
|
||||
* @param rateLimitHandler the rate limit handler to use for querying actions
|
||||
* @param webRequest the web request to send for this action
|
||||
* @param responseType the expected response type, null if none
|
||||
* @param <T> the response type
|
||||
* @return the panel action
|
||||
*/
|
||||
@NonNull
|
||||
public static <T extends PanelModel<T>> PanelAction<T> create(@NonNull WebRequestHandler requestHandler,
|
||||
public static <T extends PanelModel<T>> PanelAction<T> create(@NonNull ClientConfig clientConfig,
|
||||
@NonNull RateLimitHandler rateLimitHandler,
|
||||
@NonNull JsonWebRequest webRequest, Class<T> responseType) {
|
||||
return new PanelAction<>(requestHandler, webRequest, responseType);
|
||||
return new PanelAction<>(clientConfig, rateLimitHandler, webRequest, responseType);
|
||||
}
|
||||
}
|
@ -28,7 +28,8 @@ import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
import lombok.NonNull;
|
||||
import lombok.experimental.Accessors;
|
||||
import me.braydon.pelican.request.WebRequestHandler;
|
||||
import me.braydon.pelican.client.ClientConfig;
|
||||
import me.braydon.pelican.request.RateLimitHandler;
|
||||
|
||||
/**
|
||||
* A set of actions that can
|
||||
@ -39,7 +40,12 @@ import me.braydon.pelican.request.WebRequestHandler;
|
||||
@AllArgsConstructor @Getter(AccessLevel.PROTECTED) @Accessors(fluent = true)
|
||||
public abstract class PanelActions {
|
||||
/**
|
||||
* The request handler to use for action execution.
|
||||
* The client config to use for executing actions.
|
||||
*/
|
||||
@NonNull private final WebRequestHandler requestHandler;
|
||||
@NonNull private final ClientConfig clientConfig;
|
||||
|
||||
/**
|
||||
* The rate limit handler to use for querying actions.
|
||||
*/
|
||||
@NonNull private final RateLimitHandler rateLimitHandler;
|
||||
}
|
@ -25,7 +25,8 @@ package me.braydon.pelican.action.pelican;
|
||||
|
||||
import lombok.NonNull;
|
||||
import me.braydon.pelican.action.pterodactyl.PteroPanelActions;
|
||||
import me.braydon.pelican.request.WebRequestHandler;
|
||||
import me.braydon.pelican.client.ClientConfig;
|
||||
import me.braydon.pelican.request.RateLimitHandler;
|
||||
|
||||
/**
|
||||
* Implemented actions for the Pelican panel.
|
||||
@ -34,7 +35,7 @@ import me.braydon.pelican.request.WebRequestHandler;
|
||||
* @see <a href="https://pelican.dev">Pelican Website</a>
|
||||
*/
|
||||
public class PelicanPanelActions extends PteroPanelActions {
|
||||
public PelicanPanelActions(@NonNull WebRequestHandler requestHandler) {
|
||||
super(requestHandler);
|
||||
public PelicanPanelActions(@NonNull ClientConfig clientConfig, @NonNull RateLimitHandler rateLimitHandler) {
|
||||
super(clientConfig, rateLimitHandler);
|
||||
}
|
||||
}
|
@ -28,7 +28,8 @@ import lombok.NonNull;
|
||||
import lombok.experimental.Accessors;
|
||||
import me.braydon.pelican.action.PanelActions;
|
||||
import me.braydon.pelican.action.pterodactyl.application.ApplicationNodeActions;
|
||||
import me.braydon.pelican.request.WebRequestHandler;
|
||||
import me.braydon.pelican.client.ClientConfig;
|
||||
import me.braydon.pelican.request.RateLimitHandler;
|
||||
|
||||
/**
|
||||
* Implemented actions for the Pterodactyl panel.
|
||||
@ -43,8 +44,8 @@ public class PteroPanelActions extends PanelActions {
|
||||
*/
|
||||
@NonNull private final Application application;
|
||||
|
||||
public PteroPanelActions(@NonNull WebRequestHandler requestHandler) {
|
||||
super(requestHandler);
|
||||
public PteroPanelActions(@NonNull ClientConfig clientConfig, @NonNull RateLimitHandler rateLimitHandler) {
|
||||
super(clientConfig, rateLimitHandler);
|
||||
this.application = new Application();
|
||||
}
|
||||
|
||||
@ -56,6 +57,6 @@ public class PteroPanelActions extends PanelActions {
|
||||
/**
|
||||
* Node actions for the application.
|
||||
*/
|
||||
@NonNull private final ApplicationNodeActions nodes = new ApplicationNodeActions(requestHandler());
|
||||
@NonNull private final ApplicationNodeActions nodes = new ApplicationNodeActions(clientConfig(), rateLimitHandler());
|
||||
}
|
||||
}
|
@ -26,9 +26,10 @@ package me.braydon.pelican.action.pterodactyl.application;
|
||||
import lombok.NonNull;
|
||||
import me.braydon.pelican.action.PanelAction;
|
||||
import me.braydon.pelican.action.PanelActions;
|
||||
import me.braydon.pelican.client.ClientConfig;
|
||||
import me.braydon.pelican.model.Node;
|
||||
import me.braydon.pelican.request.JsonWebRequest;
|
||||
import me.braydon.pelican.request.WebRequestHandler;
|
||||
import me.braydon.pelican.request.RateLimitHandler;
|
||||
|
||||
/**
|
||||
* Application node actions
|
||||
@ -37,8 +38,8 @@ import me.braydon.pelican.request.WebRequestHandler;
|
||||
* @author Braydon
|
||||
*/
|
||||
public final class ApplicationNodeActions extends PanelActions {
|
||||
public ApplicationNodeActions(@NonNull WebRequestHandler requestHandler) {
|
||||
super(requestHandler);
|
||||
public ApplicationNodeActions(@NonNull ClientConfig clientConfig, @NonNull RateLimitHandler rateLimitHandler) {
|
||||
super(clientConfig, rateLimitHandler);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -49,7 +50,7 @@ public final class ApplicationNodeActions extends PanelActions {
|
||||
* @return the action
|
||||
*/
|
||||
public PanelAction<Node> details(int id) {
|
||||
return PanelAction.create(requestHandler(), JsonWebRequest.builder()
|
||||
return PanelAction.create(clientConfig(), rateLimitHandler(), JsonWebRequest.builder()
|
||||
.endpoint("/application/nodes/" + id)
|
||||
.build(), Node.class);
|
||||
}
|
||||
|
@ -31,7 +31,7 @@ import lombok.extern.slf4j.Slf4j;
|
||||
import me.braydon.pelican.action.PanelActions;
|
||||
import me.braydon.pelican.action.pelican.PelicanPanelActions;
|
||||
import me.braydon.pelican.action.pterodactyl.PteroPanelActions;
|
||||
import me.braydon.pelican.request.WebRequestHandler;
|
||||
import me.braydon.pelican.request.RateLimitHandler;
|
||||
|
||||
import java.io.Closeable;
|
||||
|
||||
@ -57,7 +57,9 @@ public final class Pelican4J<A extends PanelActions> implements Closeable {
|
||||
@SneakyThrows
|
||||
private Pelican4J(@NonNull ClientConfig config, @NonNull Class<A> actionsClass) {
|
||||
this.config = config;
|
||||
actions = actionsClass.getConstructor(WebRequestHandler.class).newInstance(new WebRequestHandler(config));
|
||||
actions = actionsClass.getConstructor(ClientConfig.class, RateLimitHandler.class).newInstance(
|
||||
config, new RateLimitHandler(config)
|
||||
);
|
||||
if (config.debugging()) {
|
||||
log.debug("Created a new {} client: {}", actionsClass == PelicanPanelActions.class ? "Pelican" : "Ptero", config);
|
||||
}
|
||||
|
@ -29,6 +29,8 @@ import com.google.gson.JsonObject;
|
||||
import lombok.Builder;
|
||||
import lombok.NonNull;
|
||||
import lombok.SneakyThrows;
|
||||
import lombok.ToString;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import me.braydon.pelican.client.ClientConfig;
|
||||
import me.braydon.pelican.exception.PanelAPIException;
|
||||
import me.braydon.pelican.model.PanelModel;
|
||||
@ -39,7 +41,8 @@ import okhttp3.*;
|
||||
*
|
||||
* @author Braydon
|
||||
*/
|
||||
@Builder
|
||||
@Builder @ToString
|
||||
@Slf4j(topic = "Web Request")
|
||||
public class JsonWebRequest {
|
||||
private static final OkHttpClient HTTP_CLIENT = new OkHttpClient();
|
||||
private static final MediaType JSON_MEDIA = MediaType.get("application/json");
|
||||
@ -71,10 +74,17 @@ public class JsonWebRequest {
|
||||
* @param <T> the response type
|
||||
*/
|
||||
@SneakyThrows
|
||||
protected <T extends PanelModel<T>> T execute(@NonNull ClientConfig clientConfig, Class<T> responseType) {
|
||||
public <T extends PanelModel<T>> T execute(@NonNull ClientConfig clientConfig, Class<T> responseType) {
|
||||
String endpoint = clientConfig.panelUrl() + "/api" + this.endpoint;
|
||||
if (clientConfig.debugging()) {
|
||||
log.debug("Sending a {} request to {}...", method, endpoint);
|
||||
if (body != null) {
|
||||
log.debug("With Body: {}", body);
|
||||
}
|
||||
}
|
||||
Request request = new Request.Builder()
|
||||
.method(method.name(), body == null ? null : RequestBody.create(body, JSON_MEDIA))
|
||||
.url(clientConfig.panelUrl() + "/api" + endpoint)
|
||||
.url(endpoint)
|
||||
.addHeader("Content-Type", "application/json")
|
||||
.addHeader("Accept", "Application/vnd.pterodactyl.v1+json")
|
||||
.addHeader("Authorization", "Bearer " + clientConfig.apiKey())
|
||||
@ -85,6 +95,10 @@ public class JsonWebRequest {
|
||||
int status = response.code(); // The HTTP response code
|
||||
String json = response.body().string(); // The json response
|
||||
|
||||
if (clientConfig.debugging()) {
|
||||
log.debug("Receive response: {}", status);
|
||||
}
|
||||
|
||||
// If the status is not 200 (OK), handle the error
|
||||
if (status != 200) {
|
||||
JsonObject errorJsonObject = GSON.fromJson(json, JsonObject.class);
|
||||
|
@ -23,33 +23,43 @@
|
||||
*/
|
||||
package me.braydon.pelican.request;
|
||||
|
||||
import lombok.AccessLevel;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
import lombok.NonNull;
|
||||
import me.braydon.pelican.client.ClientConfig;
|
||||
import me.braydon.pelican.action.PanelAction;
|
||||
import me.braydon.pelican.model.PanelModel;
|
||||
|
||||
import java.util.function.BiConsumer;
|
||||
|
||||
/**
|
||||
* The handler for processing web requests.
|
||||
* Represents a queued action.
|
||||
*
|
||||
* @author Braydon
|
||||
*/
|
||||
@AllArgsConstructor
|
||||
public final class WebRequestHandler {
|
||||
@AllArgsConstructor(access = AccessLevel.PROTECTED) @Getter
|
||||
public class QueuedAction<T extends PanelModel<T>> {
|
||||
/**
|
||||
* The client config used to make requests.
|
||||
* The action that's queued.
|
||||
*/
|
||||
@NonNull private final ClientConfig clientConfig;
|
||||
@NonNull private final PanelAction<?> action;
|
||||
|
||||
/**
|
||||
* Handle the given web request.
|
||||
*
|
||||
* @param request the request to handle
|
||||
* @param responseType the expected response type, null if none
|
||||
* @return the response, null if none
|
||||
* @param <T> the response type
|
||||
* The callback to invoke when
|
||||
* this action is executed.
|
||||
*/
|
||||
public <T extends PanelModel<T>> T handle(@NonNull JsonWebRequest request, Class<T> responseType) {
|
||||
// TODO: handle rate limit handling for async reqs
|
||||
return request.execute(clientConfig, responseType);
|
||||
@NonNull private final BiConsumer<T, Exception> callback;
|
||||
|
||||
/**
|
||||
* The amount of times this
|
||||
* action has been retried.
|
||||
*/
|
||||
private int retries;
|
||||
|
||||
/**
|
||||
* Increment the retry count.
|
||||
*/
|
||||
protected void incrementRetries() {
|
||||
retries++;
|
||||
}
|
||||
}
|
146
src/main/java/me/braydon/pelican/request/RateLimitHandler.java
Normal file
146
src/main/java/me/braydon/pelican/request/RateLimitHandler.java
Normal file
@ -0,0 +1,146 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2024 Braydon (Rainnny).
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
package me.braydon.pelican.request;
|
||||
|
||||
import lombok.NonNull;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import me.braydon.pelican.action.PanelAction;
|
||||
import me.braydon.pelican.client.ClientConfig;
|
||||
import me.braydon.pelican.exception.PanelAPIException;
|
||||
import me.braydon.pelican.model.PanelModel;
|
||||
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Timer;
|
||||
import java.util.TimerTask;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.function.BiConsumer;
|
||||
|
||||
/**
|
||||
* Responsible for handling panel rate limits.
|
||||
*
|
||||
* @author Braydon
|
||||
*/
|
||||
@Slf4j(topic = "Rate Limit Handler")
|
||||
public final class RateLimitHandler {
|
||||
/**
|
||||
* The interval at which requests should be retried at if they are rate limited.
|
||||
*/
|
||||
private static final long INTERVAL = TimeUnit.SECONDS.toMillis(2L);
|
||||
|
||||
/**
|
||||
* The amount of times a queued action should
|
||||
* be retried before failing the action.
|
||||
*/
|
||||
private static final int MAX_RETRIES = 25;
|
||||
|
||||
/**
|
||||
* The client config to use.
|
||||
*/
|
||||
@NonNull private final ClientConfig clientConfig;
|
||||
|
||||
/**
|
||||
* The currently queued actions, awaiting to be re-tried.
|
||||
*/
|
||||
private final List<QueuedAction<?>> queuedActions = new LinkedList<>();
|
||||
|
||||
public RateLimitHandler(@NonNull ClientConfig clientConfig) {
|
||||
this.clientConfig = clientConfig;
|
||||
|
||||
// Schedule a task to process the queue.
|
||||
// This will take the first element in the queue
|
||||
// every X interval, and try to execute it. Each
|
||||
// time the action fails, the retry count will be
|
||||
// incremented. If the action is successful, or reaches
|
||||
// the max retries, it will be removed from the queue.
|
||||
new Timer().scheduleAtFixedRate(new TimerTask() {
|
||||
@Override
|
||||
public void run() {
|
||||
int queueSize = queuedActions.size(); // The size of the queue
|
||||
if (queueSize == 0) { // Queue is empty
|
||||
return;
|
||||
}
|
||||
if (clientConfig.debugging()) {
|
||||
log.debug("Processing queue of size {}...", queueSize);
|
||||
}
|
||||
QueuedAction<?> queuedAction = queuedActions.get(0); // Get the first queued action
|
||||
boolean maxedRetries = queuedAction.getRetries() >= MAX_RETRIES; // If the action has maxed retries
|
||||
|
||||
// Re-try the queued action, and if it fails, increment the retry count and move on
|
||||
if (!tryRequest(queuedAction.getAction(), queuedAction.getCallback(), maxedRetries, false)) {
|
||||
queuedAction.incrementRetries();
|
||||
if (!maxedRetries) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
// The queued action was either successful, or
|
||||
// reached the max retries, remove it from the queue
|
||||
queuedActions.remove(0);
|
||||
}
|
||||
}, INTERVAL, INTERVAL);
|
||||
}
|
||||
|
||||
/**
|
||||
* Try and execute the given action.
|
||||
*
|
||||
* @param action the action to try
|
||||
* @param callback the callback to invoke
|
||||
* @param throwRateLimitErrors whether rate limit errors should be thrown
|
||||
* @param retry should the action be retried if a rate limit is hit (and being thrown)?
|
||||
* @return whether the request was successful
|
||||
* @param <T> the action response type
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public <T extends PanelModel<T>> boolean tryRequest(@NonNull PanelAction<?> action, BiConsumer<T, Exception> callback, boolean throwRateLimitErrors, boolean retry) {
|
||||
try { // Try and execute the request
|
||||
if (clientConfig.debugging()) {
|
||||
log.debug("Trying to execute action {}...", action.getWebRequest());
|
||||
}
|
||||
callback.accept((T) action.execute(), null);
|
||||
return true; // Success
|
||||
} catch (Exception ex) {
|
||||
// The API rate limit has been reached, queue
|
||||
// the task to be re-tried later
|
||||
if (ex instanceof PanelAPIException) {
|
||||
PanelAPIException apiException = (PanelAPIException) ex;
|
||||
if (apiException.getCode() == 404 && !throwRateLimitErrors) {
|
||||
if (clientConfig.debugging()) {
|
||||
log.debug("Panel API rate limit exceeded{}", retry ? ", queued action to be re-tried later..." : "");
|
||||
}
|
||||
if (retry) {
|
||||
queuedActions.add(new QueuedAction<>(action, callback, 0));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
// Not a rate limit, invoke
|
||||
// the callback with the error
|
||||
if (clientConfig.debugging()) {
|
||||
log.debug("Failed to execute action:", ex);
|
||||
}
|
||||
callback.accept(null, ex);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
@ -23,11 +23,10 @@
|
||||
*/
|
||||
package me.braydon.pelican.test;
|
||||
|
||||
import lombok.SneakyThrows;
|
||||
import me.braydon.pelican.action.pelican.PelicanPanelActions;
|
||||
import me.braydon.pelican.client.ClientConfig;
|
||||
import me.braydon.pelican.client.Pelican4J;
|
||||
import me.braydon.pelican.exception.PanelAPIException;
|
||||
import me.braydon.pelican.model.Node;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Test;
|
||||
@ -65,11 +64,12 @@ public final class PelicanActionTests {
|
||||
* Test getting a list of
|
||||
* nodes from the panel.
|
||||
*/
|
||||
@Test
|
||||
@Test @SneakyThrows
|
||||
void testGetNodes() {
|
||||
Node node = client.actions().application().nodes().details(-1).execute();
|
||||
System.out.println("node = " + node);
|
||||
// TODO: ...
|
||||
client.actions().application().nodes().details(1).queue(node -> {
|
||||
System.out.println("node = " + node);
|
||||
});
|
||||
Thread.sleep(60000L);
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user