Skip to content

Race Conditions when closing server, port is still bound #879

@Jonahss

Description

@Jonahss

Describe the bug

Hello! I'm not a Java expert, but was happy to find this easy websocket implementation.
I've run into a race condition with closing a server, and immediately opening a new server on the same port.
This came up as part of my unit tests. Each test passes in isolation, but when they run quickly one after another, well, they started to throw java.net.BindException: Address already in use exceptions.

I've built a really great repro case for you! I was really embarrassed to add a Thread.sleep() call after my server.close() call, so I created a repro.

The following code will run just fine. This is the naiive case of starting a server, closing it, and starting another server. The issue comes up in the particular way that my client is spamming the server to create new connections.

import org.java_websocket.WebSocket;
import org.java_websocket.handshake.ClientHandshake;
import org.java_websocket.server.WebSocketServer;
import org.junit.Test;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.URISyntaxException;
import java.nio.ByteBuffer;

public class WebsocketServerTest {

    @Test
    public void QuickStopTest() throws IOException, InterruptedException, URISyntaxException {
        int port = 8765;

        class SimpleServer extends WebSocketServer {

            public SimpleServer(InetSocketAddress address) {
                super(address);
            }

            @Override
            public void onOpen(WebSocket conn, ClientHandshake handshake) {
                conn.send("Welcome to the server!"); //This method sends a message to the new client
                broadcast( "new connection: " + handshake.getResourceDescriptor() ); //This method sends a message to all clients connected
                System.out.println("new connection to " + conn.getRemoteSocketAddress());
            }

            @Override
            public void onClose(WebSocket conn, int code, String reason, boolean remote) {
                System.out.println("closed " + conn.getRemoteSocketAddress() + " with exit code " + code + " additional info: " + reason);
            }

            @Override
            public void onMessage(WebSocket conn, String message) {
                System.out.println("received message from "	+ conn.getRemoteSocketAddress() + ": " + message);
            }

            @Override
            public void onMessage( WebSocket conn, ByteBuffer message ) {
                System.out.println("received ByteBuffer from "	+ conn.getRemoteSocketAddress());
            }

            @Override
            public void onError(WebSocket conn, Exception ex) {
                System.err.println("an error occured on connection " + conn.getRemoteSocketAddress()  + ":" + ex);
            }

            @Override
            public void onStart() {
                System.out.println("server started successfully");
            }
        }
        SimpleServer serverA = new SimpleServer(new InetSocketAddress("localhost", port));
        SimpleServer serverB = new SimpleServer(new InetSocketAddress("localhost", port));

        serverA.start();

        Thread.sleep(3000);

        serverA.stop();
        serverB.start();
    }
}

So that code passes.

BUT. When I have a server spamming connections, the code above will sometimes pass, sometimes fail with my "Address already in use error" and sometimes fail with a ClosedSelectorException.

Here's the code which spams the connection. I did my best to help here, but I had to pull the code out of a Python script which was running, I couldn't repro with a java client.

(use python3 and install the websockets module)

import asyncio
import websockets

# requirements.txt:
# websockets==6.0.0

async def websocket_loop():
    """
    Processes messages from self.queue until mitmproxy shuts us down.
    """
    while True:
        try:
            async with websockets.connect('ws://localhost:8765', max_size = None) as websocket:
                while True:
                    # Make sure connection is still live.
                    await websocket.ping()

        except websockets.exceptions.ConnectionClosed:
            # disconnected from server
            pass
        except BrokenPipeError:
            # Connect failed
            pass
        except IOError:
            # disconnected from server mis-transfer
            pass
        except:
            print("[mitmproxy-node plugin] Unexpected error:", sys.exc_info())
            traceback.print_exc(file=sys.stdout)

worker_event_loop = asyncio.new_event_loop()
worker_event_loop.run_until_complete(websocket_loop())

Expected behavior
Well, my unit tests should be able to start servers on the same port as a previously stopped server without adding a 500ms sleep in between.

Debug log
The the debug log (set the log level to TRACE) to help explain your problem.
^I could do this if you walk me through how.

Environment(please complete the following information):

  • Version used: 2.0-m02
  • Java version: 1.8
  • Operating System and version: MacOS 10.14.4 (18E226)
  • Endpoint Name and version: N/A (self hosted in repro case)
  • Link to your project: coming soon...

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions