All Downloads are FREE. Search and download functionalities are using the official Maven repository.

tech.deplant.java4ever.binding.ffi.EverSdkContext Maven / Gradle / Ivy

package tech.deplant.java4ever.binding.ffi;

import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import tech.deplant.java4ever.binding.*;

import java.lang.foreign.Arena;
import java.lang.foreign.MemorySegment;
import java.util.Map;
import java.util.Objects;
import java.util.Queue;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedDeque;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.ReentrantLock;
import java.util.function.Consumer;

/**
 * The type Ever sdk context.
 */
public class EverSdkContext implements tc_response_handler_t.Function {

	private final static System.Logger logger = System.getLogger(EverSdkContext.class.getName());

	private final int id;
	private final AtomicInteger requestCount = new AtomicInteger();
	private final Client.ClientConfig clientConfig;
	private final long timeout;
	@JsonIgnore private final Map requests = new ConcurrentHashMap<>();
	@JsonIgnore private final Queue cleanupQueue = new ConcurrentLinkedDeque<>();

	/**
	 * Instantiates a new Ever sdk context.
	 *
	 * @param id           the id
	 * @param clientConfig the client config
	 */
	public EverSdkContext(int id, Client.ClientConfig clientConfig) {
		this.id = id;
		this.clientConfig = clientConfig;
		this.timeout = extractTimeout(clientConfig);
	}

