Thread safe caching of JAX-WS clientproxies

According to http://cxf.apache.org/faq.html#FAQ-AreJAX-WSclientproxiesthreadsafe? , clientproxies are not thread safe so I decided to create a Component which can pool the connections. I do it it by using Singleton design pattern.
What is handled by this Singleton:

  • This class creates the connections on the start and then caches them
  • If the connection to the server is not available it will wait for 1 second and try again 10 times.
  • A scheduler thread runs every 10 minutes to check if an error occurred in between. If an error occurred then all the cached instances are created new.
  • This implementation is thread safe

ConnectionCache.java

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import javax.xml.ws.Service;

import org.apache.commons.lang3.concurrent.BasicThreadFactory;
import org.apache.logging.log4j.Logger;

/**
 * This keeps the cache of MAX_CONNECTIONS_POOL_SIZE number of
 * connections and tries to shares them equally amongst the threads. All the
 * connections are created right at the start and if an error occurs then the
 * cache is created again.
 *
 *
 * Are JAX-WS client proxies thread safe
 * According to the JAX-WS spec,
 * the client proxies are NOT thread safe. To write portable code, you should
 * treat them as non-thread safe and synchronize access or use a pool of
 * instances or similar.
 */

public abstract class ConnectionCache<T extends Service> {

 private static final Logger logger = org.apache.logging.log4j.LogManager.getLogger(ConnectionCache.class);

 private static final int MAX_CONNECTIONS_POOL_SIZE = 16;
 
 private final BlockingQueue<T> freeConnections = new ArrayBlockingQueue<T>(MAX_CONNECTIONS_POOL_SIZE);
 
 private final BlockingQueue<T> usedConnections = new ArrayBlockingQueue<T>(MAX_CONNECTIONS_POOL_SIZE);
 
 private int totalConnections = 0;
 
 private boolean firstTimeLoad = true;
 
 private boolean shutdownCalled = false;

 private ScheduledExecutorService scheduler;


 public void init() {
  logger.info("starting connectionCache");
  logger.info("start caching connections"); ;;
  BasicThreadFactory factory = new BasicThreadFactory.Builder()
    .namingPattern("connectioncache-scheduler-thread-%d").build();
  scheduler = Executors.newScheduledThreadPool(1, factory);

  scheduler.scheduleAtFixedRate(new Runnable() {
   @Override
   public void run() {
    try {
     initializeCache();
     firstTimeLoad = false;
    } catch (InterruptedException e) {
     Thread.currentThread().interrupt();
    }
    
   }

  }, 0, 10, TimeUnit.MINUTES);

 }


 public void destroy() {
  shutdownCalled = true;
  try {
   scheduler.awaitTermination(1, TimeUnit.SECONDS);
  } catch (InterruptedException e) {
   Thread.currentThread().interrupt();
  }
 }

 /**
  * It is important to call release after calling this method to release the connection
  * @return
  * @throws Exception
  * @throws InterruptedException
  */
 public T acquire() throws Exception, InterruptedException {
  T take;
   if(totalConnections < MAX_CONNECTIONS_POOL_SIZE){
    synchronized (ConnectionCache.class) {
     if(totalConnections < MAX_CONNECTIONS_POOL_SIZE){
      logger.info("service connects, free connections: " + freeConnections.size() + ", used connections: " + usedConnections.size());
      T service = tryConnect();
      freeConnections.offer(service, 1, TimeUnit.MINUTES);
      totalConnections++;
      logger.info("connect done, free connections: " + freeConnections.size() + ", used connections: " + usedConnections.size());
     }
    }
   }
   // a thread waits a maximum 13 minutes for a connection
   take = freeConnections.poll(13, TimeUnit.MINUTES);
   if (take == null) {
    throw new Exception("connection not avaliable for 13 minutes so aborting connection wait!");
   }
   usedConnections.offer(take);
   
//  }
  return take;
 }
 
 
 public void release(T service){
   try {
    boolean remove = usedConnections.remove(service);
    if(remove){
     // this will make sure that if a cleanup has been done
     // the removed objects are not added to the connections cache
     freeConnections.offer(service,1, TimeUnit.MINUTES);
    }
   } catch (InterruptedException e) {
    Thread.currentThread().interrupt();
   }
 }
 
