2 * Copyright (c) 2010-2023 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
7 * This program and the accompanying materials are made available under the
8 * terms of the Eclipse Public License 2.0 which is available at
9 * http://www.eclipse.org/legal/epl-2.0
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.http.internal.http;
16 import java.util.concurrent.CancellationException;
17 import java.util.concurrent.CompletableFuture;
18 import java.util.concurrent.LinkedBlockingQueue;
19 import java.util.concurrent.RejectedExecutionException;
20 import java.util.concurrent.ScheduledExecutorService;
21 import java.util.concurrent.ScheduledFuture;
22 import java.util.concurrent.TimeUnit;
24 import org.eclipse.jdt.annotation.NonNullByDefault;
25 import org.eclipse.jdt.annotation.Nullable;
26 import org.eclipse.jetty.client.HttpClient;
27 import org.eclipse.jetty.client.api.AuthenticationStore;
28 import org.eclipse.jetty.client.api.Request;
29 import org.eclipse.jetty.client.util.StringContentProvider;
30 import org.eclipse.jetty.http.HttpMethod;
33 * The {@link RateLimitedHttpClient} is a wrapper for a Jetty HTTP client that limits the number of requests by delaying
34 * the request creation
36 * @author Jan N. Klug - Initial contribution
39 public class RateLimitedHttpClient {
40 private static final int MAX_QUEUE_SIZE = 1000; // maximum queue size
41 private HttpClient httpClient;
42 private int delay = 0; // in ms
43 private final ScheduledExecutorService scheduler;
44 private final LinkedBlockingQueue<RequestQueueEntry> requestQueue = new LinkedBlockingQueue<>(MAX_QUEUE_SIZE);
46 private @Nullable ScheduledFuture<?> processJob;
48 public RateLimitedHttpClient(HttpClient httpClient, ScheduledExecutorService scheduler) {
49 this.httpClient = httpClient;
50 this.scheduler = scheduler;
54 * Stop processing the queue and clear it
56 public void shutdown() {
58 requestQueue.forEach(queueEntry -> queueEntry.future.completeExceptionally(new CancellationException()));
64 * @param delay in ms between to requests
66 public void setDelay(int delay) {
68 throw new IllegalArgumentException("Delay needs to be larger or equal to zero");
73 processJob = scheduler.scheduleWithFixedDelay(this::processQueue, 0, delay, TimeUnit.MILLISECONDS);
80 * @param httpClient secure or insecure Jetty http client
82 public void setHttpClient(HttpClient httpClient) {
83 this.httpClient = httpClient;
87 * Create a new request to the given URL respecting rate-limits
89 * @param finalUrl the request URL
90 * @param method http request method GET/PUT/POST
91 * @param content the content (if method PUT/POST)
92 * @return a CompletableFuture that completes with the request
94 public CompletableFuture<Request> newRequest(URI finalUrl, HttpMethod method, String content) {
95 // if no delay is set, return a completed CompletableFuture
96 CompletableFuture<Request> future = new CompletableFuture<>();
97 RequestQueueEntry queueEntry = new RequestQueueEntry(finalUrl, method, content, future);
99 queueEntry.completeFuture(httpClient);
101 if (!requestQueue.offer(queueEntry)) {
102 future.completeExceptionally(new RejectedExecutionException("Maximum queue size exceeded."));
109 * Get the AuthenticationStore from the wrapped client
113 public AuthenticationStore getAuthenticationStore() {
114 return httpClient.getAuthenticationStore();
117 private void stopProcessJob() {
118 ScheduledFuture<?> processJob = this.processJob;
119 if (processJob != null) {
120 processJob.cancel(false);
121 this.processJob = null;
125 private void processQueue() {
126 RequestQueueEntry queueEntry = requestQueue.poll();
127 if (queueEntry != null) {
128 queueEntry.completeFuture(httpClient);
132 private static class RequestQueueEntry {
133 private URI finalUrl;
134 private HttpMethod method;
135 private String content;
136 private CompletableFuture<Request> future;
138 public RequestQueueEntry(URI finalUrl, HttpMethod method, String content, CompletableFuture<Request> future) {
139 this.finalUrl = finalUrl;
140 this.method = method;
141 this.content = content;
142 this.future = future;
146 * complete the future with a request
148 * @param httpClient the client to create the request
150 public void completeFuture(HttpClient httpClient) {
151 Request request = httpClient.newRequest(finalUrl).method(method);
152 if (method != HttpMethod.GET && !content.isEmpty()) {
153 request.content(new StringContentProvider(content));
155 future.complete(request);