13 changed files with 473 additions and 5 deletions
@ -0,0 +1,9 @@ |
|||
package org.thingsboard.server; |
|||
|
|||
import com.google.common.util.concurrent.ListenableFuture; |
|||
|
|||
public interface TbQueueAdmin { |
|||
|
|||
ListenableFuture<Void> createTopicIfNotExists(String topic); |
|||
|
|||
} |
|||
@ -0,0 +1,27 @@ |
|||
/** |
|||
* Copyright © 2016-2020 The Thingsboard Authors |
|||
* |
|||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|||
* you may not use this file except in compliance with the License. |
|||
* You may obtain a copy of the License at |
|||
* |
|||
* http://www.apache.org/licenses/LICENSE-2.0
|
|||
* |
|||
* Unless required by applicable law or agreed to in writing, software |
|||
* distributed under the License is distributed on an "AS IS" BASIS, |
|||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
* See the License for the specific language governing permissions and |
|||
* limitations under the License. |
|||
*/ |
|||
package org.thingsboard.server; |
|||
|
|||
import com.google.common.util.concurrent.ListenableFuture; |
|||
|
|||
/** |
|||
* Created by ashvayka on 05.10.18. |
|||
*/ |
|||
public interface TbQueueHandler<Request extends TbQueueMsg, Response extends TbQueueMsg> { |
|||
|
|||
ListenableFuture<Response> handle(Request request); |
|||
|
|||
} |
|||
@ -1,13 +1,12 @@ |
|||
package org.thingsboard.server; |
|||
|
|||
import java.util.Map; |
|||
import java.util.UUID; |
|||
|
|||
public interface TbQueueMsg { |
|||
|
|||
UUID getKey(); |
|||
|
|||
Map<String, byte[]> getHeaders(); |
|||
TbQueueMsgHeaders getHeaders(); |
|||
|
|||
byte[] getData(); |
|||
} |
|||
|
|||
@ -0,0 +1,8 @@ |
|||
package org.thingsboard.server; |
|||
|
|||
public interface TbQueueMsgHeaders { |
|||
|
|||
byte[] put(String key, byte[] value); |
|||
|
|||
byte[] get(String key); |
|||
} |
|||
@ -0,0 +1,8 @@ |
|||
package org.thingsboard.server; |
|||
|
|||
public interface TbQueueResponseTemplate<Request extends TbQueueMsg, Response extends TbQueueMsg> { |
|||
|
|||
void init(TbQueueHandler<Request, Response> handler); |
|||
|
|||
void stop(); |
|||
} |
|||
@ -0,0 +1,32 @@ |
|||
package org.thingsboard.server.common; |
|||
|
|||
import java.nio.ByteBuffer; |
|||
import java.nio.charset.StandardCharsets; |
|||
import java.util.UUID; |
|||
|
|||
public class AbstractTbQueueTemplate { |
|||
protected static final String REQUEST_ID_HEADER = "requestId"; |
|||
protected static final String RESPONSE_TOPIC_HEADER = "responseTopic"; |
|||
|
|||
protected byte[] uuidToBytes(UUID uuid) { |
|||
ByteBuffer buf = ByteBuffer.allocate(16); |
|||
buf.putLong(uuid.getMostSignificantBits()); |
|||
buf.putLong(uuid.getLeastSignificantBits()); |
|||
return buf.array(); |
|||
} |
|||
|
|||
protected static UUID bytesToUuid(byte[] bytes) { |
|||
ByteBuffer bb = ByteBuffer.wrap(bytes); |
|||
long firstLong = bb.getLong(); |
|||
long secondLong = bb.getLong(); |
|||
return new UUID(firstLong, secondLong); |
|||
} |
|||
|
|||
protected byte[] stringToBytes(String string) { |
|||
return string.getBytes(StandardCharsets.UTF_8); |
|||
} |
|||
|
|||
protected String bytesToString(byte[] data) { |
|||
return new String(data, StandardCharsets.UTF_8); |
|||
} |
|||
} |
|||
@ -0,0 +1,66 @@ |
|||
/** |
|||
* Copyright © 2016-2020 The Thingsboard Authors |
|||
* |
|||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|||
* you may not use this file except in compliance with the License. |
|||
* You may obtain a copy of the License at |
|||
* |
|||
* http://www.apache.org/licenses/LICENSE-2.0
|
|||
* |
|||
* Unless required by applicable law or agreed to in writing, software |
|||
* distributed under the License is distributed on an "AS IS" BASIS, |
|||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
* See the License for the specific language governing permissions and |
|||
* limitations under the License. |
|||
*/ |
|||
package org.thingsboard.server.common; |
|||
|
|||
import com.google.common.util.concurrent.FutureCallback; |
|||
import com.google.common.util.concurrent.Futures; |
|||
import com.google.common.util.concurrent.ListenableFuture; |
|||
|
|||
import java.util.concurrent.Executor; |
|||
import java.util.concurrent.ScheduledExecutorService; |
|||
import java.util.concurrent.TimeUnit; |
|||
import java.util.function.Consumer; |
|||
|
|||
/** |
|||
* Created by ashvayka on 05.10.18. |
|||
*/ |
|||
public class AsyncCallbackTemplate { |
|||
|
|||
public static <T> void withCallbackAndTimeout(ListenableFuture<T> future, |
|||
Consumer<T> onSuccess, |
|||
Consumer<Throwable> onFailure, |
|||
long timeoutInMs, |
|||
ScheduledExecutorService timeoutExecutor, |
|||
Executor callbackExecutor) { |
|||
future = Futures.withTimeout(future, timeoutInMs, TimeUnit.MILLISECONDS, timeoutExecutor); |
|||
withCallback(future, onSuccess, onFailure, callbackExecutor); |
|||
} |
|||
|
|||
public static <T> void withCallback(ListenableFuture<T> future, Consumer<T> onSuccess, |
|||
Consumer<Throwable> onFailure, Executor executor) { |
|||
FutureCallback<T> callback = new FutureCallback<T>() { |
|||
@Override |
|||
public void onSuccess(T result) { |
|||
try { |
|||
onSuccess.accept(result); |
|||
} catch (Throwable th) { |
|||
onFailure(th); |
|||
} |
|||
} |
|||
|
|||
@Override |
|||
public void onFailure(Throwable t) { |
|||
onFailure.accept(t); |
|||
} |
|||
}; |
|||
if (executor != null) { |
|||
Futures.addCallback(future, callback, executor); |
|||
} else { |
|||
Futures.addCallback(future, callback); |
|||
} |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,172 @@ |
|||
package org.thingsboard.server.common; |
|||
|
|||
import com.google.common.util.concurrent.Futures; |
|||
import com.google.common.util.concurrent.ListenableFuture; |
|||
import com.google.common.util.concurrent.SettableFuture; |
|||
import lombok.Builder; |
|||
import lombok.extern.slf4j.Slf4j; |
|||
import org.apache.kafka.common.errors.InterruptException; |
|||
import org.thingsboard.server.TbQueueAdmin; |
|||
import org.thingsboard.server.TbQueueCallback; |
|||
import org.thingsboard.server.TbQueueConsumer; |
|||
import org.thingsboard.server.TbQueueMsg; |
|||
import org.thingsboard.server.TbQueueMsgMetadata; |
|||
import org.thingsboard.server.TbQueueProducer; |
|||
import org.thingsboard.server.TbQueueRequestTemplate; |
|||
|
|||
import java.util.List; |
|||
import java.util.UUID; |
|||
import java.util.concurrent.ConcurrentHashMap; |
|||
import java.util.concurrent.ConcurrentMap; |
|||
import java.util.concurrent.ExecutorService; |
|||
import java.util.concurrent.Executors; |
|||
import java.util.concurrent.TimeoutException; |
|||
|
|||
@Slf4j |
|||
public class DefaultTbQueueRequestTemplate<Request extends TbQueueMsg, Response extends TbQueueMsg> extends AbstractTbQueueTemplate |
|||
implements TbQueueRequestTemplate<Request, Response> { |
|||
|
|||
private final TbQueueAdmin queueAdmin; |
|||
private final TbQueueProducer<Request> requestTemplate; |
|||
private final TbQueueConsumer<Response> responseTemplate; |
|||
private final ConcurrentMap<UUID, DefaultTbQueueRequestTemplate.ResponseMetaData<Response>> pendingRequests; |
|||
private final boolean internalExecutor; |
|||
private final ExecutorService executor; |
|||
private final long maxRequestTimeout; |
|||
private final long maxPendingRequests; |
|||
private final long pollInterval; |
|||
private volatile long tickTs = 0L; |
|||
private volatile long tickSize = 0L; |
|||
private volatile boolean stopped = false; |
|||
|
|||
@Builder |
|||
public DefaultTbQueueRequestTemplate(TbQueueAdmin queueAdmin, |
|||
TbQueueProducer<Request> requestTemplate, |
|||
TbQueueConsumer<Response> responseTemplate, |
|||
long maxRequestTimeout, |
|||
long maxPendingRequests, |
|||
long pollInterval, |
|||
ExecutorService executor) { |
|||
this.queueAdmin = queueAdmin; |
|||
this.requestTemplate = requestTemplate; |
|||
this.responseTemplate = responseTemplate; |
|||
this.pendingRequests = new ConcurrentHashMap<>(); |
|||
this.maxRequestTimeout = maxRequestTimeout; |
|||
this.maxPendingRequests = maxPendingRequests; |
|||
this.pollInterval = pollInterval; |
|||
if (executor != null) { |
|||
internalExecutor = false; |
|||
this.executor = executor; |
|||
} else { |
|||
internalExecutor = true; |
|||
this.executor = Executors.newSingleThreadExecutor(); |
|||
} |
|||
} |
|||
|
|||
public void init() { |
|||
queueAdmin.createTopicIfNotExists(responseTemplate.getTopic()); |
|||
this.requestTemplate.init(); |
|||
tickTs = System.currentTimeMillis(); |
|||
responseTemplate.subscribe(); |
|||
executor.submit(() -> { |
|||
long nextCleanupMs = 0L; |
|||
while (!stopped) { |
|||
try { |
|||
List<Response> responses = responseTemplate.poll(pollInterval); |
|||
if (responses.size() > 0) { |
|||
log.trace("Polling responses completed, consumer records count [{}]", responses.size()); |
|||
} |
|||
responses.forEach(response -> { |
|||
log.trace("Received response to Kafka Template request: {}", response); |
|||
byte[] requestIdHeader = response.getHeaders().get(REQUEST_ID_HEADER); |
|||
UUID requestId = null; |
|||
if (requestIdHeader == null) { |
|||
log.error("[{}] Missing requestId in header and body", response); |
|||
} else { |
|||
requestId = bytesToUuid(requestIdHeader); |
|||
log.trace("[{}] Response received", requestId); |
|||
ResponseMetaData<Response> expectedResponse = pendingRequests.remove(requestId); |
|||
if (expectedResponse == null) { |
|||
log.trace("[{}] Invalid or stale request", requestId); |
|||
} else { |
|||
expectedResponse.future.set(response); |
|||
} |
|||
} |
|||
}); |
|||
responseTemplate.commit(); |
|||
tickTs = System.currentTimeMillis(); |
|||
tickSize = pendingRequests.size(); |
|||
if (nextCleanupMs < tickTs) { |
|||
//cleanup;
|
|||
pendingRequests.forEach((key, value) -> { |
|||
if (value.expTime < tickTs) { |
|||
ResponseMetaData<Response> staleRequest = pendingRequests.remove(key); |
|||
if (staleRequest != null) { |
|||
log.trace("[{}] Request timeout detected, expTime [{}], tickTs [{}]", key, staleRequest.expTime, tickTs); |
|||
staleRequest.future.setException(new TimeoutException()); |
|||
} |
|||
} |
|||
}); |
|||
nextCleanupMs = tickTs + maxRequestTimeout; |
|||
} |
|||
} catch (InterruptException ie) { |
|||
if (!stopped) { |
|||
log.warn("Fetching data from kafka was interrupted.", ie); |
|||
} |
|||
} catch (Throwable e) { |
|||
log.warn("Failed to obtain responses from queue.", e); |
|||
try { |
|||
Thread.sleep(pollInterval); |
|||
} catch (InterruptedException e2) { |
|||
log.trace("Failed to wait until the server has capacity to handle new responses", e2); |
|||
} |
|||
} |
|||
} |
|||
}); |
|||
} |
|||
|
|||
public void stop() { |
|||
stopped = true; |
|||
if (internalExecutor) { |
|||
executor.shutdownNow(); |
|||
} |
|||
} |
|||
|
|||
@Override |
|||
public ListenableFuture<Response> send(String key, Request request) { |
|||
if (tickSize > maxPendingRequests) { |
|||
return Futures.immediateFailedFuture(new RuntimeException("Pending request map is full!")); |
|||
} |
|||
UUID requestId = UUID.randomUUID(); |
|||
request.getHeaders().put(REQUEST_ID_HEADER, uuidToBytes(requestId)); |
|||
request.getHeaders().put(RESPONSE_TOPIC_HEADER, stringToBytes(responseTemplate.getTopic())); |
|||
SettableFuture<Response> future = SettableFuture.create(); |
|||
ResponseMetaData<Response> responseMetaData = new ResponseMetaData<>(tickTs + maxRequestTimeout, future); |
|||
pendingRequests.putIfAbsent(requestId, responseMetaData); |
|||
log.trace("[{}] Sending request, key [{}], expTime [{}]", requestId, key, responseMetaData.expTime); |
|||
requestTemplate.send(request, new TbQueueCallback() { |
|||
@Override |
|||
public void onSuccess(TbQueueMsgMetadata metadata) { |
|||
log.trace("[{}] Request sent: {}", requestId, metadata); |
|||
} |
|||
|
|||
@Override |
|||
public void onFailure(Throwable t) { |
|||
pendingRequests.remove(requestId); |
|||
future.setException(t); |
|||
} |
|||
}); |
|||
return future; |
|||
} |
|||
|
|||
private static class ResponseMetaData<T> { |
|||
private final long expTime; |
|||
private final SettableFuture<T> future; |
|||
|
|||
ResponseMetaData(long ts, SettableFuture<T> future) { |
|||
this.expTime = ts; |
|||
this.future = future; |
|||
} |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,137 @@ |
|||
package org.thingsboard.server.common; |
|||
|
|||
import lombok.Builder; |
|||
import lombok.extern.slf4j.Slf4j; |
|||
import org.apache.kafka.common.errors.InterruptException; |
|||
import org.thingsboard.server.TbQueueConsumer; |
|||
import org.thingsboard.server.TbQueueHandler; |
|||
import org.thingsboard.server.TbQueueMsg; |
|||
import org.thingsboard.server.TbQueueProducer; |
|||
import org.thingsboard.server.TbQueueResponseTemplate; |
|||
|
|||
import java.util.List; |
|||
import java.util.UUID; |
|||
import java.util.concurrent.ConcurrentHashMap; |
|||
import java.util.concurrent.ConcurrentMap; |
|||
import java.util.concurrent.ExecutorService; |
|||
import java.util.concurrent.Executors; |
|||
import java.util.concurrent.ScheduledExecutorService; |
|||
import java.util.concurrent.TimeoutException; |
|||
import java.util.concurrent.atomic.AtomicInteger; |
|||
|
|||
@Slf4j |
|||
public class DefaultTbQueueResponseTemplate<Request extends TbQueueMsg, Response extends TbQueueMsg> extends AbstractTbQueueTemplate |
|||
implements TbQueueResponseTemplate<Request, Response> { |
|||
|
|||
private final TbQueueConsumer<Request> requestTemplate; |
|||
private final TbQueueProducer<Response> responseTemplate; |
|||
private final ConcurrentMap<UUID, String> pendingRequests; |
|||
private final ExecutorService loopExecutor; |
|||
private final ScheduledExecutorService timeoutExecutor; |
|||
private final ExecutorService callbackExecutor; |
|||
private final int maxPendingRequests; |
|||
private final long requestTimeout; |
|||
|
|||
private final long pollInterval; |
|||
private volatile boolean stopped = false; |
|||
private final AtomicInteger pendingRequestCount = new AtomicInteger(); |
|||
|
|||
@Builder |
|||
public DefaultTbQueueResponseTemplate(TbQueueConsumer<Request> requestTemplate, |
|||
TbQueueProducer<Response> responseTemplate, |
|||
TbQueueHandler<Request, Response> handler, |
|||
long pollInterval, |
|||
long requestTimeout, |
|||
int maxPendingRequests, |
|||
ExecutorService executor) { |
|||
this.requestTemplate = requestTemplate; |
|||
this.responseTemplate = responseTemplate; |
|||
this.pendingRequests = new ConcurrentHashMap<>(); |
|||
this.maxPendingRequests = maxPendingRequests; |
|||
this.pollInterval = pollInterval; |
|||
this.requestTimeout = requestTimeout; |
|||
this.callbackExecutor = executor; |
|||
this.timeoutExecutor = Executors.newSingleThreadScheduledExecutor(); |
|||
this.loopExecutor = Executors.newSingleThreadExecutor(); |
|||
} |
|||
|
|||
@Override |
|||
public void init(TbQueueHandler<Request, Response> handler) { |
|||
this.responseTemplate.init(); |
|||
requestTemplate.subscribe(); |
|||
loopExecutor.submit(() -> { |
|||
while (!stopped) { |
|||
try { |
|||
while (pendingRequestCount.get() >= maxPendingRequests) { |
|||
try { |
|||
Thread.sleep(pollInterval); |
|||
} catch (InterruptedException e) { |
|||
log.trace("Failed to wait until the server has capacity to handle new requests", e); |
|||
} |
|||
} |
|||
List<Request> requests = requestTemplate.poll(pollInterval); |
|||
requests.forEach(request -> { |
|||
byte[] requestIdHeader = request.getHeaders().get(REQUEST_ID_HEADER); |
|||
if (requestIdHeader == null) { |
|||
log.error("[{}] Missing requestId in header", request); |
|||
return; |
|||
} |
|||
byte[] responseTopicHeader = request.getHeaders().get(RESPONSE_TOPIC_HEADER); |
|||
if (responseTopicHeader == null) { |
|||
log.error("[{}] Missing response topic in header", request); |
|||
return; |
|||
} |
|||
UUID requestId = bytesToUuid(requestIdHeader); |
|||
String responseTopic = bytesToString(responseTopicHeader); |
|||
try { |
|||
pendingRequestCount.getAndIncrement(); |
|||
AsyncCallbackTemplate.withCallbackAndTimeout(handler.handle(request), |
|||
response -> { |
|||
pendingRequestCount.decrementAndGet(); |
|||
response.getHeaders().put(REQUEST_ID_HEADER, uuidToBytes(requestId)); |
|||
responseTemplate.send(responseTopic, response, null); |
|||
}, |
|||
e -> { |
|||
pendingRequestCount.decrementAndGet(); |
|||
if (e.getCause() != null && e.getCause() instanceof TimeoutException) { |
|||
log.warn("[{}] Timeout to process the request: {}", requestId, request, e); |
|||
} else { |
|||
log.trace("[{}] Failed to process the request: {}", requestId, request, e); |
|||
} |
|||
}, |
|||
requestTimeout, |
|||
timeoutExecutor, |
|||
callbackExecutor); |
|||
} catch (Throwable e) { |
|||
pendingRequestCount.decrementAndGet(); |
|||
log.warn("[{}] Failed to process the request: {}", requestId, request, e); |
|||
} |
|||
}); |
|||
requestTemplate.commit(); |
|||
} catch (InterruptException ie) { |
|||
if (!stopped) { |
|||
log.warn("Fetching data from queue was interrupted.", ie); |
|||
} |
|||
} catch (Throwable e) { |
|||
log.warn("Failed to obtain messages from queue.", e); |
|||
try { |
|||
Thread.sleep(pollInterval); |
|||
} catch (InterruptedException e2) { |
|||
log.trace("Failed to wait until the server has capacity to handle new requests", e2); |
|||
} |
|||
} |
|||
} |
|||
}); |
|||
} |
|||
|
|||
public void stop() { |
|||
stopped = true; |
|||
if (timeoutExecutor != null) { |
|||
timeoutExecutor.shutdownNow(); |
|||
} |
|||
if (loopExecutor != null) { |
|||
loopExecutor.shutdownNow(); |
|||
} |
|||
} |
|||
|
|||
} |
|||
Loading…
Reference in new issue