 private void initializeCache() throws InterruptedException {
  try {
   loadCache();
   logger.info(firstTimeLoad ? "connections creation finished successfully!" : "connections checked successfully!");
  } catch (Exception e) {
   logger.error("error while initializing the cache");
  }
 }

 private void loadCache() throws Exception {
  logger.info(firstTimeLoad ? "create and cache service connections" : "checking  connections!" );
  for (int i = 0; i < MAX_CONNECTIONS_POOL_SIZE; i++) {
   if(shutdownCalled){
    return;
   }
   release(acquire());
  }
 }

 public void refreshCache()  {
  usedConnections.clear();
  freeConnections.clear();
  totalConnections =0;
 }
 
 private T tryConnect() throws Exception  {
  boolean connect = true;
  int tryNum = 0;
  T service = null;
  while (connect &amp;&amp; !Thread.currentThread().isInterrupted() &amp;&amp; !shutdownCalled) {
   try {
    service = create();
    connect = false;
   } catch (Exception e) {
    tryNum = tryReconnect(tryNum, e);
   }
  }
  return service;
 }

 private int tryReconnect(int tryNum, Exception e) throws Exception {
  logger.warn(Thread.currentThread().getName() + " service service not available! : " + e);
  // try 10 times, if
  if (tryNum++ < 10) {
   try {
    logger.warn(Thread.currentThread().getName() + " wait 1 second");
    Thread.sleep(1000);
   } catch (InterruptedException f) {
    // restore interrupt
    Thread.currentThread().interrupt();
   }
  } else {
   logger.warn(" service could not connect, number of times tried: " + (tryNum - 1));
   refreshCache();
   throw new Exception(e);
  }
  logger.info(" try reconnect number: " + tryNum);
  return tryNum;
 }

 
 protected abstract T create() ;
}

So you can use this as your base class and have to implement only the create method for your service.
Here in an example of a sub class:

MyServiceConnectionCache.java

import com.tak.ws.MyService;

public class MyServiceConnectionCache extends ConnectionCache<MyService> {

 private static  MyServiceConnectionCache myServiceConnectionCache = new MyServiceConnectionCache();
 
 public static MyServiceConnectionCache getInstance(){
  return myServiceConnectionCache;
 }
 
 private MyServiceConnectionCache() {
 }
 
 @Override
 protected MyService create() {
  return new MyService();
 }

}

Here is the Test for testing the service is working

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

import com.tak.ws.MyService;

public class Test {

 
 public static void main(String[] args) throws InterruptedException {
  ExecutorService executor = Executors.newFixedThreadPool(100);
  for(int i = 0; i < 100; i++){
   executor.execute(new Runnable() {
    
    @Override
    public void run() {
     MyServiceConnectionCache myServiceConnectionCache = MyServiceConnectionCache.getInstance();
     
     MyService service = null;
     try {
      service = myServiceConnectionCache.acquire();
      System.out.println(service.getMyServicePort().getAllLanguages());
     } catch (Exception e) {
      // TODO Auto-generated catch block
      e.printStackTrace();
     }finally{
      if(service != null){
       myServiceConnectionCache.release(service);
      }
     }
     
    }
   });
  }
  executor.shutdown();
  executor.awaitTermination(5, TimeUnit.MINUTES);
  
 }
}

And the console output for the above test :

