/* * The MIT License * * Copyright (c) 2010, InfraDNA, Inc. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package hudson.remoting; import java.io.IOException; import java.io.OutputStream; import java.util.logging.Logger; import static java.util.logging.Level.FINER; /** * Keeps track of the number of bytes that the sender can send without overwhelming the receiver of the pipe. * <p/> * <p/> * {@link OutputStream} is a blocking operation in Java, so when we send byte[] to the remote to write to * {@link OutputStream}, it needs to be done in a separate thread (or else we'll fail to attend to the channel * in timely fashion.) This in turn means the byte[] being sent needs to go to a queue between a * channel reader thread and I/O processing thread, and thus in turn means we need some kind of throttling * mechanism, or else the queue can grow too much. * <p/> * <p/> * This implementation solves the problem by using TCP/IP like window size tracking. The sender allocates * a fixed length window size. Every time the sender sends something we reduce this value. When the receiver * writes data to {@link OutputStream}, it'll send back the "ack" command, which adds to this value, allowing * the sender to send more data. * * @author Kohsuke Kawaguchi */ abstract class PipeWindow { protected Throwable dead; abstract void increase(int delta); abstract int peek(); /** * Blocks until some space becomes available. * * @return size. * @throws java.io.IOException If we learned that there is an irrecoverable problem on the remote side that prevents us from writing. * @throws InterruptedException If a thread was interrupted while blocking. */ abstract int get() throws InterruptedException, IOException; abstract void decrease(int delta); /** * Indicates that the remote end has died and all the further send attempt should fail. * @param cause {@link Throwable} cause. */ void dead(Throwable cause) { this.dead = cause; } /** * If we already know that the remote end had developed a problem, throw an exception. * Otherwise no-op. * @throws java.io.IOException exception. */ protected void checkDeath() throws IOException { if (dead!=null) throw (IOException)new IOException("Pipe is already closed").initCause(dead); } /** * Fake implementation used when the receiver side doesn't support throttling. */ static class Fake extends PipeWindow { void increase(int delta) { } int peek() { return Integer.MAX_VALUE; } int get() throws InterruptedException, IOException { checkDeath(); return Integer.MAX_VALUE; } void decrease(int delta) { } } static final class Key { public final int oid; Key(int oid) { this.oid = oid; } @Override public boolean equals(Object o) { if (o == null || getClass() != o.getClass()) { return false; } return oid == ((Key) o).oid; } @Override public int hashCode() { return oid; } } static class Real extends PipeWindow { private int available; private long written; private long acked; private final int oid; /** * The only strong reference to the key, which in turn * keeps this object accessible in {@link Channel#pipeWindows}. */ private final Key key; Real(Key key, int initialSize) { this.key = key; this.oid = key.oid; this.available = initialSize; } public synchronized void increase(int delta) { if (LOGGER.isLoggable(FINER)) { LOGGER.finer(String.format("increase(%d,%d)->%d", oid, delta, delta + available)); } available += delta; acked += delta; notifyAll(); } public synchronized int peek() { return available; } /** * Blocks until some space becomes available. * * <p> * If the window size is empty, induce some delay outside the synchronized block, * to avoid fragmenting the window size. That is, if a bunch of small ACKs come in a sequence, * bundle them up into a bigger size before making a call. */ public int get() throws InterruptedException, IOException { checkDeath(); synchronized (this) { if (available>0) return available; while (available<=0) { wait(); checkDeath(); } } Thread.sleep(10); synchronized (this) { return available; } } public synchronized void decrease(int delta) { if (LOGGER.isLoggable(FINER)) { LOGGER.finer(String.format("decrease(%d,%d)->%d", oid, delta, available - delta)); } available -= delta; written += delta; /* HUDSON-7745 says the following assertion fails, which AFAICT is only possible if multiple threads write to OutputStream concurrently, but that doesn't happen in most of the situations, so I'm puzzled. For the time being, cheating by just suppressing the assertion. HUDSON-7581 appears to be related. */ // if (available<0) // throw new AssertionError(); } } private static final Logger LOGGER = Logger.getLogger(PipeWindow.class.getName()); }