package org.ovirt.engine.core.vdsbroker.jsonrpc;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import org.ovirt.engine.core.vdsbroker.TransportRunTimeException;
import org.ovirt.vdsm.jsonrpc.client.ClientConnectionException;
import org.ovirt.vdsm.jsonrpc.client.JsonRpcClient;
import org.ovirt.vdsm.jsonrpc.client.JsonRpcRequest;
import org.ovirt.vdsm.jsonrpc.client.JsonRpcResponse;
import org.ovirt.vdsm.jsonrpc.client.ResponseDecomposer;
import org.ovirt.vdsm.jsonrpc.client.utils.LockWrapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Provides asynchronous behavior to synchronous engine code. Request is sent during construction of this map but it
* blocks waiting for response only when it is needed so you can pass around this map and have little or no waiting on
* response.
*
*/
@SuppressWarnings("serial")
public class FutureMap implements Map<String, Object> {
private static final String STATUS = "status";
private static final String DEFAULT_KEY = "info";
private static final long DEFAULT_RESPONSE_WAIT = 1;
private static Logger log = LoggerFactory.getLogger(FutureMap.class);
private final Lock lock = new ReentrantLock();
private Future<JsonRpcResponse> response;
private Map<String, Object> responseMap = new HashMap<>();
private String responseKey;
private String subtypeKey;
private static final Map<String, Object> STATUS_DONE = new HashMap<String, Object>() {
{
super.put("message", "Done");
super.put("code", 0);
}
};
private static final Map<String, Object> TIMEOUT_STATUS = new HashMap<String, Object>() {
{
super.put("message", "Internal timeout occured");
super.put("code", -1);
}
};
private Class<?> clazz = STATUS_DONE.getClass();
private Class<?> subTypeClazz;
private boolean ignoreResponseKey = false;
private long timeout = 0;
private TimeUnit unit = TimeUnit.MILLISECONDS;
private boolean cleanOnTimeout;
private JsonRpcClient client;
/**
* During creation request is sent and <code>Future</code> for a response is held.
*
* @param client - Client object used to send request.
* @param request - Request to be sent.
* @throws TransportRunTimeException when there are connection issues.
*/
public FutureMap(JsonRpcClient client, JsonRpcRequest request) {
try {
this.response = client.call(request);
this.client = client;
} catch (ClientConnectionException e) {
throw new TransportRunTimeException("Connection issues during send request", e);
}
}
/**
* During creation request is sent and <code>Future</code> for a response is held.
*
* @param client - Client object used to send request.
* @param request - Request to be sent.
* @param timeout - Timeout which is used when populating response map.
* @param unit - Time unit for timeout.
* @param cleanOnTimeout - If timeout occur and set to <code>true</code> the request is removed from tracker.
* @throws TransportRunTimeException when there are connection issues.
*/
public FutureMap(JsonRpcClient client, JsonRpcRequest request, long timeout, TimeUnit unit, boolean cleanOnTimeout) {
try {
this.timeout = timeout;
this.unit = unit;
this.response = client.call(request);
this.cleanOnTimeout = cleanOnTimeout;
this.client = client;
} catch (ClientConnectionException e) {
throw new TransportRunTimeException("Connection issues during send request", e);
}
}
/**
* Whenever any method is executed to obtain value of response during the first invocation it gets real response
* from the <code>Future</code> and decompose it to object of provided type and structure.
*
* This method blocks waiting on response or error.
*/
private void lazyEval() {
try (LockWrapper wrapper = new LockWrapper(this.lock)) {
if (this.responseMap.isEmpty()) {
try {
if (timeout != 0) {
populate(this.response.get(timeout, unit));
} else {
populate(this.response.get());
}
} catch (InterruptedException | ExecutionException e) {
log.error("Exception occured during response decomposition", e);
throw new IllegalStateException(e);
} catch (TimeoutException e) {
this.responseMap.put(STATUS, TIMEOUT_STATUS);
if (cleanOnTimeout) {
client.removeCall(this.response);
}
}
}
}
}
private void populate(JsonRpcResponse response) {
ResponseDecomposer decomposer = new ResponseDecomposer(response);
if (decomposer.isError()) {
this.responseMap = decomposer.decomposeError();
} else if (Object[].class.equals(clazz) && this.subtypeKey != null && !this.subtypeKey.trim().isEmpty()
&& this.subTypeClazz != null) {
Object[] array = (Object[]) decomposer.decomposeResponse(this.clazz);
updateResponse(decomposer.decomposeTypedArray(array, this.subTypeClazz, subtypeKey));
} else {
updateResponse(decomposer.decomposeResponse(this.clazz));
}
checkAndUpdateStatus();
}
/**
* Whenever any method is executed to obtain value of response during the first invocation it gets real response
* from the <code>Future</code> and decompose it to object of provided type and structure.
*
* This method waits for a response or error for specified amount of time.
*
* @param wait - time in milliseconds how long we want to wait for response.
*/
private void lazyEval(long wait) {
try (LockWrapper wrapper = new LockWrapper(this.lock)) {
if (this.responseMap.isEmpty()) {
try {
populate(this.response.get(wait, TimeUnit.MILLISECONDS));
} catch (InterruptedException | ExecutionException | TimeoutException e) {
log.debug("Response not arrived yet");
}
}
}
}
@SuppressWarnings("unchecked")
private void updateResponse(Object object) {
if (ignoreResponseKey) {
this.responseMap = (Map<String, Object>) object;
} else {
String key = DEFAULT_KEY;
if (this.responseKey != null && !this.responseKey.trim().isEmpty()) {
key = responseKey;
}
this.responseMap.put(key, object);
}
}
private void checkAndUpdateStatus() {
if (this.responseMap.get(STATUS) == null) {
this.responseMap.put(STATUS, STATUS_DONE);
}
}
@Override
public void clear() {
lazyEval();
this.responseMap.clear();
}
@Override
public Set<Map.Entry<String, Object>> entrySet() {
lazyEval();
return this.responseMap.entrySet();
}
@Override
public boolean isEmpty() {
lazyEval();
return this.responseMap.isEmpty();
}
public boolean isDone() {
lazyEval(DEFAULT_RESPONSE_WAIT);
return !this.responseMap.isEmpty();
}
@Override
public Set<String> keySet() {
lazyEval();
return this.responseMap.keySet();
}
@Override
public Object put(String key, Object value) {
lazyEval();
return this.responseMap.put(key, value);
}
@Override
public void putAll(Map<? extends String, ? extends Object> map) {
lazyEval();
this.responseMap.putAll(map);
}
@Override
public int size() {
lazyEval();
return this.responseMap.size();
}
@Override
public Collection<Object> values() {
lazyEval();
return this.responseMap.values();
}
@Override
public boolean containsKey(Object key) {
lazyEval();
return this.responseMap.containsKey(key);
}
@Override
public boolean containsValue(Object value) {
lazyEval();
return this.responseMap.containsValue(value);
}
@Override
public Object get(Object key) {
lazyEval();
return this.responseMap.get(key);
}
public boolean isRequestCompleted() {
return response.isDone() || response.isCancelled();
}
@Override
public Object remove(Object key) {
lazyEval();
return this.responseMap.remove(key);
}
/**
* @param responseKey
* - Key used to store response value in result <code>Map</code>.
* @return this <code>FutureMap</code>.
*/
public FutureMap withResponseKey(String responseKey) {
this.responseKey = responseKey;
return this;
}
/**
* @param clazz- A type of response which will be use instead of default <code>Map</code>.
* @return this <code>FutureMap</code>.
*/
public FutureMap withResponseType(Class<?> clazz) {
this.clazz = clazz;
return this;
}
/**
* During response decomposition we will ignore default key and use raw response structure as result
* <code>Map</code>.
*
* @return this <code>FutureMap</code>.
*/
public FutureMap withIgnoreResponseKey() {
this.ignoreResponseKey = true;
return this;
}
/**
* @param subTypeKey - Key which is used to put subtype to result map.
* @return this <code>FutureMap</code>.
*/
public FutureMap withSubtypeKey(String subTypeKey) {
this.subtypeKey = subTypeKey;
return this;
}
/**
* @param clazz - type of the subtype.
* @return this <code>FutureMap</code>.
*/
public FutureMap withSubTypeClazz(Class<?> clazz) {
this.subTypeClazz = clazz;
return this;
}
}