16:22:47.661 [pool-1-thread-1] INFO  ConnectionCache:92 - service connects, free connections: 0, used connections: 0
16:22:47.832 [pool-1-thread-1] INFO  ConnectionCache:96 - connect done, free connections: 1, used connections: 0
16:22:47.832 [pool-1-thread-4] INFO  ConnectionCache:92 - service connects, free connections: 1, used connections: 0
16:22:47.875 [pool-1-thread-4] INFO  ConnectionCache:96 - connect done, free connections: 1, used connections: 1
16:22:47.875 [pool-1-thread-20] INFO  ConnectionCache:92 - service connects, free connections: 0, used connections: 2
16:22:47.925 [pool-1-thread-20] INFO  ConnectionCache:96 - connect done, free connections: 1, used connections: 2
16:22:47.933 [pool-1-thread-15] INFO  ConnectionCache:92 - service connects, free connections: 0, used connections: 3
16:22:47.972 [pool-1-thread-15] INFO  ConnectionCache:96 - connect done, free connections: 1, used connections: 3
16:22:47.972 [pool-1-thread-12] INFO  ConnectionCache:92 - service connects, free connections: 0, used connections: 4
16:22:48.014 [pool-1-thread-12] INFO  ConnectionCache:96 - connect done, free connections: 1, used connections: 4
16:22:48.015 [pool-1-thread-7] INFO  ConnectionCache:92 - service connects, free connections: 0, used connections: 5
16:22:48.073 [pool-1-thread-7] INFO  ConnectionCache:96 - connect done, free connections: 1, used connections: 5
16:22:48.075 [pool-1-thread-6] INFO  ConnectionCache:92 - service connects, free connections: 0, used connections: 6
16:22:48.119 [pool-1-thread-6] INFO  ConnectionCache:96 - connect done, free connections: 1, used connections: 6
16:22:48.119 [pool-1-thread-13] INFO  ConnectionCache:92 - service connects, free connections: 0, used connections: 7
16:22:48.159 [pool-1-thread-13] INFO  ConnectionCache:96 - connect done, free connections: 1, used connections: 7
16:22:48.159 [pool-1-thread-16] INFO  ConnectionCache:92 - service connects, free connections: 0, used connections: 8
16:22:48.194 [pool-1-thread-16] INFO  ConnectionCache:96 - connect done, free connections: 1, used connections: 8
16:22:48.194 [pool-1-thread-21] INFO  ConnectionCache:92 - service connects, free connections: 0, used connections: 9
16:22:48.228 [pool-1-thread-21] INFO  ConnectionCache:96 - connect done, free connections: 1, used connections: 9
16:22:48.229 [pool-1-thread-24] INFO  ConnectionCache:92 - service connects, free connections: 0, used connections: 10
16:22:48.260 [pool-1-thread-24] INFO  ConnectionCache:96 - connect done, free connections: 1, used connections: 10
16:22:48.260 [pool-1-thread-23] INFO  ConnectionCache:92 - service connects, free connections: 0, used connections: 11
16:22:48.292 [pool-1-thread-23] INFO  ConnectionCache:96 - connect done, free connections: 1, used connections: 11
16:22:48.308 [pool-1-thread-28] INFO  ConnectionCache:92 - service connects, free connections: 0, used connections: 12
16:22:48.345 [pool-1-thread-28] INFO  ConnectionCache:96 - connect done, free connections: 1, used connections: 12
16:22:48.345 [pool-1-thread-31] INFO  ConnectionCache:92 - service connects, free connections: 0, used connections: 13
16:22:48.383 [pool-1-thread-31] INFO  ConnectionCache:96 - connect done, free connections: 1, used connections: 13
16:22:48.383 [pool-1-thread-29] INFO  ConnectionCache:92 - service connects, free connections: 0, used connections: 14
16:22:48.415 [pool-1-thread-29] INFO  ConnectionCache:96 - connect done, free connections: 1, used connections: 14
16:22:48.416 [pool-1-thread-32] INFO  ConnectionCache:92 - service connects, free connections: 0, used connections: 15
16:22:48.449 [pool-1-thread-32] INFO  ConnectionCache:96 - connect done, free connections: 1, used connections: 15
[FRENCH, ENGLISH, RUSSIAN]
[FRENCH, ENGLISH, RUSSIAN]
[FRENCH, ENGLISH, RUSSIAN]
[FRENCH, ENGLISH, RUSSIAN]
[FRENCH, ENGLISH, RUSSIAN]
[FRENCH, ENGLISH, RUSSIAN]
[FRENCH, ENGLISH, RUSSIAN]
[FRENCH, ENGLISH, RUSSIAN]
[FRENCH, ENGLISH, RUSSIAN]
[FRENCH, ENGLISH, RUSSIAN]
.........

Please drop your comments suggestions!

Leave a Comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.