package com.webpieces.http2engine.impl.shared;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import org.webpieces.data.api.DataWrapper;
import org.webpieces.data.api.DataWrapperGenerator;
import org.webpieces.data.api.DataWrapperGeneratorFactory;
import org.webpieces.util.logging.Logger;
import org.webpieces.util.logging.LoggerFactory;
import com.webpieces.http2engine.impl.DataTry;
import com.webpieces.http2parser.api.ConnectionException;
import com.webpieces.http2parser.api.ParseFailReason;
import com.webpieces.http2parser.api.dto.DataFrame;
import com.webpieces.http2parser.api.dto.WindowUpdateFrame;
import com.webpieces.http2parser.api.dto.lib.PartialStream;
public class Level6RemoteFlowControl {
private static final Logger log = LoggerFactory.getLogger(Level6RemoteFlowControl.class);
private static final DataWrapperGenerator dataGen = DataWrapperGeneratorFactory.createDataWrapperGenerator();
private HeaderSettings remoteSettings;
private Level7MarshalAndPing layer6NotifyListener;
private long remoteWindowSize;
private Object remoteLock = new Object();
private LinkedList<DataTry> dataQueue = new LinkedList<>();
private StreamState streamState;
public Level6RemoteFlowControl(
StreamState streamState,
Level7MarshalAndPing layer6NotifyListener,
HeaderSettings remoteSettings
) {
this.streamState = streamState;
this.layer6NotifyListener = layer6NotifyListener;
this.remoteSettings = remoteSettings;
remoteWindowSize = remoteSettings.getInitialWindowSize();
}
public CompletableFuture<Void> sendPayloadToSocket(Stream stream, PartialStream payload) {
log.info("sending payload to socket="+payload);
if(!(payload instanceof DataFrame))
return layer6NotifyListener.sendFrameToSocket(payload);
DataFrame f = (DataFrame) payload;
CompletableFuture<Void> future = new CompletableFuture<>();
DataTry data = new DataTry(stream, f, future, false);
trySendPayload(data);
return future;
}
private void trySendPayload(DataTry data) {
long length = data.getDataFrame().getTransmitFrameLength();
Stream stream = data.getStream();
boolean send;
long min = Math.min(remoteWindowSize, stream.getRemoteWindowSize());
long lengthToSend = Math.min(length, min);
if(length != lengthToSend) {
//must split DataFrame into two since WindowUpdateSize is not large enough
List<DataTry> tuple = splitDataFrame(data, lengthToSend);
data = tuple.get(0); //swap the right size to send
dataQueue.add(0, tuple.get(1));
}
if(lengthToSend > 0) {
stream.incrementRemoteWindow(-lengthToSend);
remoteWindowSize -= lengthToSend;
send = true;
} else if(data.isWasQueuedBefore()) {
dataQueue.add(0, data); //insert BACK at beginning of queue
send = false;
} else {
dataQueue.add(data); //insert at end of queue
send = false;
}
log.info("flow control. send="+send+" window="+remoteWindowSize+" streamWindow="+stream.getRemoteWindowSize());
if(send) {
DataTry finalTry = data;
layer6NotifyListener.sendFrameToSocket(data.getDataFrame())
.handle((v, t) -> processComplete(v, t, finalTry.getFuture()));
}
}
private List<DataTry> splitDataFrame(DataTry dataTry, long lengthToSend) {
if(lengthToSend > Integer.MAX_VALUE)
throw new IllegalStateException("bug, length to send should not be this large(per spec)="+lengthToSend);
int len = (int) lengthToSend;
DataFrame dataFrame = dataTry.getDataFrame();
DataWrapper data = dataFrame.getData();
if(dataFrame.getPadding().getReadableSize() > 0)
throw new UnsupportedOperationException("Splitting padding under these conditions would be"
+ " quite difficult so we skipped it. perhaps stop using padding or"
+ " modify this code but some padding would have to go in one"
+ " DataFrame and the rest in the next as the window size is not large enough");
List<? extends DataWrapper> split = dataGen.split(data, len);
DataFrame dF1 = new DataFrame();
dF1.setData(split.get(0));
DataFrame dF2 = new DataFrame();
dF2.setData(split.get(1));
List<DataTry> tuple = new ArrayList<>();
tuple.add(new DataTry(dataTry.getStream(), dF1, null, dataTry.isWasQueuedBefore()));
tuple.add(new DataTry(dataTry.getStream(), dF2, dataTry.getFuture(), dataTry.isWasQueuedBefore()));
return tuple;
}
private Object processComplete(Void v, Throwable t, CompletableFuture<Void> future) {
if(future == null)
return null; //nothing to do as this was only first piece of data
if(t != null) {
future.completeExceptionally(new RuntimeException(t));
} else
future.complete(null);
return null;
}
public void resetInitialWindowSize(long initialWindow) {
long difference = initialWindow - remoteSettings.getInitialWindowSize();
log.info("modify window size="+initialWindow);
remoteWindowSize += difference;
//next line MUST be set before updating all streams or some streams could be created
//just after updating all streams and before updating initial window size(ie. they are created with old size). instead
//make sure all new streams are using this initialWindow first
remoteSettings.setInitialWindowSize(initialWindow);
//now, update all streams that need updating
streamState.updateAllStreams(initialWindow);
}
//NOTE: this method virtually single threaded when used with channelmanager
//synchronized is to synchronize with other client threads
public CompletableFuture<Void> updateConnectionWindowSize(WindowUpdateFrame msg) {
int increment = msg.getWindowSizeIncrement();
if(increment == 0) {
throw new ConnectionException(ParseFailReason.WINDOW_SIZE_INVALID, msg.getStreamId(),
"Received windowUpdate size increment=0");
}
DataTry dataTry = null;
DataTry temp = dataQueue.peek();
synchronized(remoteLock) {
remoteWindowSize += increment;
if(remoteWindowSize > Integer.MAX_VALUE)
throw new ConnectionException(ParseFailReason.FLOW_CONTROL_ERROR, 0,
"(remote end bad)global remoteWindowSize too large="+remoteWindowSize+" from windows increment="+increment);
if(temp != null && remoteWindowSize > temp.getDataFrame().getTransmitFrameLength())
dataTry = dataQueue.poll();
log.info("updated window to="+remoteWindowSize+" increment="+msg.getWindowSizeIncrement()+" dataTry to submit="+dataTry);
}
if(dataTry != null) {
dataTry.setWasQueuedBefore(true);
trySendPayload(dataTry);
}
return CompletableFuture.completedFuture(null);
}
public CompletableFuture<Void> updateStreamWindowSize(Stream stream, WindowUpdateFrame msg) {
if(msg.getWindowSizeIncrement() == 0) {
throw new ConnectionException(ParseFailReason.WINDOW_SIZE_INVALID, msg.getStreamId(),
"Received windowUpdate size increment=0");
}
DataTry dataTry = null;
DataTry temp = dataQueue.peek();
synchronized(remoteLock) {
long remoteWindowSize = stream.incrementRemoteWindow(msg.getWindowSizeIncrement());
if(temp != null && remoteWindowSize > temp.getDataFrame().getTransmitFrameLength())
dataTry = dataQueue.poll();
log.info("updated stream "+stream.getStreamId()+" window to="
+stream.getRemoteWindowSize()+" increment="+msg.getWindowSizeIncrement()+" dataTry to submit="+dataTry);
}
if(dataTry != null) {
dataTry.setWasQueuedBefore(true);
trySendPayload(dataTry);
}
//someday, remove synchronized above and then complete future when it is complete instead maybe
return CompletableFuture.completedFuture(null);
}
public CompletableFuture<Void> goAway(ConnectionException e) {
return layer6NotifyListener.goAway(e);
}
}