	/**
	 * Most used call to EVER-SDK with some output object
	 *
	 * @param             Class of the result object
	 * @param 

Class of the function params object * @param Class of the AppObject param calls * @param Class of the AppObject result callbacks * @param functionName the function name * @param functionInputs record of input type, usually ParamsOf... * @param resultClass class of output type record, usually ResultOf...class * @param eventConsumer the event consumer * @param appObject the app object * @return output type record, usually ResultOf... * @throws EverSdkException the ever sdk exception */ public CompletableFuture callAsync(final String functionName, final P functionInputs, final Class resultClass, final Consumer eventConsumer, final AppObject appObject) { final int requestId = requestCountNextVal(); // let's clean previous requests and their native memory sessions cleanup(); // it's better to explicitly mark requests as void to not recheck this every time in response handler boolean hasResponse = !resultClass.equals(Void.class); var request = new RequestData(hasResponse, Arena.ofAuto(), new ReentrantLock(), resultClass, new CompletableFuture<>(), eventConsumer, appObject); this.requests.put(requestId, request); // with reentrant lock, multiple results on any given single request will be processed one by one var paramsJson = processParams(functionInputs); request.queueLock().lock(); try { NativeMethods.tcRequest(this.id, functionName, paramsJson, request.nativeArena(), requestId, this); } finally { request.queueLock().unlock(); logger.log(System.Logger.Level.TRACE, () -> EverSdk.LOG_FORMAT.formatted(this.id, requestId, functionName, "SEND", paramsJson)); } if (!hasResponse) { request.responseFuture().complete(null); } return request.responseFuture(); } /** * Request count next val int. * * @return the int */ public int requestCountNextVal() { return this.requestCount.incrementAndGet(); } /** * Add response. * * @param requestId the request id * @param responseString the response string */ public void addResponse(int requestId, final RequestData request, final String responseString) { if (request.hasResponse()) { try { if (!request.responseFuture().isDone()) { logger.log(System.Logger.Level.TRACE, () -> "CTX:%d REQ:%d RESP:%s".formatted(this.id, requestId, responseString)); request.responseFuture() .complete(JsonContext.SDK_JSON_MAPPER().readValue(responseString, request.responseClass())); } else { logger.log(System.Logger.Level.ERROR, "Slot for this request not found on processing response! CTX:%d REQ:%d RESP:%s".formatted( this.id, requestId, responseString)); } } catch (JsonProcessingException ex2) { // successful response but parsing failed logger.log(System.Logger.Level.ERROR, () -> "CTX:%d REQ:%d EVER-SDK Response deserialization failed! %s".formatted(this.id, requestId, ex2.toString())); request.responseFuture() .completeExceptionally(new EverSdkException(new EverSdkException.ErrorResult(-500, "EVER-SDK response deserialization failed!"), ex2.getCause())); } } } private void addError(int requestId, final RequestData request, final String responseString) { if (request.responseFuture() instanceof CompletableFuture future) { try { // These errors are sent by SDK, response_type=1 //String everSdkError = ex.getCause().getMessage(); logger.log(System.Logger.Level.WARNING, () -> "CTX:%d REQ:%d ERR:%s".formatted(this.id, requestId, responseString)); // let's try to parse error response EverSdkException.ErrorResult sdkResponse = JsonContext.SDK_JSON_MAPPER() .readValue(responseString, EverSdkException.ErrorResult.class); future.completeExceptionally(new EverSdkException(sdkResponse, "Error response from SDK")); } catch (JsonProcessingException ex1) { // if error response parsing failed logger.log(System.Logger.Level.ERROR, () -> "CTX:%d REQ:%d EVER-SDK Error deserialization failed! %s".formatted(this.id, requestId, ex1.toString())); future.completeExceptionally(new EverSdkException(new EverSdkException.ErrorResult(-500, "EVER-SDK Error deserialization failed!"), "EVER-SDK Error deserialization failed!", ex1)); } } else { logger.log(System.Logger.Level.ERROR, "Slot for this request not found on processing error response! CTX:%d REQ:%d ERR:%s".formatted( this.id, requestId, responseString)); } } // responseType = 100 means good answer, 101 means error or reconnection private void addAppObjectRequest(int requestId, final RequestData request, final String appRequestString) { if (request.appObject() != null) { try { var appRequest = JsonContext.SDK_JSON_MAPPER() .readValue(appRequestString, Client.ParamsOfAppRequest.class); try { request.appObject().consumeParams(this.id, appRequest.appRequestId(), appRequest.requestData()); } catch (Exception ex1) { logger.log(System.Logger.Level.ERROR, () -> "REQ:%d EVENT:%s AppRequest processing failed! %s".formatted(requestId, appRequestString, ex1.toString())); } } catch (JsonProcessingException ex2) { logger.log(System.Logger.Level.ERROR, () -> "REQ:%d EVENT:%s AppRequest JSON deserialization failed! %s".formatted(requestId, appRequestString, ex2.toString())); } } else { logger.log(System.Logger.Level.ERROR, "No app request consumer for this request_id! CTX:%d REQ:%d EVENT:%s".formatted(this.id, requestId, appRequestString)); } } // responseType = 100 means good answer, 101 means error or reconnection private void addEvent(int requestId, final RequestData request, final String responseString, int responseType) { if (request.subscriptionHandler() != null) { try { JsonNode node = JsonContext.ABI_JSON_MAPPER().readTree(responseString); try { request.subscriptionHandler().accept(node); } catch (Exception ex1) { logger.log(System.Logger.Level.ERROR, () -> "REQ:%d EVENT:%s Subscribe Event Action processing failed! %s".formatted(requestId, responseString, ex1.toString())); } } catch (JsonProcessingException ex2) { logger.log(System.Logger.Level.ERROR, () -> "REQ:%d EVENT:%s Subscribe Event JSON deserialization failed! %s".formatted(requestId, responseString, ex2.toString())); } } else { logger.log(System.Logger.Level.ERROR, "No event consumer for this request_id! CTX:%d REQ:%d EVENT:%s".formatted(this.id, requestId, responseString)); } } private void finishRequest(int requestId, final RequestData request) { this.cleanupQueue.add(request); this.requests.remove(requestId); } private void cleanup() { while (this.cleanupQueue.poll() instanceof RequestData request) { if (request.queueLock().isLocked()) { request.queueLock().unlock(); } } } private

String processParams(final P params) { try { return (null == params) ? "" : JsonContext.SDK_JSON_MAPPER().writeValueAsString(params); } catch (JsonProcessingException e) { logger.log(System.Logger.Level.ERROR, () -> "Parameters serialization failed!" + e.getMessage() + e.getCause()); throw new IllegalArgumentException("Parameters serialization failed!", e); } } private long extractTimeout(Client.ClientConfig cfg) { return switch (cfg) { case null -> 60000L; case Client.ClientConfig cl -> switch (cl.network()) { case null -> 60000L; case Client.NetworkConfig ntwrk -> Objects.requireNonNullElse(ntwrk.queryTimeout(), 60000L); }; }; } /** * Id int. * * @return the int */ public int id() { return this.id; } /** * Request count int. * * @return the int */ public int requestCount() { return this.requestCount.get(); } /** * Config client . client config. * * @return the client . client config */ public Client.ClientConfig config() { return this.clientConfig; } /** * @param request_id id of the request in this context that this answer applies to * @param params_json memory segment with native response string * @param response_type Type of response with following possible values: * RESULT = 1, real response. * NOP = 2, no operation. In combination with finished = true signals that the request handling was finished. * APP_REQUEST = 3, request some data from application. See Application objects * APP_NOTIFY = 4, notify application with some data. See Application objects * RESERVED = 5..99 – reserved for protocol internal purposes. Application (or binding) must ignore this response. * CUSTOM >= 100 - additional function data related to request handling. Depends on the function. * @param finished is this a final answer for this request_id */ @Override public void apply(int request_id, final MemorySegment params_json, int response_type, boolean finished) { final String responseString = NativeStrings.toJava(params_json); if (logger.isLoggable(System.Logger.Level.TRACE)) { logger.log(System.Logger.Level.TRACE, "CTX:%d, REQ:%d TYPE:%d FINISHED:%s JSON:%s".formatted(this.id, request_id, response_type, String.valueOf(finished), responseString)); } if (this.requests.get(request_id) instanceof RequestData request) { // Request is present, let's lock it request.queueLock().lock(); try { switch (tc_response_types.of(response_type)) { case tc_response_types.TC_RESPONSE_SUCCESS -> addResponse(request_id, request, responseString); case tc_response_types.TC_RESPONSE_ERROR -> addError(request_id, request, responseString); case tc_response_types.TC_RESPONSE_CUSTOM -> addEvent(request_id, request, responseString, response_type); case tc_response_types.TC_RESPONSE_APP_REQUEST -> addAppObjectRequest(request_id, request, responseString); } // if "finished" boolean flag received, let's cleanup request if (finished) { finishRequest(request_id, request); } } catch (Exception e) { logger.log(System.Logger.Level.ERROR, "REQ:%d TYPE:%d EVER-SDK Unexpected upcall error! %s".formatted(request_id, response_type, e.toString())); } finally { request.queueLock().unlock(); } } else { // Let's process the situation when request already cleaned up logger.log(System.Logger.Level.ERROR, "REQ:%d TYPE:%d EVER-SDK Response on cleaned request!".formatted(request_id, response_type)); } } /** * The type Request data. * It's VERY IMPORTANT to hold the pointer to RequestData for all interconnection of EVER-SDK request. * That's because nativeArena field is managed by GC. * If the RequestData will be cleaned up by GC, all subsequent answer will fail the JVM. * * @param the type parameter */ private record RequestData(boolean hasResponse, Arena nativeArena, ReentrantLock queueLock, Class responseClass, CompletableFuture responseFuture, Consumer subscriptionHandler, AppObject appObject) { } /** * The type Subscription handler. */ record SubscriptionHandler(Consumer eventAction) { } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy