hudson.remoting.PipeWindow Maven / Gradle / Ivy
Show all versions of hudson-remoting Show documentation
/*******************************************************************************
*
* Copyright (c) 2010, InfraDNA, Inc.
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
*
*
*
*
*******************************************************************************/
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.
*
*
* {@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.
*
*
* 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.
*
*
* 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());
}