Monday, September 15, 2014

Java process that can be stopped via sockets

While writing the code for Flip a coin and get the same results 100 times in a row, one thing that troubled me was finding a nice way to stop the process. The easiest (but ugliest) thing I could do was press control+c on the console to force-close (kill) the app. This is terrible because it doesn't allow me to clean up anything. I couldn't create a closing report on results. Worse, as I worked towards the next step - writing results to a database - I had no way to close the database. If you kill an app, it prevents finally clauses or try-with-resources blocks from doing their jobs.

One easy way to stop a long running process is to listen for keyboard input. The code below will do this.

public void run() {
   boolean keepGoing = true;
   try (Scanner keyboard = new Scanner(System.in)) {
      while (keepGoing) {
         System.out.println("Doing work in a loop. Enter STOP to stop.");
         if (keyboard.hasNextLine()) {
            final String next = keyboard.nextLine();
            System.out.println("You entered [" + next + "].");
            if (next.equals("STOP")) {
               keepGoing = false;
            }
         }
      }
   }
   System.out.println("Process stopped.");
}

The problem with this is that my process uses the console to output ongoing results. I write to a file as well, but I find it very useful to write to the console too (standard out) and I don't want to interrupt console reporting. It was really awkward listening for keyboard input from a console that I am busy writing to. It might say "Enter STOP" at the top of the console, but after a few hours, 100s of lines will be written out. So I needed another way.

The approach I decided upon was to use sockets. I based it on the Echo Client/Server from the Java Trail, Reading from and Writing to a Socket. It meant that I had to make my application multi-threaded. Here is quasi Sequence Diagram representing the interactions in this pattern.

My main class (RandomInARow - running in a thread) starts up the StopListener thread.

// Spin up different thread - socket to listen for STOP signal.
stopListener = new StopListener();
stopListener.start();

And the RandomInARow's run() method does the work, and with every iteration, it will first check if the stopListener thread has received the STOP signal. If it has, then the application should clean up and finish.

public void run() {
   // Now do our own work in this thread.
   int target = INCREMENT_BY;
   while (stopListener.isActive()) {
      repeatUntilWeGetCount(target);
      target += INCREMENT_BY;
   }
   System.out.println("See results in log file [" + logFile.toAbsolutePath()
         + "].\n");
}

StopListener is a thread that will open up a socket and listen to it. As soon as it reads anything, it sets the boolean active to false and exits.

public void run() {
   try (ServerSocket serverSocket = new ServerSocket(port);
         Socket clientSocket = serverSocket.accept();
         PrintWriter out =
               new PrintWriter(clientSocket.getOutputStream(), true);
         BufferedReader in =
               new BufferedReader(new InputStreamReader(
                     clientSocket.getInputStream()));) {
      String inputLine;
      while ((inputLine = in.readLine()) != null) {
         message.append(inputLine);
         out.println("Stop signal received.");
         active = false;
         break;
      }
   } catch (IOException e) {
      System.out.println("Exception caught when trying to listen on port ["
            + port + "] or listening for a connection");
      System.out.println(e.getMessage());
   }
}

It is doing a few other things of course.

  • Note the use of try-with-resources to open up several Autocloseable objects: a ServerSocket, Socket, a PrintWriter and a BufferedReader. From the Java Trail, The try-with-resources Statement: note that the close methods of resources are called in the opposite order of their creation.
  • ServerSocket and Socket are two objects that talk to each other across a network via a given port. A Socket is a client: it sends requests to a server and reads a response. A ServerSocket is a server: it listens for requests from a client and sends a response. Note that "across a network" can still mean a client and server sitting on the same machine - they will open a port and send messages via that port in the same way they would if they were on different machines. On a Windows machine, when running a program that attempts to send/receive data via sockets, your Firewall may show a pop-up asking if you give permission for the network communication to happen.
  • Although ServerSocket and Socket objects communicate to each other, they don't know how read/write strings, integers etc - that job goes to a PrintWriter (which does the writing) and a BufferedReader (which does the reading).
  • The while loop will wait for something to be read, acknowledge the signal, store a message and set a flag to outside code that processing should stop.
    1. The while loop will wait for something to be read (in.readLine()).
    2. When something is read, it is stored (with message.append(inputLine)).
    3. An acknowledgment is then written back to the client Socket (out.println("Stop signal received.")).
    4. The boolean called active is set to false.
    5. The loop is stopped via break.

StopSender is a thread that will open up a socket on the same port that StopListener listens to. It sends a message via that port - either a default message or one supplied by the user. It reads a response which it outputs to console, and then finishes.

public void sendStopSignal(final String message) {
   try (Socket socket = new Socket(HOST, StopListener.DEFAULT_PORT);
         PrintWriter out = new PrintWriter(socket.getOutputStream(), true);
         BufferedReader in =
               new BufferedReader(new InputStreamReader(
                     socket.getInputStream()));) {
      out.println(message);
      final String received = in.readLine();
      System.out.println(received);
   } catch (UnknownHostException e) {
      System.err.println("Don't know about host [" + HOST + "].");
      System.exit(1);
   } catch (IOException e) {
      System.err.println("Couldn't get I/O for the connection to [" + HOST
            + "].");
      System.exit(1);
   }
}

Note that HOST is defined to be localhost. This means that StopSender must be run on the same machine that StopListener is being run on! I have defined this in a variable to make it just a bit easier if I want to expand this to run across machines in the future.

public static final String HOST = "localhost";

Would you like to try out this code? You can download the jar containing all the class and java files at https://app.box.com/s/x6rxzaw7v8jpt52lrkej. It was compiled under JDK 7. Save the jar file somewhere, and in the console, run it with the command below. The program will output to the console and to a text file.

java -jar randomInARow.jar

You can control where the text file is written to by supplying a path argument as below (the path/folder must already exist).

java -jar randomInARow.jar "dir/to/write/log/to"

The program will run forever, but you can stop it by running StopSender in another console.

java -cp randomInARow.jar com.rmb.randomnodatabase.taskmanagement.StopSender

This uses the default STOP message. You can supply your own message by adding an argument to the commaned, as below.

java -cp randomInARow.jar com.rmb.randomnodatabase.taskmanagement.StopSender "Because I want this to STOP NOW!"

Here is an extract from the log created when I used the above command to stop the application.

New count [ 24] at [14 Sep 2014, 11:36:16.946 PM] after 1 seconds and 538 milliseconds.
New count [ 22] at [14 Sep 2014, 11:36:17.371 PM] after 1 seconds and 963 milliseconds.
New count [ 26] at [14 Sep 2014, 11:36:17.459 PM] after 2 seconds and 51 milliseconds.
Finished at [14 Sep 2014, 11:36:18.454 PM] with target [30]
-----
User cancelled operation. It took 3 seconds and 46 milliseconds to get [26] results in a row - with target [30].
Reason for stopping: Because I want this to STOP NOW
-----
How often we flipped the same result [   1] times in a row: 13737650.
How often we flipped the same result [   2] times in a row: 6866089.
How often we flipped the same result [   3] times in a row: 3430324.

If you just want to see the code without downloading the jar, you can see the three classes I talk about there in separate pastebin pages.