Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Issue #7344 - wait for forked jetty process #7374

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,16 @@
import java.net.ConnectException;
import java.net.InetAddress;
import java.net.Socket;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;

import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;
import org.apache.maven.plugins.annotations.Mojo;
import org.apache.maven.plugins.annotations.Parameter;


/**
* This goal stops a running instance of jetty.
*
Expand Down Expand Up @@ -72,38 +75,146 @@ public void execute() throws MojoExecutionException, MojoFailureException

String command = "forcestop";

try (Socket s = new Socket(InetAddress.getByName("127.0.0.1"), stopPort);)
if (stopWait > 0)
{
OutputStream out = s.getOutputStream();
out.write((stopKey + "\r\n" + command + "\r\n").getBytes());
out.flush();

if (stopWait > 0)
//try to get the pid of the forked jetty process
Long pid = null;
try
{
String response = send(stopKey + "\r\n" + "pid" + "\r\n", stopWait);
pid = Long.valueOf(response);
}
catch (NumberFormatException e)
{
getLog().info("Server returned bad pid");
}
catch (ConnectException e)
{
s.setSoTimeout(stopWait * 1000);
s.getInputStream();
//jetty not running, no point continuing
getLog().info("Jetty not running!");
return;
}
catch (Exception e)
{
//jetty running, try to stop it regardless of error
getLog().error(e);
}

getLog().info("Waiting " + stopWait + " seconds for jetty to stop");
LineNumberReader lin = new LineNumberReader(new InputStreamReader(s.getInputStream()));
String response;
boolean stopped = false;
while (!stopped && ((response = lin.readLine()) != null))
//now send the stop command and wait for confirmation - either an ack from jetty, or
//that the process has stopped
if (pid == null)
{
//no pid, so just wait until jetty reports itself stopped
try
{
getLog().info("Waiting " + stopWait + " seconds for jetty to stop");
String response = send(stopKey + "\r\n" + command + "\r\n", stopWait);

if ("Stopped".equals(response))
{
stopped = true;
getLog().info("Server reports itself as stopped");
}
else
getLog().info("Couldn't verify server as stopped, received " + response);
}
catch (ConnectException e)
{
getLog().info("Jetty not running!");
}
catch (Exception e)
{
getLog().error(e);
}
}
else
{
//wait for pid to stop
getLog().info("Waiting " + stopWait + " seconds for jetty " + pid + " to stop");
Optional<ProcessHandle> optional = ProcessHandle.of(pid);
optional.ifPresentOrElse(p ->
{
try
{
send(stopKey + "\r\n" + command + "\r\n", 0);
CompletableFuture<ProcessHandle> future = p.onExit();
if (p.isAlive())
{
p = future.get(stopWait, TimeUnit.SECONDS);
}

if (p.isAlive())
getLog().info("Couldn't verify server process stop");
else
getLog().info("Server process stopped");
}
catch (ConnectException e)
{
//jetty not listening on the given port, don't wait for the process
getLog().info("Jetty not running!");
}
catch (TimeoutException e)
{
getLog().error("Timeout expired while waiting for server process to stop");
}
catch (Throwable e)
{
getLog().error(e);
}
}, () -> getLog().info("Process not running"));
}
}
catch (ConnectException e)
else
{
getLog().info("Jetty not running!");
//send the stop command but don't wait to verify the stop
getLog().info("Stopping jetty");
try
{
send(stopKey + "\r\n" + command + "\r\n", 0);
}
catch (ConnectException e)
{
getLog().info("Jetty not running!");
}
catch (Exception e)
{
getLog().error(e);
}
}
catch (Exception e)
}

/**
* Send a command to a jetty process, optionally waiting for a response.
*
* @param command the command to send
* @param wait length of time in sec to wait for a response
* @return the response, if any, to the command
* @throws Exception
*/
private String send(String command, int wait)
throws Exception
{
String response = null;
try (Socket s = new Socket(InetAddress.getByName("127.0.0.1"), stopPort); OutputStream out = s.getOutputStream();)
{
getLog().error(e);
out.write(command.getBytes());
out.flush();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, TCP is not very friendly with open/write/close.
You should always read to have a confirmation that the other peer received the data (even if it's only reading -1).
The other machine could be overloaded, so SYN+PSH+FIN may be received by the recipient at OS level (but not yet at application level), but because the machine is overloaded timeouts may happen, so the OS decides to RST back to the sender, which has already closed.
The net result is that the sender does not know that something went wrong, the recipient never received the data at the application level, and everything went into /dev/null.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the sender is interested in getting confirmation from the remote jetty, then they configure a non-zero stopWait timeout, in which case we set the socket soTimeout and read from it to get the "Shutting down" message. Alternatively, if we have been able to obtain the pid, we send the message and then wait up to stopWait seconds on the process itself to stop. So in either case, there will be a wait for either a response message, or the process to die. If no wait is configured, then we don't wait for any response, we just send the message and exit.

Copy link
Contributor

@sbordet sbordet Jan 17, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are assuming that the message you send reaches the other end. I'm saying that it may not.
Now, if you care whether the message reaches the other end, you should read, at least a -1.
If you don't care whether the message reaches the other end, then you may not read (like the code above), just don't assume that send() actually produces the results you want.
It's not about getting confirmation about the specific command.
It's about getting a confirmation that the message even arrived to the other process.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So @sbordet how long does the sender need to wait to read from the socket to ensure that the message was sent? The behaviour of the ShutdownMonitor is to stop jetty before replying with "Stopped" and exiting. In the case where the caller has not configured a stopWait time they do not want to wait until the socket is closed etc.


if (wait > 0)
{
//Wait for a response
s.setSoTimeout(wait * 1000);

try (LineNumberReader lin = new LineNumberReader(new InputStreamReader(s.getInputStream()));)
{
response = lin.readLine();
}
}
else
{
//Wait only a small amount of time to ensure TCP has sent the message
s.setSoTimeout(1000);
s.getInputStream().read();
}

return response;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
//
// ========================================================================
// Copyright (c) 1995-2022 Mort Bay Consulting Pty Ltd and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
//
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
// ========================================================================
//

package org.eclipse.jetty.maven.plugin;

import java.net.ServerSocket;

import org.eclipse.jetty.toolchain.test.IO;

/**
* MockShutdownMonitor
* A helper class that grabs a ServerSocket, spawns a thread and then
* passes the ServerSocket to the Runnable. This class has a main so
* that it can be used for forking, to mimic the actions of the
* org.eclipse.jetty.server.ShutdownMonitor.
*/
public class MockShutdownMonitor
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why can't we use ShutdownMonitor?
It should be possible because there are tests that use ShutdownMonitor directly and not a mock.
We really want to test the real ShutdownMonitor, not this mock class.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@sbordet I'm not interested in testing ShutdownMonitor - we have other tests for that ;). The unit test it relates to is of the JettyStopMojo - all I want to do is to test the behaviour of the JettyStopMojo, and for that I need to control the behaviour of the ShutdownMonitor and its ShutdownMonitorRunnable. I can't do that by subclassing because ShutdownMonitor is effectively not extensible, hence the mocks.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I made ShutdownMonitor.start() and stop() public and I can rewrite these tests using directly ShutdownMonitor without 200 lines of mock code that must to be bug-free and maintained.
I'd prefer to fix ShutdownMonitor than adding a mock for it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@sbordet I have changed the final test testStopWait to talk to the real ShutdownMonitor. None of the other tests can use the real class because I need to test failure/fallback modes, and ShutdownMonitor is unsubclassable (private no-args constructor).

{
String key;
MockShutdownMonitorRunnable testerRunnable;
ServerSocket serverSocket;

public MockShutdownMonitor(String key, MockShutdownMonitorRunnable testerRunnable)
throws Exception
{
this.key = key;
this.testerRunnable = testerRunnable;
listen();
}

private ServerSocket listen()
throws Exception
{
serverSocket = new ServerSocket(0);
try
{
serverSocket.setReuseAddress(true);
return serverSocket;
}
catch (Throwable e)
{
IO.close(serverSocket);
throw e;
}
}

public int getPort()
{
if (serverSocket == null)
return 0;
return serverSocket.getLocalPort();
}

public void start()
throws Exception
{
testerRunnable.setServerSocket(serverSocket);
testerRunnable.setKey(key);
Thread thread = new Thread(testerRunnable);
thread.setDaemon(true);
thread.setName("Tester Thread");
thread.start();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
//
// ========================================================================
// Copyright (c) 1995-2022 Mort Bay Consulting Pty Ltd and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
//
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
// ========================================================================
//

package org.eclipse.jetty.maven.plugin;

import java.io.InputStreamReader;
import java.io.LineNumberReader;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.nio.charset.StandardCharsets;

import org.eclipse.jetty.toolchain.test.IO;

/**
* MockShutdownMonitorRunnable
*
* Mimics the actions of the org.eclipse.jetty.server.ShutdownMonitor.ShutdownMonitorRunnable
* to aid testing.
*/
public class MockShutdownMonitorRunnable implements Runnable
{
ServerSocket serverSocket;
String key;
String statusResponse = "OK";
String pidResponse;
String defaultResponse = "Stopped";
boolean exit;

public void setExit(boolean exit)
{
this.exit = exit;
}

public void setKey(String key)
{
this.key = key;
}

public void setServerSocket(ServerSocket serverSocket)
{
this.serverSocket = serverSocket;
}

public void setPidResponse(String pidResponse)
{
this.pidResponse = pidResponse;
}

public void run()
{
try
{
while (true)
{
try (Socket socket = serverSocket.accept())
{
LineNumberReader reader = new LineNumberReader(new InputStreamReader(socket.getInputStream()));
String receivedKey = reader.readLine();
if (!key.equals(receivedKey))
{
continue;
}

String cmd = reader.readLine();
OutputStream out = socket.getOutputStream();

if ("status".equalsIgnoreCase(cmd))
{
out.write((statusResponse + "\r\n").getBytes(StandardCharsets.UTF_8));
out.flush();
}
else if ("pid".equalsIgnoreCase(cmd))
{
out.write((pidResponse + "\r\n").getBytes(StandardCharsets.UTF_8));
out.flush();
}
else
{
out.write((defaultResponse + "\r\n").getBytes(StandardCharsets.UTF_8));
out.flush();
if (exit)
System.exit(0);
}
}
catch (Throwable x)
{
x.printStackTrace();
}
}
}
catch (Throwable x)
{
x.printStackTrace();
}
finally
{
IO.close(serverSocket);
}
}
}
Loading