10
\$\begingroup\$

I have an application that essentially "pings" all of the servers on my network. I have about 100 servers, and this ping will happen every 10 seconds.

public class HealthChecker {
 private static List<InetSocketAddress> servers = new     ArrayList<InetSocketAddress>();
 public static void main(String[] args){
  // not shown: populating servers list
  new Thread(){
   public void run(){
    while (true){
     try {
      Thread.sleep(10*1000);
      for (final InetSocketAddress server : servers){
       new Thread(){
        public void run(){
         Socket connection = new Socket();
         try {
          connection.connect(server, 5*1000);
         } catch (IOException e) {
          servers.remove(server);
         }
        }
       }.start();
      }
     } catch (InterruptedException e) {
      e.printStackTrace();
     }
    }
   }
  }.start();
 }

So, do you think this code can be improved? I really feel like opening a new thread every time I make a connection isn't a good thing.

EDIT: So looking over all the proposed solutions, they are all very good, and I will over the next week try and all and see which one appears to be the most efficient.

\$\endgroup\$
1
  • \$\begingroup\$ I have added an answer with an example which utilizes ThreadPoolExecutor \$\endgroup\$
    – Cengiz Can
    Jan 4, 2014 at 13:13

4 Answers 4

6
\$\begingroup\$

Here's an example adapted to your case which uses ThreadPoolExecutor.

This example allows a configurable throughput while establishing TCP connections to servers.

Checking Future.get() for each submitted worker thread ensures that no servers are checked more than others.

What happens here:

  1. Target domains are added to a list
  2. If the domain is an unknown host (DNS unreachable) than it's skipped.
  3. Resolvable domains are submitted as 1 domain per 1 thread into the pool.
  4. If the pool gets overflown by PingWork threads, that submission is delayed for 3 seconds until the pool is ready to accept.
  5. Each TCP port 80 connection has a timeout of 5 seconds.
  6. When there are no exceptions thrown by a TCP connection to a certain host, it is assumed reachable and not reported.
  7. When all PingWork threads finish, main thread waits for 10 seconds until next round.

Here's the source

// package
// imports

public class Main {
    private static List<InetSocketAddress> servers = 
            new ArrayList<InetSocketAddress>();

    public static void main(String[] args) 
            throws InterruptedException, ExecutionException {
        String[] domains = new String[]{
                "3Com.com",
                // etc. etc.
                "Kai.com"
        };

        for (String hostname : domains) {
            try {
                servers.add(
                        new InetSocketAddress(
                                InetAddress.getByName(hostname), 
                                80
                        )
                );
            } catch (UnknownHostException e) {
                System.out.println("Unknown host: " + hostname);
            }
        }

        System.out.println(
                "Total number of target servers: " + servers.size()
        );

        BlockingQueue<Runnable> work = 
                new ArrayBlockingQueue<Runnable>(5);

        ThreadPoolExecutor pool = new ThreadPoolExecutor(
                5,                      // corePoolSize
                10,                     // maximumPoolSize
                5000,                   // keepAliveTime
                TimeUnit.MILLISECONDS,  // TimeUnit
                work                    // workQueue
        );

        pool.prestartAllCoreThreads();

        pool.setRejectedExecutionHandler(
                new RejectedExecutionHandler() {
                    @Override
                    public void rejectedExecution(
                            Runnable r, 
                            ThreadPoolExecutor executor
                    ) {
                        System.out.println("Work queue is currently full");
                        try {
                            Thread.sleep(3000);
                        } catch (InterruptedException ignore) {

                        }
                        executor.submit(r);
                    }
                }
        );

        Collection<Future<?>> futures = new LinkedList<Future<?>>();

        while (true) {
            for (InetSocketAddress server : servers) {
                futures.add(pool.submit(new PingWork(server)));
            }
            for (Future<?> future : futures) {
                future.get();
            }
            System.out.println(
                    "All servers checked. Will wait for 10 seconds until next round"
            );
            Thread.sleep(10000);
        }

    }

    private static class PingWork implements Runnable {
        private static final int TIMEOUT = 5000;
        private InetSocketAddress target;

        private PingWork(InetSocketAddress target) {
            this.target = target;
        }

        @Override
        public void run() {
            Socket connection = new Socket();
            boolean reachable;

            try {
                try {
                    connection.connect(target, TIMEOUT);
                } finally {
                    connection.close();
                }
                reachable = true;
            } catch (Exception e) {
                reachable = false;
            }

            if (!reachable) {
                System.out.println(
                        String.format(
                                "%s:%d was UNREACHABLE",
                                target.getAddress(),
                                target.getPort()
                        )
                );
            }
        }
    }
}

Here's the output of 30 domain sample

