com.hazelcast.client.spi.impl.ClientResponseHandlerSupplier Maven / Gradle / Ivy
/*
* Copyright (c) 2008-2020, Hazelcast, Inc. All Rights Reserved.
*
* 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 com.hazelcast.client.spi.impl;
import com.hazelcast.client.impl.clientside.HazelcastClientInstanceImpl;
import com.hazelcast.client.impl.protocol.ClientMessage;
import com.hazelcast.client.impl.protocol.codec.ErrorCodec;
import com.hazelcast.internal.util.concurrent.MPSCQueue;
import com.hazelcast.logging.ILogger;
import com.hazelcast.spi.properties.HazelcastProperties;
import com.hazelcast.spi.properties.HazelcastProperty;
import com.hazelcast.util.MutableInteger;
import com.hazelcast.util.function.Consumer;
import com.hazelcast.util.function.Supplier;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.atomic.AtomicBoolean;
import static com.hazelcast.client.spi.properties.ClientProperty.RESPONSE_THREAD_COUNT;
import static com.hazelcast.client.spi.properties.ClientProperty.RESPONSE_THREAD_DYNAMIC;
import static com.hazelcast.instance.OutOfMemoryErrorDispatcher.onOutOfMemory;
import static com.hazelcast.spi.impl.operationservice.impl.InboundResponseHandlerSupplier.getIdleStrategy;
import static com.hazelcast.util.HashUtil.hashToIndex;
/**
* A {@link Supplier} for {@link Supplier} instance that processes responses for client
* invocations.
*
* Depending on the configuration the supplier provides:
*
* - a on thread ClientResponseHandler (so no offloading to a different thread)
* - a single threaded ClientResponseHandler that offloads the response processing
* a ResponseThread/li>
*
- a multi threaded ClientResponseHandler that offloads the response processing
* to a pool of ResponseThreads.
*
*
* {@see InboundResponseHandlerSupplier}.
*/
public class ClientResponseHandlerSupplier implements Supplier> {
// expert setting; we don't want to expose this to the public
private static final HazelcastProperty IDLE_STRATEGY
= new HazelcastProperty("hazelcast.client.responsequeue.idlestrategy", "block");
// expert setting; we don't want to expose this to the public
// there can be some concurrent request from HZ itself that we need to exclude
// with a single user thread doing e.g. map.get, 4 is the minimum number of concurrent invocations
// before we consider the system to be actually concurrent and to process on response threads.
private static final HazelcastProperty MIN_CONCURRENT_INVOCATIONS
= new HazelcastProperty("hazelcast.client.responsequeue.dynamic.min.concurrent.invocations", "4");
private static final ThreadLocal INT_HOLDER = new ThreadLocal() {
@Override
protected MutableInteger initialValue() {
return new MutableInteger();
}
};
private final AbstractClientInvocationService invocationService;
private final ResponseThread[] responseThreads;
private final HazelcastClientInstanceImpl client;
private final ILogger logger;
private final Consumer responseHandler;
private final boolean responseThreadsDynamic;
private final int minConcurrentInvocations;
public ClientResponseHandlerSupplier(AbstractClientInvocationService invocationService) {
this.invocationService = invocationService;
this.client = invocationService.client;
this.logger = invocationService.invocationLogger;
HazelcastProperties properties = client.getProperties();
this.minConcurrentInvocations = properties.getInteger(MIN_CONCURRENT_INVOCATIONS);
int responseThreadCount = properties.getInteger(RESPONSE_THREAD_COUNT);
if (responseThreadCount < 0) {
throw new IllegalArgumentException(RESPONSE_THREAD_COUNT.getName() + " can't be smaller than 0");
}
this.responseThreadsDynamic = properties.getBoolean(RESPONSE_THREAD_DYNAMIC);
logger.info("Running with " + responseThreadCount + " response threads, dynamic=" + responseThreadsDynamic);
this.responseThreads = new ResponseThread[responseThreadCount];
for (int k = 0; k < responseThreads.length; k++) {
responseThreads[k] = new ResponseThread(invocationService.client.getName() + ".responsethread-" + k + "-");
}
if (responseThreadCount == 0) {
this.responseHandler = new SyncResponseHandler();
} else if (responseThreadsDynamic) {
this.responseHandler = new DynamicResponseHandler();
} else {
this.responseHandler = new AsyncResponseHandler();
}
}
public void start() {
if (responseThreadsDynamic) {
// when dynamic mode is enabled, response threads should not be started.
// this is useful for ephemeral clients where thread creation can be a
// significant part of the total execution time.
return;
}
for (ResponseThread responseThread : responseThreads) {
responseThread.start();
}
}
public void shutdown() {
for (ResponseThread responseThread : responseThreads) {
responseThread.interrupt();
}
}
@Override
public Consumer get() {
return responseHandler;
}
private void process(ClientMessage response) {
try {
handleResponse(response);
} catch (Exception e) {
logger.severe("Failed to process response: " + response
+ " on responseThread: " + Thread.currentThread().getName(), e);
}
}
private void handleResponse(ClientMessage message) {
long correlationId = message.getCorrelationId();
ClientInvocation future = invocationService.deRegisterCallId(correlationId);
if (future == null) {
logger.warning("No call for callId: " + correlationId + ", response: " + message);
return;
}
if (ErrorCodec.TYPE == message.getMessageType()) {
future.notifyException(client.getClientExceptionFactory().createException(message));
} else {
future.notify(message);
}
}
private ResponseThread nextResponseThread() {
if (responseThreads.length == 1) {
return responseThreads[0];
} else {
int index = hashToIndex(INT_HOLDER.get().getAndInc(), responseThreads.length);
return responseThreads[index];
}
}
private class ResponseThread extends Thread {
private final BlockingQueue responseQueue;
private final AtomicBoolean started = new AtomicBoolean();
ResponseThread(String name) {
super(name);
setContextClassLoader(client.getClientConfig().getClassLoader());
this.responseQueue = new MPSCQueue(this,
getIdleStrategy(client.getProperties(), IDLE_STRATEGY));
}
@Override
public void run() {
try {
doRun();
} catch (OutOfMemoryError e) {
onOutOfMemory(e);
} catch (Throwable t) {
invocationService.invocationLogger.severe(t);
}
}
private void doRun() {
while (!invocationService.isShutdown()) {
ClientMessage response;
try {
response = responseQueue.take();
} catch (InterruptedException e) {
continue;
}
process(response);
}
}
private void queue(ClientMessage message) {
responseQueue.add(message);
}
@SuppressFBWarnings(value = "IA_AMBIGUOUS_INVOCATION_OF_INHERITED_OR_OUTER_METHOD",
justification = "The thread.start method is the one we want to call")
private void ensureStarted() {
if (!started.get() && started.compareAndSet(false, true)) {
start();
}
}
}
class SyncResponseHandler implements Consumer {
@Override
public void accept(ClientMessage message) {
process(message);
}
}
class AsyncResponseHandler implements Consumer {
@Override
public void accept(ClientMessage message) {
nextResponseThread().queue(message);
}
}
// dynamically switches between direct processing on io thread and processing on
// response thread based on the number of invocations.
class DynamicResponseHandler implements Consumer {
@Override
public void accept(ClientMessage message) {
if (invocationService.concurrentInvocations() <= minConcurrentInvocations) {
process(message);
} else {
ResponseThread responseThread = nextResponseThread();
responseThread.queue(message);
responseThread.ensureStarted();
}
}
}
}