eclipse-ee4j / eclipse-ee4j/orb

ORB#destroy() does not close IIOP ServerSocket / ServerSocketChannel

Open
#330 1 comment 0 reactions 0 assignees View on GitHub
Dominant language
Java
Stars
30
Forks
28
Avg merge
3h 43m
Merged PRs (30d)
9

Description

### Problem

In our project, we are using the GlassFish CORBA ORB libraries (`org.glassfish.corba:glassfish-corba-orb`, tested on both **`5.0.0`** and **`5.0.2`**) in a plain Java SE application (standalone, without any Jakarta EE / Java EE application server).

Our Java process is a long-running service that does not terminate when the ORB is stopped. The ORB is dynamically initialized and stopped during the lifecycle of the JVM (e.g. stopping or restarting the CORBA workers in our application). Because the JVM continues running, the server port remains open.

Now after the restart, CORBA opens a new random port, but clients trying to connect to the old port hang because the old socket is still accepting TCP connections instead of refusing them.

e.g. after orb.destroy(), 'netstat -aon' still shows the port open and listening.

This might relate to previously reported issues [#29](https://github.com/eclipse-ee4j/orb/issues/29) and [#26](https://github.com/eclipse-ee4j/orb/issues/26).

---
### Workaround
Using reflection to manually call close on the acceptors and selector does close the port.

### Possible Cause

Looking at `TransportManagerImpl` and `AcceptorImpl` in `glassfish-corba-orb` (5.0.0 / 5.0.2):

When `orb.destroy()` is called, it invokes `TransportManagerImpl.close()`.
`TransportManagerImpl.close()` iterates over `outboundConnectionCaches` and `inboundConnectionCaches` and closes them, but it never closes the registered `Acceptor` instances:

```java
// TransportManagerImpl.java
public void close() {
for (OutboundConnectionCache cache : outboundConnectionCaches.values()) {
cache.close();
}
for (InboundConnectionCache cache : inboundConnectionCaches.values()) {
cache.close();
}
getSelector(0).close();
// The registered Acceptors (and their ServerSockets) are never closed here
}
```
### Minimal Reproducer Test (JUnit 5)
Here is a self-contained test reproducing the behavior:

```java
import org.junit.jupiter.api.Test;
import org.omg.CORBA.ORB;
import org.omg.CORBA.ServerRequest;
import org.omg.PortableServer.DynamicImplementation;
import org.omg.PortableServer.POA;
import org.omg.PortableServer.POAHelper;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Properties;

import static org.junit.jupiter.api.Assertions.fail;

/**
* Minimal, self-contained test case demonstrating the server socket port leak
* in GlassFish CORBA ORB (5.0.0 & 5.0.2) after orb.destroy().
*/
public class MinimalPortLeakReproducerTest {

static class TestServant extends DynamicImplementation {
@Override
public void invoke(ServerRequest request) {
}

@Override
public String[] _all_interfaces(POA poa, byte[] objectId) {
return new String[] { "IDL:TestServant:1.0" };
}
}

private static String getNetstatOutput(int port) {
StringBuilder sb = new StringBuilder();
try {
boolean isWindows = System.getProperty("os.name", "").toLowerCase().contains("win");
ProcessBuilder pb = isWindows
? new ProcessBuilder("cmd.exe", "/c", "netstat -aon | findstr :" + port)
: new ProcessBuilder("sh", "-c", "netstat -an | grep " + port);
Process p = pb.start();
java.nio.charset.Charset charset = isWindows ? java.nio.charset.Charset.forName("Cp850") : java.nio.charset.StandardCharsets.UTF_8;
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(p.getInputStream(), charset))) {
String line;
while ((line = reader.readLine()) != null) {
sb.append(" ").append(line).append(System.lineSeparator());
}
}
p.waitFor();
} catch (Exception e) {
sb.append(" (Could not run netstat: ").append(e.getMessage()).append(")");
}
return sb.length() > 0 ? sb.toString() : " (No socket found via netstat)\n";
}

@Test
public void testServerPortLeakAfterDestroy() throws Exception {
Properties props = new Properties();
props.put("org.omg.CORBA.ORBClass", "com.sun.corba.ee.impl.orb.ORBImpl");
props.put("org.omg.CORBA.ORBSingletonClass", "com.sun.corba.ee.impl.orb.ORBSingleton");

ORB orb = ORB.init(new String[0], props);
POA rootPOA = POAHelper.narrow(orb.resolve_initial_references("RootPOA"));
rootPOA.the_POAManager().activate();

// Register servant to open server port
TestServant servant = new TestServant();
org.omg.CORBA.Object ref = rootPOA.servant_to_reference(servant);
String ior = orb.object_to_string(ref);

// Get server port from acceptor
com.sun.corba.ee.spi.orb.ORB eeOrb = (com.sun.corba.ee.spi.orb.ORB) orb;
int serverPort = eeOrb.getTransportManager().getAcceptors().iterator().next().getPort();

System.out.println("Servant IOR: " + ior);
System.out.println("Active Server Port: " + serverPort);

// Verify that server port is reachable
try (Socket socket = new Socket()) {
socket.connect(new InetSocketAddress("localhost", serverPort), 1000);
}

// Standard shutdown
rootPOA.destroy(true, true);
orb.shutdown(true);
orb.destroy();

Thread.sleep(2000);

// Query OS netstat to prove the socket is still in LISTENING state
String netstatOutput = getNetstatOutput(serverPort);
System.out.println("\n[netstat output for port " + serverPort + " after orb.destroy()]:");
System.out.println(netstatOutput.isEmpty() ? " (No socket found)" : netstatOutput);
System.out.println();

// The port should be closed.
// Problem: Port is still open, rebind throws BindException.
try (ServerSocket rebindSocket = new ServerSocket(serverPort)) {
System.out.println("SUCCESS: Port was released and rebind succeeded.");
} catch (IOException e) {
fail("BUG REPRODUCED: Port " + serverPort + " is still bound after orb.destroy()! (" + e.getMessage() + ")\n"
+ "OS netstat still reports:\n" + netstatOutput);
}
}
}

```
org.opentest4j.AssertionFailedError: Port 64539 is still bound after orb.destroy()! (Address already in use: bind)
at MinimalPortLeakReproducerTest.testServerPortLeakAfterDestroy(MinimalPortLeakReproducerTest.java:58)

Contributor guide

Open the contributing guide

Research direction

Start with TransportManagerImpl.close() and AcceptorImpl, then run the MinimalPortLeakReproducerTest described in the issue. Trace how registered acceptors and their ServerSockets are handled during orb.destroy(). Done means the reproducer can rebind the original port after shutdown without the port remaining in LISTENING state.

Written by the indexing model from the issue text.

Assessment

Tech stack
java
Domain
networking
Issue type
Bug
Difficulty
3/5
Estimated time
1-2 days
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
76/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.