    Total number of target servers: 30
    Work queue is currently full
    Work queue is currently full
    Amdahl.com/129.212.11.21:80 was UNREACHABLE
    All servers checked. Will wait for 10 seconds until next round
    Work queue is currently full
    Work queue is currently full
    Amdahl.com/129.212.11.21:80 was UNREACHABLE
    All servers checked. Will wait for 10 seconds until next round   

You can increase the maximumPoolSize of ThreadPoolExecutor in order to get all domains into queue in just one iteration. However I left that part for you.

\$\endgroup\$
3
\$\begingroup\$

You are not pinging, you are establishing a TCP connection. The three way handshake is way more overhead than a simple ICMP ECHO_REQUEST (aka ping). Have a look at InetAddress.isReachable(int).

Using a threadpool was already stated. But it would be best to avoid threading completely. Unfortunately I didn't find a non blocking ICMP implementation for Java. But if you go the protocol stack a bit up (TCP or better UDP) you can use Java NIO.

Personally I wouldn't write a dedicated monitor application, I would use existing stable software like Nagios or OpenNMS.

\$\endgroup\$
2
  • \$\begingroup\$ Unfortunately I have to write my own application, unless you are aware of an application with an API... not much of help to me. As mentioned before, I would love an example of thread pooling if you could, or link me possibly to some sources if that isn't too much trouble. Finally, I've read that InetAdress.isReachable doesn't work, is that false? \$\endgroup\$
    – user872129
    Jan 4, 2014 at 8:03
  • \$\begingroup\$ InetAdress.isReachable() has restrictions. Read the API. If you can't use ICMP then go the NIO way (preferably over UDP). BTW OpenNMS might have a Java API. \$\endgroup\$ Jan 4, 2014 at 8:10
3
\$\begingroup\$
  1. Instead of spawning a new thread every time you probably should execute them on a thread pool.

  2. You want to wrap your servers list in a Collections.synchronizedList() or else you might run into unexpected race conditions when removing multiple servers at the same time from the list from two different threads.

Update

Disclaimer: I don't write much Java so the below is mostly put together from the Java docs and might not compile and could certainly be improved but should illustrate the idea of using a threadpool for executing the network check.

Apparently there is no way of stopping it nicely but I leave that to you.

public class ServerTester implements Runnable {

    private InetSocketAddress server;
    List<InetSocketAddress> servers;

    public ServerTester(InetSocketAddress server, List<InetSocketAddress> servers){
        this.server = server;
        this.servers = servers;
    }

    @Override
    public void run() {
        Socket connection = new Socket();
        try {
            connection.connect(server, 5*1000);
        } catch (IOException e) {
            servers.remove(server);
        }
    }
}

public class HealthChecker {
    private static List<InetSocketAddress> servers = Collections.synchronizedList(new ArrayList<InetSocketAddress>());

