diff --git a/src/main/java/org/jvnet/hudson/plugins/port_allocator/ClusterPortAllocationManager.java b/src/main/java/org/jvnet/hudson/plugins/port_allocator/ClusterPortAllocationManager.java new file mode 100644 index 0000000..36c4c79 --- /dev/null +++ b/src/main/java/org/jvnet/hudson/plugins/port_allocator/ClusterPortAllocationManager.java @@ -0,0 +1,39 @@ +package org.jvnet.hudson.plugins.port_allocator; + +import hudson.model.AbstractBuild; + +import java.io.IOException; +import java.util.HashSet; +import java.util.Set; + +public class ClusterPortAllocationManager implements PortAllocationManager { + + /** + * Ports currently in use, to the build that uses it. + */ + private final Set ports = new HashSet(); + + public int allocateRandom(AbstractBuild owner, int prefPort) throws InterruptedException, IOException { + throw new UnsupportedOperationException("Allocate a random port is not available in cluster mode."); + } + + public synchronized int allocate(AbstractBuild owner, int port) throws InterruptedException, IOException { + while (ports.contains(port)) { + wait(); + } + + ports.add(port); + + return port; + } + + public synchronized boolean isFree(int port) { + return !ports.contains(port); + } + + public synchronized void free(int n) { + ports.remove(n); + notifyAll(); // wake up anyone who's waiting for this port + } + +} diff --git a/src/main/java/org/jvnet/hudson/plugins/port_allocator/NodePortAllocationManager.java b/src/main/java/org/jvnet/hudson/plugins/port_allocator/NodePortAllocationManager.java new file mode 100644 index 0000000..aed14b5 --- /dev/null +++ b/src/main/java/org/jvnet/hudson/plugins/port_allocator/NodePortAllocationManager.java @@ -0,0 +1,224 @@ +package org.jvnet.hudson.plugins.port_allocator; + +import hudson.model.AbstractBuild; +import hudson.model.Computer; +import hudson.remoting.Callable; + +import java.io.IOException; +import java.net.ServerSocket; +import java.util.HashMap; +import java.util.Map; +import java.util.Random; + +/** + * Manages ports in use. + * + * @author Rama Pulavarthi + * @author Kohsuke Kawaguchi + */ +public final class NodePortAllocationManager implements PortAllocationManager { + private final Computer node; + + /** Maximum number of tries to allocate a specific port range. */ + private static final int MAX_TRIES = 100; + + /** + * Ports currently in use, to the build that uses it. + */ + private final Map ports = new HashMap(); + + NodePortAllocationManager(Computer node) { + this.node = node; + } + + public synchronized int allocateRandom(AbstractBuild owner, int prefPort) throws InterruptedException, IOException { + int i; + try { + // try to allocate preferential port, + i = allocatePort(prefPort); + } catch (PortUnavailableException ex) { + // if not available, assign a random port + i = allocatePort(0); + } + ports.put(i,owner); + return i; + } + + /** + * Allocate a continuous range of ports within specified limits. + * The caller is responsible for freeing the individual ports within + * the allocated range. + * @param portAllocator + * @param build the current build + * @param start the first in the range of allowable ports + * @param end the last entry in the range of allowable ports + * @param count the number of ports to allocate + * @param isConsecutive true if the allocated ports should be consecutive + * @return the ports allocated + * @throws InterruptedException if the allocation was interrupted + * @throws IOException if the allocation failed + */ + public int[] allocatePortRange( + final AbstractBuild owner, + int start, int end, int count, boolean isConsecutive) + throws InterruptedException, IOException { + int[] allocated = new int[count]; + + boolean allocationFailed = true; + Random rnd = new Random(); + + // Attempt the whole allocation a few times using a brute force approach. + for (int trynum = 0; (allocationFailed && (trynum < MAX_TRIES)); trynum++) { + allocationFailed = false; + + // Allocate all of the ports in the range + for (int offset = 0; offset < count; offset++) { + + final int requestedPort; + if (!isConsecutive || (offset == 0)) { + requestedPort = rnd.nextInt((end - start) - count) + start; + } else { + requestedPort = allocated[0] + offset; + } + try { + final int i; + synchronized (this) { + i = allocatePort(requestedPort); + ports.put(i, owner); + } + allocated[offset] = i; + } catch (PortUnavailableException ex) { + // Did not get requested port + allocationFailed = true; + // Free off allocated ports ready to try again + for (int freeOffset = offset - 1; freeOffset >= 0; freeOffset--) { + free(allocated[freeOffset]); + } + // Try again from the beginning. + break; + } + } + } + if (allocationFailed) { + throw new IOException("Failed to allocate port range"); + } + return allocated; + } + + public synchronized int allocate(AbstractBuild owner, int port) throws InterruptedException, IOException { + while(ports.get(port)!=null) + wait(); + + /* + TODO: + despite the fact that the following commented out implementation is smarter, + I need to comment it out for the following reasons: + + Often, a job that requires a particular port (let's say 8080) fails in the middle, + leaving behind a process that occupies the port. A typical defensive measure taken + to fight this is for the build to try to stop the server at the beginning. + + While clunky, this technique is used frequently, and if this method blocks until + the port actually becomes available (not only in our book-keeping but also at OS level), + then such a run-away process will remain forever and thus the next build will + never happen. + + This unfortunately doesn't protect us from a case where job X runs and dies, leaving + a server behind, then job Y comes in, looking for the same port, and fails. + + We need to revisit this. + */ +// while(true) { +// try { +// allocatePort(port); +// break; +// } catch (PortUnavailableException e) { +// // the requested port is not available right now. +// // the port might be blocked by a reason outside Hudson, +// // so we need to occasionally wake up to see if the port became available +// wait(10000); +// } +// } + ports.put(port,owner); + return port; + } + + public synchronized boolean isFree(int port) { + AbstractBuild owner = ports.get(port); + if (owner == null) { + return true; + } + return false; + } + + public synchronized void free(int n) { + ports.remove(n); + notifyAll(); // wake up anyone who's waiting for this port + } + + +// private boolean isPortInUse(final int port) throws InterruptedException { +// try { +// return node.getChannel().call(new Callable() { +// public Boolean call() throws IOException { +// new ServerSocket(port).close(); +// return true; +// } +// }); +// } catch (IOException e) { +// return false; +// } +// } + + /** + * @param port 0 to assign a free port + * @return port that gets assigned + * @throws PortUnavailableException + * If the specified port is not available + */ + private int allocatePort(final int port) throws InterruptedException, IOException { + AbstractBuild owner = ports.get(port); + if(owner!=null) + throw new PortUnavailableException("Owned by "+owner); + + return node.getChannel().call(new AllocateTask(port)); + } + + static final class PortUnavailableException extends IOException { + PortUnavailableException(String msg) { + super(msg); + } + + // not compatible with JDK1.5 +// PortUnavailableException(Throwable cause) { +// super(cause); +// } + + private static final long serialVersionUID = 1L; + } + + private static final class AllocateTask implements Callable { + private final int port; + + public AllocateTask(int port) { + this.port = port; + } + + public Integer call() throws IOException { + ServerSocket server; + try { + server = new ServerSocket(port); + } catch (IOException e) { + // fail to bind to the port + PortUnavailableException t = new PortUnavailableException(e.getLocalizedMessage()); + t.initCause(e); + throw t; + } + int localPort = server.getLocalPort(); + server.close(); + return localPort; + } + + private static final long serialVersionUID = 1L; + } +} diff --git a/src/main/java/org/jvnet/hudson/plugins/port_allocator/Pool.java b/src/main/java/org/jvnet/hudson/plugins/port_allocator/Pool.java index 4d04539..e9e26af 100644 --- a/src/main/java/org/jvnet/hudson/plugins/port_allocator/Pool.java +++ b/src/main/java/org/jvnet/hudson/plugins/port_allocator/Pool.java @@ -9,6 +9,7 @@ public class Pool { public String name; public String ports; + public boolean isGlobal; public int[] getPortsAsInt() { diff --git a/src/main/java/org/jvnet/hudson/plugins/port_allocator/PooledPortType.java b/src/main/java/org/jvnet/hudson/plugins/port_allocator/PooledPortType.java index 3ed9f01..ea01124 100644 --- a/src/main/java/org/jvnet/hudson/plugins/port_allocator/PooledPortType.java +++ b/src/main/java/org/jvnet/hudson/plugins/port_allocator/PooledPortType.java @@ -30,14 +30,7 @@ public PooledPortType(String name) { * Wait for a short period if no free port is available, then try again. */ @Override - public Port allocate( - AbstractBuild build, - final PortAllocationManager manager, - int prefPort, - Launcher launcher, - BuildListener buildListener - ) throws IOException, InterruptedException { - + public Port allocate(AbstractBuild build, final PortAllocationManager manager, int prefPort, Launcher launcher, BuildListener buildListener) throws IOException, InterruptedException { try { while (true) { diff --git a/src/main/java/org/jvnet/hudson/plugins/port_allocator/PortAllocationManager.java b/src/main/java/org/jvnet/hudson/plugins/port_allocator/PortAllocationManager.java index c884fb0..f648b12 100644 --- a/src/main/java/org/jvnet/hudson/plugins/port_allocator/PortAllocationManager.java +++ b/src/main/java/org/jvnet/hudson/plugins/port_allocator/PortAllocationManager.java @@ -1,258 +1,32 @@ package org.jvnet.hudson.plugins.port_allocator; import hudson.model.AbstractBuild; -import hudson.model.Computer; -import hudson.remoting.Callable; import java.io.IOException; -import java.lang.ref.WeakReference; -import java.net.ServerSocket; -import java.util.HashMap; -import java.util.Map; -import java.util.Random; -import java.util.WeakHashMap; +public interface PortAllocationManager { -/** - * Manages ports in use. - * - * @author Rama Pulavarthi - * @author Kohsuke Kawaguchi - */ -public final class PortAllocationManager { - private final Computer node; + /** + * Assigns the requested port. + * + * This method blocks until the port becomes available. + */ + int allocate(AbstractBuild owner, int port) throws InterruptedException, IOException; - /** Maximum number of tries to allocate a specific port range. */ - private static final int MAX_TRIES = 100; + /** + * Allocates a random port on the Computer where the jobs gets executed. + * + *

+ * If the preferred port is not available, assigns a random available port. + * + * @param prefPort + * Preffered port. This method trys to assign this port, and upon failing, fall back to + * assigning a random port. + */ + int allocateRandom(AbstractBuild owner, int prefPort) throws InterruptedException, IOException; - /** - * Ports currently in use, to the build that uses it. - */ - private final Map ports = new HashMap(); + void free(int n); - private static final Map> INSTANCES = - new WeakHashMap>(); + boolean isFree(int port); - private PortAllocationManager(Computer node) { - this.node = node; - } - - /** - * Allocates a random port on the Computer where the jobs gets executed. - * - *

- * If the preferred port is not available, assigns a random available port. - * - * @param prefPort - * Preffered port. This method trys to assign this port, and upon failing, fall back to - * assigning a random port. - */ - public synchronized int allocateRandom(AbstractBuild owner, int prefPort) throws InterruptedException, IOException { - int i; - try { - // try to allocate preferential port, - i = allocatePort(prefPort); - } catch (PortUnavailableException ex) { - // if not available, assign a random port - i = allocatePort(0); - } - ports.put(i,owner); - return i; - } - - /** - * Allocate a continuous range of ports within specified limits. - * The caller is responsible for freeing the individual ports within - * the allocated range. - * @param portAllocator - * @param build the current build - * @param start the first in the range of allowable ports - * @param end the last entry in the range of allowable ports - * @param count the number of ports to allocate - * @param isConsecutive true if the allocated ports should be consecutive - * @return the ports allocated - * @throws InterruptedException if the allocation was interrupted - * @throws IOException if the allocation failed - */ - public int[] allocatePortRange( - final AbstractBuild owner, - int start, int end, int count, boolean isConsecutive) - throws InterruptedException, IOException { - int[] allocated = new int[count]; - - boolean allocationFailed = true; - Random rnd = new Random(); - - // Attempt the whole allocation a few times using a brute force approach. - for (int trynum = 0; (allocationFailed && (trynum < MAX_TRIES)); trynum++) { - allocationFailed = false; - - // Allocate all of the ports in the range - for (int offset = 0; offset < count; offset++) { - - final int requestedPort; - if (!isConsecutive || (offset == 0)) { - requestedPort = rnd.nextInt((end - start) - count) + start; - } else { - requestedPort = allocated[0] + offset; - } - try { - final int i; - synchronized (this) { - i = allocatePort(requestedPort); - ports.put(i, owner); - } - allocated[offset] = i; - } catch (PortUnavailableException ex) { - // Did not get requested port - allocationFailed = true; - // Free off allocated ports ready to try again - for (int freeOffset = offset - 1; freeOffset >= 0; freeOffset--) { - free(allocated[freeOffset]); - } - // Try again from the beginning. - break; - } - } - } - if (allocationFailed) { - throw new IOException("Failed to allocate port range"); - } - return allocated; - } - - /** - * Assigns the requested port. - * - * This method blocks until the port becomes available. - */ - public synchronized int allocate(AbstractBuild owner, int port) throws InterruptedException, IOException { - while(ports.get(port)!=null) - wait(); - - /* - TODO: - despite the fact that the following commented out implementation is smarter, - I need to comment it out for the following reasons: - - Often, a job that requires a particular port (let's say 8080) fails in the middle, - leaving behind a process that occupies the port. A typical defensive measure taken - to fight this is for the build to try to stop the server at the beginning. - - While clunky, this technique is used frequently, and if this method blocks until - the port actually becomes available (not only in our book-keeping but also at OS level), - then such a run-away process will remain forever and thus the next build will - never happen. - - This unfortunately doesn't protect us from a case where job X runs and dies, leaving - a server behind, then job Y comes in, looking for the same port, and fails. - - We need to revisit this. - */ -// while(true) { -// try { -// allocatePort(port); -// break; -// } catch (PortUnavailableException e) { -// // the requested port is not available right now. -// // the port might be blocked by a reason outside Hudson, -// // so we need to occasionally wake up to see if the port became available -// wait(10000); -// } -// } - ports.put(port,owner); - return port; - } - - public synchronized boolean isFree(int port) { - AbstractBuild owner = ports.get(port); - if (owner == null) { - return true; - } - return false; - } - - public static PortAllocationManager getManager(Computer node) { - PortAllocationManager pam; - WeakReference ref = INSTANCES.get(node); - if (ref != null) { - pam = ref.get(); - if (pam != null) - return pam; - } - pam = new PortAllocationManager(node); - INSTANCES.put(node, new WeakReference(pam)); - return pam; - } - - public synchronized void free(int n) { - ports.remove(n); - notifyAll(); // wake up anyone who's waiting for this port - } - - -// private boolean isPortInUse(final int port) throws InterruptedException { -// try { -// return node.getChannel().call(new Callable() { -// public Boolean call() throws IOException { -// new ServerSocket(port).close(); -// return true; -// } -// }); -// } catch (IOException e) { -// return false; -// } -// } - - /** - * @param port 0 to assign a free port - * @return port that gets assigned - * @throws PortUnavailableException - * If the specified port is not available - */ - private int allocatePort(final int port) throws InterruptedException, IOException { - AbstractBuild owner = ports.get(port); - if(owner!=null) - throw new PortUnavailableException("Owned by "+owner); - - return node.getChannel().call(new AllocateTask(port)); - } - - static final class PortUnavailableException extends IOException { - PortUnavailableException(String msg) { - super(msg); - } - - // not compatible with JDK1.5 -// PortUnavailableException(Throwable cause) { -// super(cause); -// } - - private static final long serialVersionUID = 1L; - } - - private static final class AllocateTask implements Callable { - private final int port; - - public AllocateTask(int port) { - this.port = port; - } - - public Integer call() throws IOException { - ServerSocket server; - try { - server = new ServerSocket(port); - } catch (IOException e) { - // fail to bind to the port - PortUnavailableException t = new PortUnavailableException(e.getLocalizedMessage()); - t.initCause(e); - throw t; - } - int localPort = server.getLocalPort(); - server.close(); - return localPort; - } - - private static final long serialVersionUID = 1L; - } } diff --git a/src/main/java/org/jvnet/hudson/plugins/port_allocator/PortAllocationManagerFactory.java b/src/main/java/org/jvnet/hudson/plugins/port_allocator/PortAllocationManagerFactory.java new file mode 100644 index 0000000..96ca6dd --- /dev/null +++ b/src/main/java/org/jvnet/hudson/plugins/port_allocator/PortAllocationManagerFactory.java @@ -0,0 +1,32 @@ +package org.jvnet.hudson.plugins.port_allocator; + +import hudson.model.Computer; + +import java.lang.ref.WeakReference; +import java.util.Map; +import java.util.WeakHashMap; + +public class PortAllocationManagerFactory { + + public static final ClusterPortAllocationManager CLUSTER_INSTANCE = new ClusterPortAllocationManager(); + + private static final Map> INSTANCES = + new WeakHashMap>(); + + public static NodePortAllocationManager getManager(Computer node) { + NodePortAllocationManager pam; + WeakReference ref = INSTANCES.get(node); + if (ref != null) { + pam = ref.get(); + if (pam != null) + return pam; + } + + pam = new NodePortAllocationManager(node); + + INSTANCES.put(node, new WeakReference(pam)); + + return pam; + } + +} diff --git a/src/main/java/org/jvnet/hudson/plugins/port_allocator/PortAllocator.java b/src/main/java/org/jvnet/hudson/plugins/port_allocator/PortAllocator.java index bc12a32..48826e6 100644 --- a/src/main/java/org/jvnet/hudson/plugins/port_allocator/PortAllocator.java +++ b/src/main/java/org/jvnet/hudson/plugins/port_allocator/PortAllocator.java @@ -22,7 +22,7 @@ * *

* This just mediates between different Jobs running on the same Computer - * by assigning free ports and its the jobs responsibility to open and close the ports. + * by assigning free ports and its the jobs responsibility to open and close the ports. * * @author Rama Pulavarthi */ @@ -49,11 +49,24 @@ public Environment setUp(AbstractBuild build, Launcher launcher, BuildListener l prefPortMap = prevAlloc.getPreviousAllocatedPorts(); } } - final PortAllocationManager pam = PortAllocationManager.getManager(cur); Map portMap = new HashMap(); final List allocated = new ArrayList(); for (PortType pt : ports) { + PortAllocationManager pam = PortAllocationManagerFactory.getManager(cur); + + if (pt instanceof PooledPortType) { + try { + Pool pool = PortAllocator.DESCRIPTOR.getPoolByName(pt.name); + + if (pool.isGlobal) { + pam = PortAllocationManagerFactory.CLUSTER_INSTANCE; + } + } catch (PoolNotDefinedException e) { + throw new RuntimeException("Undefined pool: " + pt.name); + } + } + logger.println("Allocating TCP port "+pt.name); int prefPort = prefPortMap.get(pt.name)== null?0:prefPortMap.get(pt.name); Port p = pt.allocate(build, pam, prefPort, launcher, listener); @@ -114,7 +127,7 @@ public String getHelpFile() { } public List getPortTypes() { - return PortTypeDescriptor.LIST; + return PortTypeDescriptor.LIST; } @Override diff --git a/src/main/resources/org/jvnet/hudson/plugins/port_allocator/PortAllocator/global.jelly b/src/main/resources/org/jvnet/hudson/plugins/port_allocator/PortAllocator/global.jelly index 2c4e71c..c9a8a61 100644 --- a/src/main/resources/org/jvnet/hudson/plugins/port_allocator/PortAllocator/global.jelly +++ b/src/main/resources/org/jvnet/hudson/plugins/port_allocator/PortAllocator/global.jelly @@ -16,6 +16,9 @@ + + + diff --git a/src/main/webapp/help-pool-definition-visibility.html b/src/main/webapp/help-pool-definition-visibility.html new file mode 100644 index 0000000..919b716 --- /dev/null +++ b/src/main/webapp/help-pool-definition-visibility.html @@ -0,0 +1,5 @@ +

+

+ The visibility of the pool indicating if the allocated port number should be global to the cluster. +

+
\ No newline at end of file diff --git a/src/test/java/org/jvnet/hudson/plugins/port_allocator/PortAllocationManagerTest.java b/src/test/java/org/jvnet/hudson/plugins/port_allocator/NodePortAllocationManagerTest.java similarity index 91% rename from src/test/java/org/jvnet/hudson/plugins/port_allocator/PortAllocationManagerTest.java rename to src/test/java/org/jvnet/hudson/plugins/port_allocator/NodePortAllocationManagerTest.java index 78fb431..bef9e30 100644 --- a/src/test/java/org/jvnet/hudson/plugins/port_allocator/PortAllocationManagerTest.java +++ b/src/test/java/org/jvnet/hudson/plugins/port_allocator/NodePortAllocationManagerTest.java @@ -1,19 +1,16 @@ package org.jvnet.hudson.plugins.port_allocator; -import java.io.IOException; - import hudson.model.AbstractBuild; import hudson.model.Computer; import hudson.remoting.Callable; import hudson.remoting.VirtualChannel; - -import org.jvnet.hudson.plugins.port_allocator.PortAllocationManager; -import org.jvnet.hudson.plugins.port_allocator.PortAllocationManager.PortUnavailableException; +import junit.framework.TestCase; +import org.jvnet.hudson.plugins.port_allocator.NodePortAllocationManager.PortUnavailableException; import org.mockito.Mockito; -import junit.framework.TestCase; +import java.io.IOException; -public class PortAllocationManagerTest extends TestCase { +public class NodePortAllocationManagerTest extends TestCase { private class TestMonitor { public boolean ready; @@ -29,7 +26,7 @@ private class TestMonitor { public void testAllocate() throws Exception { final Computer computer = Mockito.mock(Computer.class); final AbstractBuild build = Mockito.mock(AbstractBuild.class); - final PortAllocationManager manager = PortAllocationManager.getManager(computer); + final NodePortAllocationManager manager = PortAllocationManagerFactory.getManager(computer); final TestMonitor monitor = new TestMonitor(); @@ -121,7 +118,7 @@ public void testAllocateRandom() throws Throwable { Mockito.when(computer.getChannel()).thenReturn(channel); Mockito.when(channel.call(Mockito.isNotNull(Callable.class))).thenReturn(mockPort); - final PortAllocationManager manager = PortAllocationManager.getManager(computer); + final NodePortAllocationManager manager = PortAllocationManagerFactory.getManager(computer); int port = manager.allocateRandom(build, mockPort); assertEquals(mockPort, port); @@ -157,7 +154,7 @@ public void testAllocatePortRange() throws Throwable { .thenReturn(mockPort + 2) .thenReturn(mockPort + 3); - final PortAllocationManager manager = PortAllocationManager.getManager(computer); + final NodePortAllocationManager manager = PortAllocationManagerFactory.getManager(computer); int[] ports = manager.allocatePortRange(build, mockStart, mockEnd, 2, true); assertNotNull(ports);