I ping services nodes and check if it is alive or not. My class, which ping all services is below:
public abstract class PingCallable implements Callable<ResponseEntity<String>> {
#Override
public ResponseEntity call() throws Exception {
try {
RestTemplate restTemplate = new RestTemplate();
HttpHeaders headers = new HttpHeaders();
HttpEntity<String> entity = new HttpEntity<>("", headers);
return restTemplate.exchange(new URI(getPingUrl()), HttpMethod.GET, entity, String.class);
} catch (Exception ex) {
ex.printStackTrace();
return new ResponseEntity("Exception occured while ping. Exception message = " + ExceptionUtil.getFullCauseMessage(ex), HttpStatus.INTERNAL_SERVER_ERROR);
}
}
protected abstract String getPingUrl();
}
public class ServicePingCallable extends PingCallable {
private Node node;
public ServicePingCallable(Node node) {
this.node = node;
}
#Override
protected String getPingUrl() {
return "//" + node.getIp() + "/api/" + node.getService().getServicePath() + "/pingService";
}
public Node getNode(){
return this.node;
}
}
Method which pings service nodes is here:
#Scheduled(fixedDelayString = "${config.ping.fixedDelay}")
public void pingServices() throws InterruptedException {
List<Node> nodeList = serviceRepository.findAll();
List<ServicePingCallable> callableList = new ArrayList<>(nodeList.size());
ExecutorService executor = Executors.newFixedThreadPool(10);
try {
for (Node node : nodeList) {
ServicePingCallable pingCallable = new ServicePingCallable(node);
callableList.add(pingCallable);
}
List<Future<ResponseEntity<String>>> responses = executor.invokeAll(callableList, 10, TimeUnit.SECONDS);
for(Future<ResponseEntity<String>> response: responses){
// here I should check response HttpStatus and response text and save node ping state.
// But how I can know here for which Node is response?
//It whould be great to have such method:
//ServicePingCallable callable = (ServicePingCallable)response.getCallable();
//Node = callable.getNode();
}
} catch (Exception ex) {
//catch and log here
} finally {
if (!executor.isShutdown()) {
try {
executor.shutdown();
} catch (Exception ex1) {
}
}
}
I can add to PingCallable private filed ResponseEntity and return from call() the PingCallable but this approach is like workaround and not clear.
I think this is common task and should be some pattern how to get the callable from the future result. If you know it please share.
Related
I'm trying to use futures to make concurrent api calls. Code:
private void init() throws ExecutionException, InterruptedException {
Long start = System.currentTimeMillis();
List<ApiResponse> responses = fetchAllUsingFuture(ids, 3);
log.info(responses.toString());
Long finish = System.currentTimeMillis();
log.info(MessageFormat.format("Process duration: {0} in ms", finish-start));
}
private List<ApiResponse> fetchAllUsingFuture(List<String> ids, int threadCount) throws ExecutionException, InterruptedException {
ExecutorService executorService = Executors.newFixedThreadPool(threadCount);
List<List<String>> chunks = Utils.splitToChunks(ids, threadCount);
List<Future<List<ApiResponse>>> futures = new ArrayList<>();
chunks.forEach(chunk -> {
futures.add(wrapFetchInFuture(chunk));
});
Future<List<ApiResponse>> resultFuture = executorService.submit(() -> {
List<ApiResponse> responses = new ArrayList<>();
futures.forEach(future -> {
try {
responses.addAll(future.get());
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
});
return responses;
});
executorService.shutdown();
return resultFuture.get();
}
private Future<List<ApiResponse>> wrapFetchInFuture(List<String> ids) {
return new FutureTask<>(() -> {
List<ApiResponse> responses = new ArrayList<>();
ids.forEach(id -> {
responses.add(fetchData(id));
});
return responses;
});
}
private ApiResponse fetchData(String id) {
ResponseEntity<ApiResponse> response = restTemplate.getForEntity(id, ApiResponse.class);
log.info(MessageFormat.format("Fetching from {0}", id));
ApiResponse body = response.getBody();
log.info(MessageFormat.format("Retrieved {0}", body));
return body;
}
It doesn't execute, the app starts and then just pends. Futures don't get fulfilled. All advices are appreciated.
P.S. I'm aware this is much more easily done using CompletableFuture, I was just wondering how to do this with Futures
In the original version of the question, you are creating a list of FutureTasks but never send them to the ExecutorService to run them. The tasks never complete, so Future.get blocks forever.
In the updated version of the question, you have put the code that does the waiting into the executor service as a task. The FutureTasks never run, so FutureTask.get will still block forever.
I would suggest you change the code in fetchAllUsingFuture to:
List<Callable<List<ApiResponse>>> tasks = new ArrayList<>();
chunks.forEach(chunk -> {
tasks.add(wrapFetchInCallable(chunk));
});
List<Future<List<ApiResponse>>> futures = executorService.invokeAll(tasks);
where wrapFetchInCallable creates a Callable instead of FutureTask:
private static Callable<List<ApiResponse>> wrapFetchInCallable(List<String> ids) {
return () -> {
List<ApiResponse> responses = new ArrayList<>();
ids.forEach(id -> {
responses.add(fetchData(id));
});
return responses;
};
}
It looks like you are creating a list of FutureTasks but never send them to the ExecutorService to run them.
I have implemented ExecutorService with Future Object as below, i hope it helps you:
Service layer:
public List<MovieDTO> searchMoviesParallel(String limit, String offset, String searchPhrase) throws Exception {
ExecutorService executor = Executors.newFixedThreadPool(1);
Future<List<MovieDTO>> digitoonResult = executor.submit(new DigitoonSearchTask(limit, offset, searchPhrase));
List<MovieDTO> movieDTOList = digitoonResult.get();
executor.shutdown();
return movieDTOList;
}
And my Search task(DigitoonSearchTask class) is as below:
public class DigitoonSearchTask implements Callable<List<MovieDTO>> {
private String limit;
private String offset;
private String searchPhrase;
private final static String digitoonSearchBaseUrl = "http://apitwo.xxx.com/partner/search/?q=";
public DigitoonSearchTask(String limit, String offset, String searchPhrase) {
this.limit = limit;
this.offset = offset;
this.searchPhrase = searchPhrase;
}
#Override
public List<MovieDTO> call() throws Exception {
List<MovieDTO> movieDTOList = new ArrayList<>();
ObjectMapper mapper = new ObjectMapper();
try {
String uri = digitoonSearchBaseUrl + URLEncoder.encode(searchPhrase, "utf-8") + "&limit=" + limit + "&offset=" + offset;
URL url = new URL(uri);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("GET");
conn.setRequestProperty("Accept", "application/json");
conn.setRequestProperty("authorization", "xxxxxxxxxx");
if (conn.getResponseCode() != 200) {
throw new RuntimeException("Failed : HTTP error code : "
+ conn.getResponseCode());
}
BufferedReader br = new BufferedReader(new InputStreamReader(
(conn.getInputStream())));
String output;
while ((output = br.readLine()) != null) {
movieDTOList = Arrays.asList(mapper.readValue(output, MovieDTO[].class));
}
br.close();
conn.disconnect();
} catch (UnknownHostException e) {
call();
} catch (MalformedURLException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
return movieDTOList;
}}
consider that now I have just one API and after getting others they can be added as another Search task in service layer by increasing the thread number.
I am unable to catch thrown exceptions from an async method in Spring. I have written an uncaught exception handler to catch but was unsuccessful.
The application will enable to start any number of forever running asynchronous jobs.
I think my async method needs to return Future so that I can store it in hashmap and check its status or stop the job. I also can get all running jobs by storing it.
I think I can't use get method of future because if the input is correct it blocks and my job will be forever running. I need to send status as started if the input is fine. Whenever an exception occurs in the Async method it is thrown but I am unable to catch it. How can I do that?
Here is my complete code.
Application.java
#SpringBootApplication
#EnableAsync
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
AsyncConfig.java
#EnableAsync
#Configuration
public class AsyncConfig implements AsyncConfigurer {
#Override
public Executor getAsyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(5);
executor.setMaxPoolSize(5);
executor.setQueueCapacity(100);
executor.setThreadNamePrefix("MyExecutor-");
executor.initialize();
return executor;
}
#Override
public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
return new AsyncExceptionHandler();
}
}
AsyncExceptionHandler.java
public class AsyncExceptionHandler implements AsyncUncaughtExceptionHandler {
#Override
public void handleUncaughtException(Throwable throwable, Method method, Object... obj) {
System.out.println("Exception Cause - " + throwable.getMessage());
System.out.println("Method name - " + method.getName());
for (Object param : obj) {
System.out.println("Parameter value - " + param);
}
}
}
createBucket.java
#Service
public class createBucket {
#Async
public Future<String> start(String config){
try {
JSONObject map = new JSONObject(config);
Jedis jedis = new Jedis(map.getString("jedisip"));
jedis.auth(map.getString("password"));
// code to make a kafka consumer subscribe to a topic given in config input
while(true) {
//forever running code which polls using a kafka consumer
}
}
catch(JedisException j) {
throw new JedisException("Some msg");
}
}
}
Endpoint.java
#Controller
public class Endpoint {
#Autowired
private createBucket service;
private Future<String> out;
private HashMap<String, Future<String>> maps = new HashMap<>();
#PostMapping(value = "/start", consumes = "application/json", produces = "application/json")
public ResponseEntity<String> starttask(#RequestBody String conf) {
try {
out = service.start(conf);
maps.put(conf, out);
}
catch (Exception e) {
return new ResponseEntity<>("exception", HttpStatus.BAD_REQUEST);
}
return new ResponseEntity<>("{\"started\":\"true\"}", HttpStatus.CREATED);
}
}
As stated in official doc, AsyncUncaughtExceptionHandler is used for void return value.
https://docs.spring.io/spring/docs/5.1.10.RELEASE/spring-framework-reference/integration.html#spring-integration
In your scenario, I recommend using CompletableFuture and DeferredResult:
#Async
public CompletableFuture<String> start(String config) {
CompletableFuture completableFuture = new CompletableFuture();
try {
JSONObject map = new JSONObject(config);
Jedis jedis = new Jedis(map.getString("jedisip"));
jedis.auth(map.getString("password"));
completableFuture.complete("started!");
}
catch(JedisException j) {
completableFuture.completeExceptionally(j);
}
return completableFuture;
}
#PostMapping(value = "/start", consumes = "application/json", produces = "application/json")
public DeferredResult<ResponseEntity> starttask(#RequestBody String conf) {
CompletableFuture<String> start = service.start(conf);
DeferredResult<ResponseEntity> deferredResult = new DeferredResult<>();
start.whenComplete((res, ex) -> {
if (ex == null) {
ResponseEntity<String> successEntity = new ResponseEntity<>("{\"started\":\"true\"}", HttpStatus.CREATED);\
deferredResult.setResult(successEntity);
} else {
// handle ex here!
ResponseEntity<String> exEntity = new ResponseEntity<>("exception", HttpStatus.BAD_REQUEST);
deferredResult.setResult(exEntity);
}
});
return deferredResult;
}
There is another serious problem. The following code is not thread safe.
private Future<String> out;
private HashMap<String, Future<String>> maps = new HashMap<>();
I have a java SDK,which use OkHttp client(4.0.0) to get token from IAM server and return token to application.The relation may like this:Applicaiton Sync call SDK,SDK Async call IAM.Refer to this answerJava - Retrieving Result from OkHttp Asynchronous GET,the code like:
The Async Class:
class BaseAsyncResult<T> {
private final CompletableFuture<T> future = new CompletableFuture<>();
T getResult() {
try {
return future.get();
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
return null;
}
void onFailure(IOException e) {
future.completeExceptionally(e);
}
void onResponse(Response response) throws IOException {
String bodyString = Objects.requireNonNull(response.body()).string();
future.complete(IasClientJsonUtil.json2Pojo(bodyString, new TypeReference<T>() {}));
}
}
Okhttp call like this:
public void invoke(Request request, BaseAsyncResult result) {
okHttpClient.newCall(request).enqueue(new Callback() {
#Override
public void onFailure(#NotNull Call call, #NotNull IOException e) {
result.onFailure(e);
}
#Override
public void onResponse(#NotNull Call call, #NotNull Response response) throws IOException {
result.onResponse(response);
}
});
}
The application use sdk code like,iasClient is a wrapper of okhttp client :
BaseAsyncResult<AuthenticationResponse> iasAsyncResult = new BaseAsyncResult();
iasClient.invoke(request, iasAsyncResult);
AuthenticationResponse result = iasAsyncResult.getResult();
The erroe message:
java.lang.ClassCastException: java.util.LinkedHashMap cannot be cast to x.x.x.AuthenticationResponse
What have I missed?
You need to make sure jackson knows which class to deserialize the value to . In this case, you are asking Jackson to deserialize the response to a TypeReference , which will resolve to a Map by default unless you specify the class (in this case, AuthenticationResponse ) . The Future resolves to a linkedHashMap due to this and causes the class cast.
try replacing the below line .
future.complete(IasClientJsonUtil.json2Pojo(bodyString, new TypeReference<T>() {}));
with
future.complete(IasClientJsonUtil.json2Pojo(bodyString, new TypeReference<AuthenticationResponse>() {}));
One method from #Arpan Kanthal is add a private Class type variable to BaseAsyncResult and then use that class in your json2Pojo function,then the BaseAsyncResult may like this:
public class BaseAsyncResult<T> {
private final CompletableFuture<T> future = new CompletableFuture<>();
private Class<T> classType;
public BaseAsyncResult(Class<T> classType) {
this.classType = classType;
}
public T getResult() {
try {
return future.get();
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
return null;
}
void onFailure(IOException e) {
future.completeExceptionally(e);
}
void onResponse(Response response) throws IOException {
future.complete(JacksonUtil.json2Pojo(response.body().string(), classType));
}
}
Bottom line:
Some server generated events do not get delivered to the client in production over websockets. However, the websocket connections get established just fine.
Case study:
I open Google Chrome and connect to our server. Open the devtools. Under WS tab, I see that connection got established fine, but I receive no frames when, let’s say, server needs to update something on the page. I wait for a while and sometimes (only sometimes) I get some events with a huge amount of delay. This works as expected locally however.
Question:
Has anyone seen similar websocket behavior and has any suggestions on how to eliminate variables for this investigation.
Infrastructure:
Server: Linux Tomcat
Two servers that handle:
1. Traffic from Devices (Communicating over TCP/IP with the Server)
2. Traffic from Users
Users and Devices are many to many relationship. If a user gets connected to a server that doesn’t have a device connected. This server looks on the other server and handles the info exchange.
There is a firewall in front of the servers.
Code:
https://github.com/kino6052/websockets-issue
WebSocketServerEndpoint.java
#ServerEndpoint("/actions")
public class WebSocketServerEndpoint {
static private final org.slf4j.Logger logger = LoggerFactory.getLogger(WebSocketServerEndpoint.class);
#OnOpen
public void open(Session session) {
WebSocketSessionHandler.addSession(session);
}
#OnClose
public void close(Session session) {
WebSocketSessionHandler.removeSession(session);
}
#OnError
public void onError(Throwable error) {
//Logger.getLogger(WebSocketServerEndpoint.class.getName()).log(Level.SEVERE, null, error);
}
#OnMessage
public void handleMessage(String message, Session session) {
try (JsonReader reader = Json.createReader(new StringReader(message))) {
JsonObject jsonMessage = reader.readObject();
Long userId = null;
Long tenantId = null;
switch (WebSocketActions.valueOf(jsonMessage.getString("action"))){
case SaveUserId:
userId = getUserId(jsonMessage);
tenantId = getTenantId(jsonMessage);
Long userIdKey = WebSocketSessionHandler.saveUserId(userId, session);
Long tenantUserKey = WebSocketSessionHandler.saveTenantUser(tenantId, userId);
WebSocketSessionHandler.updateUserSessionKeys(session, tenantUserKey, userIdKey); // Needed for Making Weak Maps Keep Their Keys if Session is Currently Active
}
} catch (Exception e) {
logger.error(e.toString());
}
}
private Long getUserId(JsonObject jsonMessage) {
Long userId = null;
try {
userId = Long.parseLong(((Integer) jsonMessage.getInt("userId")).toString());
return userId;
} catch (Exception e) {
logger.error(e.getMessage());
return userId;
}
}
private Long getTenantId(JsonObject jsonMessage) {
Long tenantId = null;
try {
tenantId = Long.parseLong(((Integer) jsonMessage.getInt("tenantId")).toString());
return tenantId;
} catch (Exception e) {
logger.error(e.getMessage());
return tenantId;
}
}
}
WebSocketService.java
#Singleton
public class WebSocketService {
private static final Logger logger = LoggerFactory.getLogger(WebSocketService.class);
public enum WebSocketEvents{
OnConnection,
OnActivity,
OnAccesspointStatus,
OnClosedStatus,
OnConnectedStatus,
OnAlert,
OnSessionExpired
}
public enum WebSocketActions{
SaveUserId
}
#WebPost("/lookupWebSocketSessions")
public WebResponse lookupWebSocketSessions(#JsonArrayParam("userIds") List<Integer> userIds, #WebParam("message") String message){
try {
for (Integer userIdInt : userIds) {
Long userId = Long.parseLong(userIdInt.toString());
if (WebSocketSessionHandler.sendToUser(userId, message) == 0) {
} else {
//logger.debug("Couldn't Send to User");
}
}
} catch (ClassCastException e) {
//logger.error(e.getMessage());
return webResponseBuilder.fail(e);
} catch (Exception e) {
//logger.error(e.getMessage());
return webResponseBuilder.fail(e);
}
return webResponseBuilder.success(message);
}
#WebPost("/lookupWebSocketHistorySessions")
public WebResponse lookupWebSocketHistorySessions(#JsonArrayParam("userIds") List<Integer> userIds, #WebParam("message") String message){
try {
for (Integer userIdInt : userIds) {
Long userId = Long.parseLong(userIdInt.toString());
if (WebSocketHistorySessionHandler.sendToUser(userId, message) == 0) {
} else {
//logger.debug("Couldn't Send to User");
}
}
} catch (ClassCastException e) {
//logger.error(e.getMessage());
return webResponseBuilder.fail(e);
} catch (Exception e) {
//logger.error(e.getMessage());
return webResponseBuilder.fail(e);
}
return webResponseBuilder.success(message);
}
// Kick Out a User if Their Session is no Longer Valid
public void sendLogout(User user) {
try {
Long userId = user.getId();
List<Long> userIds = new ArrayList<>();
userIds.add(userId);
JSONObject result = new JSONObject();
result.put("userId", userId);
JSON message = WebSocketSessionHandler.createMessage(WebSocketEvents.OnSessionExpired, result);
lookOnOtherServers(userIds, message);
} catch (Exception e) {
logger.error("Couldn't Logout User");
}
}
// Send History after Processing Data
// Returns "0" if success, "-1" otherwise
public int sendHistory(Activity activity) {
try {
TimezoneService.TimeZoneConfig timeZoneConfig = timezoneService.getTimezoneConfigsByAp(null, activity.getAccesspointId());
JSONObject result = (JSONObject) JSONSerializer.toJSON(activity);
String timezoneId = timezoneService.convertTimezoneConfigToTimezoneId(timeZoneConfig);
result.put("timezoneString", timezoneId);
result.put(
"profileId",
userDao.getUserProfileId(activity.getUserId())
);
JSON message = WebSocketHistorySessionHandler.createMessage(WebSocketEvents.OnActivity, result);
List<Long> userIds = getUsersSubscribedToActivity(activity.getTenantId());
lookOnOtherServersHistory(userIds, message);
return 0;
} catch (Exception e) {
//logger.error("Couldn't Send History");
return -1;
}
}
// SendAlertUpdate after Processing Data
public void sendAlertUpdate(Alert alert) {
try {
List<Long> userIds = getUsersUnderTenantByAccesspointId(alert.getAccesspointId());
JSONObject result = JSONObject.fromObject(alert);
JSON message = WebSocketSessionHandler.createMessage(WebSocketEvents.OnAlert, result);
lookOnOtherServers(userIds, message);
} catch (Exception e) {
//logger.error("Couldn't Send Aleart");
}
}
// Send Connected Status after Processing Data
public void sendConnectedStatus(Long accesspointId, Boolean isConnected) {
try {
List<Long> userIds = getUsersUnderTenantByAccesspointId(accesspointId);
JSONObject result = new JSONObject();
result.put("accesspointId", accesspointId);
result.put("isConnected", isConnected);
JSON message = WebSocketSessionHandler.createMessage(WebSocketEvents.OnConnectedStatus, result);
lookOnOtherServers(userIds, message);
} catch (Exception e) {
//logger.error("Couldn't Send Connected Status");
}
}
public int sendHistory(CredentialActivity activity) {
try {
TimezoneService.TimeZoneConfig timeZoneConfig = timezoneService.getTimezoneConfigsByAp(null, activity.getAccesspointId());
JSONObject result = (JSONObject) JSONSerializer.toJSON(activity);
String timezoneId = timezoneService.convertTimezoneConfigToTimezoneId(timeZoneConfig);
result.put("timezoneString", timezoneId);
result.put(
"profileId",
userDao.getUserProfileId(activity.getUserId())
);
JSON message = WebSocketHistorySessionHandler.createMessage(WebSocketEvents.OnActivity, result);
List<Long> userIds = getUsersUnderTenantByAccesspointId(activity.getAccesspointId());
lookOnOtherServersHistory(userIds, message);
return 0;
} catch (Exception e) {
return -1;
}
}
public Boolean isUserSessionAvailable(Long id) {
return WebSocketSessionHandler.isUserSessionAvailable(id);
}
public void lookOnOtherServers(List<Long> userId, JSON data){
List<String> urls = awsService.getServerURLs();
for (String url : urls) {
postJSONDataToUrl(url, userId, data);
}
}
public void lookOnOtherServersHistory(List<Long> userId, JSON data){
List<String> urls = awsService.getServerURLsHistory();
for (String url : urls) {
postJSONDataToUrl(url, userId, data);
}
}
public int sendClosedStatus(AccesspointStatus accesspointStatus){
try {
JSONObject accesspointStatusJSON = new JSONObject();
accesspointStatusJSON.put("accesspointId", accesspointStatus.getAccesspointId());
accesspointStatusJSON.put("openStatus", accesspointStatus.getOpenStatus());
List<Long> userIds = getUsersUnderTenantByAccesspointId(accesspointStatus.getAccesspointId());
lookOnOtherServers(userIds, accesspointStatusJSON);
return 0;
} catch (Exception e) {
return -1;
}
}
public List<Long> getUsersSubscribedToActivity(Long tenantId) {
List<Long> userList = WebSocketSessionHandler.getUsersForTenant(tenantId);
return userList;
}
private List<Long> getUsersUnderTenantByAccesspointId(Long accesspointId) {
List<Long> userList = new ArrayList<>();
User user = userDao.getBackgroundUserByAccesspoint(accesspointId);
List<Record> recordList = tenantDao.getTenantsByUser(user, user.getId());
for (Record record : recordList) {
Long tenantId = (Long) record.get("id");
userList.addAll(getUsersSubscribedToActivity(tenantId));
}
return userList;
}
public void postJSONDataToUrl(String url, List<Long> userId, JSON data) throws AppException {
List<NameValuePair> parameters;
HttpResponse httpResponse;
HttpClientService.SimpleHttpClient simpleHttpClient = httpClientService.createHttpClient(url);
try {
parameters = httpClientService.convertJSONObjectToNameValuePair(userId, data);
} catch (Exception e) {
throw new AppException("Couldn't Convert Input Parameters");
}
try {
httpResponse = simpleHttpClient.sendHTTPPost(parameters);
} catch (Exception e) {
throw new AppException("Couldn't Get Data from the Server");
}
if (httpResponse == null) {
throw new AppException("Couldn't Send to Another Server");
} else {
//logger.error(httpResponse.getStatusLine().toString());
}
}
}
WebSocketSessionHandler.java
public class WebSocketSessionHandler {
// Apparently required to instantiate the dialogue,
// ideally it would be better to just create session map where sessions are mapped to userId,
// however, userId will be send only after the session is created.
// TODO: Investigate Instantiation of WebSocket Session Further
// WeakHashMap is Used for Automatic Memory Management (So That Removal of Keys That are no Longer Used Can be Automatically Performed)
// NOTE: However, it Requires Certain Precautions to Make Sure Their Keys Don't Expire Unexpectedly, Look for the Commented Code Below
private static final Map<Long, Set<Session>> sessionMap = new WeakHashMap<>();
private static final Map<Long, Set<Long>> tenantUserMap = new WeakHashMap<>();
public WebSocketSessionHandler() {}
public static List<Long> getUsersForTenant(Long tenantId) {
List<Long> userIds = new ArrayList<>();
Set<Long> userIdsSet = tenantUserMap.get(tenantId);
if (userIdsSet != null) {
for (Long userId : userIdsSet){
userIds.add(userId);
}
}
return userIds;
}
public static Boolean isUserSessionAvailable(Long id){
Set<Session> userSessions = sessionMap.get(id);
if (userSessions == null || userSessions.size() == 0) {
return false;
} else {
return true;
}
}
// addSession() should add "session" to "sessions" set
// returns: "0" if success and "-1" otherwise
public static int addSession(Session session) {
int output;
try {
final long ONE_DAY = 86400000;
session.setMaxIdleTimeout(ONE_DAY);
sessions.put(session, new ArrayList<>());
return sendToSession(session, createMessage(WebSocketEvents.OnConnection, "Successfully Connected"));
} catch (Exception e) {
logger.error("Couldn't Add Session");
return -1;
}
}
// removeSession() should remove "session" from "sessions" set
// Scenarios:
// sessions is null?
// returns: "0" if success and "-1" otherwise
public static int removeSession( Session session) {
try {
closeSessionProperly(session);
if (sessions.remove(session) != null) {
return 0;
} else {
return -1;
}
} catch (Exception e) {
logger.error("Couldn't Remove Session");
return -1;
}
}
private static void closeSessionProperly(Session session) {
try {
session.close();
} catch (IOException ex) {
}
}
public static Long getKeyFromMap(Map map, Long key){ // Needed for Weak Maps
Set<Long> keySet = map.keySet();
for (Long keyReference : keySet) {
if (keyReference == key) {
return keyReference;
}
}
return key; // If Not Found Return the Value Passed in
}
// saveUserId() should create an { userId -> session } entry in sessionMap
public static Long saveUserId(Long userId, Session session){
// Test Scenarios:
// Can userId be null or wrong?
// Can session be null or wrong?
try {
userId = getKeyFromMap(sessionMap, userId); // Required for Weak Maps to Work Correctly
Set<Session> sessionsForUser = sessionMap.get(userId);
if (sessionsForUser == null) {
sessionsForUser = new HashSet<>();
}
sessionsForUser.add(session);
sessionMap.put(userId, sessionsForUser);
return userId;
} catch (Exception e) {
logger.error("Couldn't Save User Id");
return null;
}
}
// saveUserId() should create an { userId -> session } entry in sessionMap
public static Long saveTenantUser(Long tenantId, Long userId){
// Test Scenarios:
// Can userId be null or wrong?
// Can session be null or wrong?
try {
tenantId = getKeyFromMap(tenantUserMap, tenantId); // Required for Weak Maps to Work Correctly
Set<Long> users = tenantUserMap.get(tenantId);
if (users == null) {
users = new HashSet<>();
}
users.add(userId);
tenantUserMap.put(tenantId, users);
return tenantId;
} catch (Exception e) {
logger.error("Couldn't Save Tenant User");
return null;
}
}
public static void updateUserSessionKeys(Session session, Long tenantId, Long userId) {
try {
List<Long> userSessionKeys = sessions.get(session);
userSessionKeys.add(0, tenantId);
userSessionKeys.add(1, userId);
} catch (Exception e) {
logger.error("Couldn't Update User Session Keys");
}
}
// removeUserId() should remove an { userId -> session } entry in sessionMap
// returns: "0" if success and "-1" otherwise
public static int removeUserId( Long userId) {
try {
sessionMap.remove(userId);
return 0;
} catch (Exception e) {
return -1;
}
}
// sendAccesspointStatus() should compose JSON message and pass it to sendToUser()
// returns: "0" if success and "-1" otherwise
public static int sendClosedStatus(Long userId, JSONObject accesspointStatus) {
try {
JSONObject accesspointStatusEventMessage = (JSONObject) createMessage(WebSocketEvents.OnClosedStatus, accesspointStatus);
sendToUser(userId, accesspointStatusEventMessage);
return 0;
} catch (Exception e) {
return -1;
}
}
// sendToUser() sends message to session that is mapped to userId
// returns: "0" if success and "-1" otherwise
public static int sendToUser( Long userId, JSON message) {
if (sessionMap.containsKey(userId)) {
Set<Session> sessionsForUser = sessionMap.get(userId);
for (Session session : sessionsForUser) {
if (!session.isOpen()) {
sessions.remove(session);
continue;
}
sendToSession(session, message);
}
return 0;
} else {
return -1;
}
}
// sendToSession() sends string message to session
// returns: "0" if success and "-1" otherwise
private static int sendToSession( Session session, JSON message){
try {
try {
Long tenantId = sessions.get(session).get(0);
((JSONObject) message).put("tenantId", tenantId);
} catch (Exception e) {
logger.error("No tenantId Found");
}
session.getBasicRemote().sendText(message.toString());
return 0;
} catch (IOException e) {
try {
session.close();
} catch (IOException ex) {
}
closeSessionProperly(session);
sessions.remove(session);
return -1;
}
}
// sendToSession() sends string message to session
// returns: "0" if success and "-1" otherwise
private static int sendToSession( Session session, String message){
try {
JSONObject newMessage = JSONObject.fromObject(message);
try {
Long tenantId = sessions.get(session).get(0);
newMessage.put("tenantId", tenantId);
} catch (Exception e) {
logger.error("No tenantId Found");
}
session.getBasicRemote().sendText(newMessage.toString());
return 0;
} catch (IOException e) {
closeSessionProperly(session);
sessions.remove(session);
return -1;
}
}
}
Probably not the only bug, but your WebSocketSessionHandler class is not thread-safe. It uses WeakHashMap internally which is not synchronized. Concurrent access to these maps may result in unexpected behavior, which may or may not cause the effects you are seeing.
Turns out this was a correct assumption. A general rule of thumb: Unexpected Behaviour ~ Race Condition
Probably not the only bug, but your WebSocketSessionHandler class is not thread-safe. It uses WeakHashMap internally which is not synchronized. Concurrent access to these maps may result in unexpected behavior, which may or may not cause the effects you are seeing.
(copied from my comment. Turns out this was the solution)
I am writing unit test for the following code which uses AsyncHttpClient. The use of CountDownLatch and decrementing of the CountDownLatch within the implementation of FutureCallback is causing my JUnit testcase to hang waiting for countdown latch to be decremented. Within the JUnit test I am capturing the FutureCallback using ArgumentCaptor and then using thenAnswer I am calling the completed method to decrement the count down latch. But it doesn't seems to work, any ideas will be helpful.
public List<QueryResponse> execute(Query query) {
List<QueryResponse> res = new ArrayList<QueryResponse>();
try {
List<HttpRequestBase> requests = query.generateHttpRequests();
List<Future<HttpResponse>> futures = new ArrayList<Future<HttpResponse>>();
final CountDownLatch requestCompletionCDLatch = new CountDownLatch(requests.size());
for (HttpRequestBase request : requests) {
futures.add(httpClient.execute(request, new FutureCallback<HttpResponse>() {
#Override
public void failed(Exception e) {
logger.error("Error while executing: " + request.toString(), e);
requestCompletionCDLatch.countDown();
}
#Override
public void completed(HttpResponse result) {
requestCompletionCDLatch.countDown();
}
#Override
public void cancelled() {
logger.info("Request cancelled while executing: " + request.toString());
requestCompletionCDLatch.countDown();
}
}));
}
requestCompletionCDLatch.await();
for (Future<HttpResponse> future : futures) {
HttpResponse response = future.get(rcaRequestTimeout, TimeUnit.SECONDS);
int status = response.getStatusLine().getStatusCode();
if (status != HttpStatus.SC_OK) {
logger.warn("Query with non-success status " + status);
} else {
res.add(query.parseResponse(response.getEntity().getContent()));
}
}
} catch (IOException | InterruptedException | ExecutionException | TimeoutException e) {
logger.error("Error while querying", e);
} catch (URISyntaxException e) {
logger.error("Error while generating the query", e);
}
return res;
}
My unit test is as follows:
#Test
public void testHttpError() throws InterruptedException, ExecutionException, TimeoutException {
StatusLine status = Mockito.mock(StatusLine.class);
when(status.getStatusCode()).thenReturn(400);
HttpResponse response = Mockito.mock(HttpResponse.class);
when(response.getStatusLine()).thenReturn(status);
Future<HttpResponse> future = Mockito.mock(Future.class);
when(future.get(anyLong(), any())).thenReturn(response);
CloseableHttpAsyncClient httpClient = Mockito.mock(CloseableHttpAsyncClient.class);
ArgumentCaptor<HttpUriRequest> requestCaptor = ArgumentCaptor.forClass(HttpUriRequest.class);
ArgumentCaptor<FutureCallback<HttpResponse>> futureCallbackCaptor = ArgumentCaptor.forClass((Class)FutureCallback.class);
when(httpClient.execute(any(), any())).thenReturn(future).thenAnswer(new Answer() {
#Override
public Object answer(InvocationOnMock invocation) throws Throwable {
verify(httpClient).execute(requestCaptor.capture(), futureCallbackCaptor.capture());
futureCallbackCaptor.getValue().completed(response);
return null;
}
});
StubbedRcaClient rcaClient = new StubbedRcaClient(httpClient);
Query query = new Query("abc", "xyz", RcaHttpRequestType.GET, 1000);
List<QueryResponse> res = rcaClient.execute(query);
assertEquals(0, res.size());
IOUtils.closeQuietly(rcaClient);
}
I made this work by updating my JUnit as follows:
when(httpClient.execute(any(), any())).thenAnswer(new Answer<Future<HttpResponse>>() {
#Override
public Future<HttpResponse> answer(InvocationOnMock invocation) throws Throwable {
verify(httpClient).execute(requestCaptor.capture(), futureCallbackCaptor.capture());
futureCallbackCaptor.getValue().completed(response);
return future;
}
});