    public static void main(String[] args) {
        // not shown: populating servers list

        ExecutorService executor = Executors.newCachedThreadPool();

        new Thread() {
            public void run() {
                while (true) {
                    try {
                        Thread.sleep(10*1000);
                        for (final InetSocketAddress server : servers){
                            Runnable tester = new ServerTester(server, servers);
                            executor.execute(tester);
                        }
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }.start();
    }
}
\$\endgroup\$
2
  • \$\begingroup\$ Would you mind showing an example of using a Thread Pool (if you can provide one). \$\endgroup\$
    – user872129
    Jan 4, 2014 at 6:49
  • \$\begingroup\$ @Dyla: Updated my answer \$\endgroup\$
    – ChrisWue
    Jan 4, 2014 at 9:54
3
\$\begingroup\$

ICMP does not appear to be an option for you... you say you are pinging 'servers', but I think what you mean is that you are pinging 'services' (actual applications running on the server). You connect to the host:port combination. You could even have multiple services running on a single server.

A brief run-down of your code suggests:

  1. every 10 seconds (or so) you create a bunch of threads.
  2. each thread tries to connect to a server:port combination.
  3. if the connect fails, you remove the server

It has already been pointed out to you that the remove process should use some form of locking... using a simple ArrayList will lead to corrupt lists as each of the 100 threads may try to modify the list at the same time.

I suggest a different approach.

I recommend creating a thread for each server you want to monitor. As the server comes available, it adds the server to the 'alive' list. As the server 'drops', it removes it from the alive list.

The simple Socket connection to the server is all you need to worry about when it is alive. The monitor thread can sit there waiting for the socket to die. If it dies, it tries to reconnect. You can choose a different approach for monitoring the TCP state, but, the model of having a separate continuous thread for each monitored server, and the concept of having a maintained collection of 'alive' threads is the important part.

Consider the following class, which has a convenient Main method:

package ping;

import java.io.IOException;
import java.io.InputStream;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.SocketTimeoutException;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class ServiceMonitor {

    private final class Monitor implements Runnable {

        private final InetSocketAddress  tomonitor;

        public Monitor(InetSocketAddress  service) {
            super();
            this.tomonitor = service;
        }


        @Override
        public void run() {
            while (true) {
                try (Socket remote = new Socket()) {
                    System.out.println("Trying to Connect to " + tomonitor);
                    remote.connect(tomonitor, 5*1000);
                    if (registerRemote(tomonitor, this)) {
                        try (InputStream is = remote.getInputStream()) {
                            byte[] buffer = new byte[512];
                            // This will likely never loop, simply wait for it to close.
                            while (is.read(buffer) >= 0) {
                                // still alive ...
                            }
                        } catch (IOException ioe) {
                            // unexpected condition, but this means the server is dead... that's all we need to know.
                            //ioe.printStackTrace();
                        } finally {
                            deregisterRemote(tomonitor, this);
                        }
                    }
                } catch (SocketTimeoutException ste) {
                    // simply try again....
                } catch (IOException e) {
                    // could not connect to remote?
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e1) {
                        return;
                    }
                    //e.printStackTrace();
                }
            }


        }


    }

    private final ConcurrentHashMap<InetSocketAddress, Monitor> alive = new ConcurrentHashMap<>();
    private final ThreadGroup tgroup = new ThreadGroup("Server Monitor Group");
    private final Lock monlock = new ReentrantLock();
    private final Condition idlecondition = monlock.newCondition();
    private final Condition busycondition = monlock.newCondition();


    public ServiceMonitor() {

    }

    public void monitorService(InetSocketAddress remote) {
        if (!alive.containsKey(remote)) {
            Thread mthread = new Thread(tgroup, new Monitor(remote), "Monitor Thread: " + remote.toString());
            mthread.setDaemon(true);
            mthread.start();
        }
    }

    private boolean registerRemote(InetSocketAddress tomonitor, Monitor monitor) {
        if (null == alive.putIfAbsent(tomonitor, monitor)) {
            // do any notification if you need to that a server is available....
            System.out.println("Register " + tomonitor + " Currently " + alive.size() + " alive");
            monlock.lock();
            try {
                busycondition.signalAll();
            } finally {
                monlock.unlock();
            }
            return true;
        }
        return false;
    }

    private void deregisterRemote(InetSocketAddress tomonitor, Monitor monitor) {
        if (alive.replace(tomonitor, monitor, monitor)) {
            // we are currently the holder... good.
            alive.remove(tomonitor);
            // do any notification if you need to that a server is 'down'.
            System.out.println("Deregister " + tomonitor + " Currently " + alive.size() + " alive");
            monlock.lock();
            try {
                if (alive.isEmpty()) {
                    idlecondition.signalAll();
                }
            } finally {
                monlock.unlock();
            }
        }
    }

    public void waitAllDead() {
        monlock.lock();
        try {
            while (!alive.isEmpty()) {
                idlecondition.await();
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
            Thread.currentThread().interrupt();
        } finally {
            monlock.unlock();
        }
    }

    public void waitBusy() {
        monlock.lock();
        try {
            while (alive.isEmpty()) {
                busycondition.await();
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
            Thread.currentThread().interrupt();
        } finally {
            monlock.unlock();
        }
    }

    public static void main(String[] args) {
        ServiceMonitor monitor = new ServiceMonitor();
        for (int i = 0; i < 10; i++) {
            monitor.monitorService(new InetSocketAddress("localhost", 40000 + i));
        }
        monitor.waitBusy();
        monitor.waitAllDead();
        System.out.println("Complete!");
    }
}

This class monitors services simply by their active TCP socket. It has a main method which monitors 10 ports from 40000 through 40009.

Here is a simple dummy server that creates services on those ports.... (and I used it for testing).

package ping;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Random;
import java.util.concurrent.ThreadLocalRandom;

public class Service extends Thread {

    private final Socket socket;

    public Service(int port, Socket socket) {
        super("Service for local:" + port + " remote:" + socket.getInetAddress().toString());
        this.socket = socket;
    }

    @Override
    public void run() {
        try (
            OutputStream os = socket.getOutputStream();
            InputStream is = socket.getInputStream();
        ) {
            final int delay = (1 + (int)(Math.random() * 5));
            Thread.sleep(delay * 1000);
            System.out.println(Thread.currentThread().getName() + " Sleeping for " + delay + " seconds.");
            os.flush();
        } catch (IOException | InterruptedException ioe) {
            ioe.printStackTrace();
        } finally {
            System.out.println("Closing " + Thread.currentThread().getName());
        }
    }

    public static void main(String[] args) throws NumberFormatException, IOException {
        int cnt = 10;
        for (int i = 0; i < cnt; i++) {
            final int port = 40000 + i;
            Runnable controller = new Runnable() {
                public void run() {
                    try (
                        ServerSocket ss = new ServerSocket(port);
                    ) {
                        int port = ss.getLocalPort();
                        System.out.println("Listening on port " + port);
                        Socket sock = null;
                        Random rand = ThreadLocalRandom.current();
                        while ((sock = ss.accept()) != null) {
                            Service service = new Service(port, sock);
                            sock = null;
                            service.start();
                            Thread.sleep(5000 + rand.nextInt(15000));
                        }
                    } catch (IOException | InterruptedException ioe) {
                        ioe.printStackTrace();
                    }
                };
            };
            Thread thread = new Thread(controller, "Controller " + i);
            thread.start();
        }
    }

}
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.