Files
Flash5/flash/src/main/java/dev/relism/flash/http2/Http2StreamDispatcher.java
T

274 lines
9.2 KiB
Java

package dev.relism.flash.http2;
import dev.relism.flash.http.HttpMethod;
import dev.relism.flash.http.HttpStatus;
import dev.relism.flash.http2.frame.Http2FrameWriter;
import dev.relism.flash.http2.message.Http2ResponseWriter;
import dev.relism.flash.http2.stream.Http2FlowController;
import dev.relism.flash.http2.stream.Http2Stream;
import dev.relism.flash.http2.stream.Http2StreamState;
import dev.relism.flash.http2.stream.Http2StreamTable;
import dev.relism.flash.models.Request;
import dev.relism.flash.models.RequestHandler;
import dev.relism.flash.models.Response;
import dev.relism.flash.transport.ConnectionContext;
import java.io.IOException;
import java.util.concurrent.RejectedExecutionException;
import lombok.extern.slf4j.Slf4j;
/** Dispatches completed request streams without blocking the connection demultiplexer. */
@Slf4j
final class Http2StreamDispatcher implements Http2Stream.ResponseSink {
@FunctionalInterface
interface FailureSink {
void fail(int streamId, Http2ErrorCode errorCode) throws IOException;
}
private final ConnectionContext context;
private final Http2FrameWriter frameWriter;
private final Http2Settings peerSettings;
private final Http2StreamTable streams;
private final Http2FlowController flowController;
private final FailureSink failures;
private final Http2Stream[] resumeScratch =
new Http2Stream[Http2Limits.MAX_CONCURRENT_STREAMS];
private volatile boolean firstResponse = true;
Http2StreamDispatcher(
ConnectionContext context,
Http2FrameWriter frameWriter,
Http2Settings peerSettings,
Http2StreamTable streams,
Http2FlowController flowController,
FailureSink failures) {
this.context = context;
this.frameWriter = frameWriter;
this.peerSettings = peerSettings;
this.streams = streams;
this.flowController = flowController;
this.failures = failures;
}
void streamWindowUpdated(Http2Stream stream) {
scheduleResume(stream);
}
void connectionWindowUpdated() {
int count = streams.copyValues(resumeScratch);
for (int i = 0; i < count; i++) {
Http2Stream stream = resumeScratch[i];
resumeScratch[i] = null;
scheduleResume(stream);
}
}
private void scheduleResume(Http2Stream stream) {
if (!stream.responseStarted() || stream.cancelled()) return;
if (!stream.beginResponseBatch()) return;
stream.markResumeTask();
try {
context.executor().execute(stream);
} catch (RejectedExecutionException rejected) {
stream.endResponseBatch();
failAndRelease(stream, Http2ErrorCode.REFUSED_STREAM, rejected);
}
}
void dispatch(Http2Stream stream) {
if (stream.cancelled()) {
streams.release(stream);
return;
}
stream.markDispatched();
stream.responseSink(this);
try {
context.executor().execute(stream);
} catch (RejectedExecutionException rejected) {
failAndRelease(stream, Http2ErrorCode.REFUSED_STREAM, rejected);
}
}
@Override
public void handleRequest(Http2Stream stream) {
handle(stream);
}
private void handle(Http2Stream stream) {
stream.touch();
if (stream.cancelled()) {
streams.release(stream);
return;
}
try {
Request request = stream.assembleRequest(context.remoteAddress(), context.sslSocket());
Response pooled = stream.resetResponse();
Response response = pooled;
Object routeScratch = stream.routeScratch(context.router());
if (!Http2Authority.isServed(request.header("host"), request.sslSession())) {
response.status(HttpStatus.MISDIRECTED_REQUEST);
} else {
RequestHandler handler = context.router().route(request, routeScratch);
if (handler == null) handler = context.router().getNotFoundHandler();
try {
Object result = handler.handle(request, response);
if (result instanceof Response returned) response = returned;
else if (result != null) response.setBody(result);
} catch (Exception handlerFailure) {
Object result =
context.router().getExceptionHandler().handle(handlerFailure, request, response);
if (result instanceof Response returned) response = returned;
else if (result != null) response.setBody(result);
}
}
boolean pushStreaming = response.isPushStreaming();
if (!pushStreaming) request.drain();
Http2ResponseWriter responseWriter = stream.responseWriter();
if (stream.cancelled()) {
request.recycle();
if (response == pooled) pooled.recycle();
streams.release(stream);
return;
}
boolean headRequest = request.method() == HttpMethod.HEAD;
int reserved;
int used;
synchronized (this) {
boolean tableUpdate = firstResponse;
reserved = flowController.reserveSend(stream, peerSettings.maxFrameSize());
used = 0;
try {
used =
responseWriter.startFlowControlled(
response,
stream.id(),
headRequest,
context.configuration().isSendDate(),
true,
context.configuration().isH2HuffmanDynamicValues(),
tableUpdate,
peerSettings.maxFrameSize(),
peerSettings.maxHeaderListSize(),
reserved);
} finally {
flowController.refundSend(stream, reserved - used);
}
firstResponse = false;
}
if (!pushStreaming) request.recycle();
stream.markResponseStarted();
applyBatchTransition(stream, responseWriter);
if (!stream.beginResponseBatch()) {
throw new IllegalStateException("response batch already in flight");
}
frameWriter.write(responseWriter);
} catch (Exception failure) {
failAndRelease(stream, Http2ErrorCode.INTERNAL_ERROR, failure);
}
}
private void tryResumeResponse(Http2Stream stream) {
stream.touch();
if (stream.cancelled()) {
stream.endResponseBatch();
streams.release(stream);
return;
}
Http2ResponseWriter responseWriter = stream.responseWriter();
int streamId = stream.id();
if (responseWriter.finished()) {
stream.endResponseBatch();
if (stream.state() == Http2StreamState.CLOSED) {
streams.retire(stream, streamId);
}
return;
}
int reserved = flowController.reserveSend(stream, peerSettings.maxFrameSize());
if (reserved == 0) {
stream.endResponseBatch();
return;
}
try {
int used = 0;
try {
used = responseWriter.resume(peerSettings.maxFrameSize(), reserved);
} finally {
flowController.refundSend(stream, reserved - used);
}
applyBatchTransition(stream, responseWriter);
frameWriter.write(responseWriter);
} catch (Exception failure) {
stream.endResponseBatch();
failAndRelease(stream, Http2ErrorCode.INTERNAL_ERROR, failure);
}
}
@Override
public void resumeResponse(Http2Stream stream) {
tryResumeResponse(stream);
}
private static void applyBatchTransition(
Http2Stream stream, Http2ResponseWriter responseWriter) {
if (responseWriter.headersInBatch()) {
if (responseWriter.finished() && responseWriter.dataBytesInBatch() == 0
&& !responseWriter.trailerHeadersInBatch()) {
stream.transition(Http2StreamState.Event.SEND_HEADERS_ES);
return;
}
stream.transition(Http2StreamState.Event.SEND_HEADERS);
}
if (responseWriter.dataBytesInBatch() != 0 || responseWriter.endStreamInBatch()) {
if (responseWriter.dataBytesInBatch() != 0) {
stream.transition(
responseWriter.endStreamInBatch() && !responseWriter.trailerHeadersInBatch()
? Http2StreamState.Event.SEND_DATA_ES
: Http2StreamState.Event.SEND_DATA);
}
if (responseWriter.trailerHeadersInBatch()) {
stream.transition(Http2StreamState.Event.SEND_HEADERS_ES);
} else if (responseWriter.dataBytesInBatch() == 0 && responseWriter.endStreamInBatch()) {
stream.transition(Http2StreamState.Event.SEND_DATA_ES);
}
}
}
@Override
public void responseBatchCompleted(Http2Stream stream) {
stream.touch();
stream.endResponseBatch();
int streamId = stream.id();
if (streamId == 0) return;
if (stream.cancelled()) {
streams.remove(stream.id());
streams.release(stream);
return;
}
if (stream.responseWriter().finished()) {
if (stream.state() == Http2StreamState.CLOSED) {
streams.retire(stream, streamId);
}
return;
}
scheduleResume(stream);
}
private void failAndRelease(Http2Stream stream, Http2ErrorCode error, Exception cause) {
int streamId = stream.id();
if (!streams.removeIfSame(stream, streamId)) return;
if (cause != null) log.error("HTTP/2 stream {} failed", streamId, cause);
try {
stream.cancel();
} catch (RuntimeException cancellationFailure) {
log.debug("Failed to cancel HTTP/2 stream {} cleanly", streamId, cancellationFailure);
}
try {
failures.fail(streamId, error);
} catch (IOException writeFailure) {
log.debug("Failed to write RST_STREAM for {}", streamId, writeFailure);
} finally {
streams.release(stream);
}
}
}