All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.eclipse.jetty.client.transport.HttpSender Maven / Gradle / Ivy

There is a newer version: 2.0.32
Show newest version
//
// ========================================================================
// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
//
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
// ========================================================================
//

package org.eclipse.jetty.client.transport;

import java.net.URI;
import java.nio.ByteBuffer;
import java.util.concurrent.Executor;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.atomic.AtomicReference;

import org.eclipse.jetty.client.Request;
import org.eclipse.jetty.client.Result;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.HttpHeaderValue;
import org.eclipse.jetty.io.Content;
import org.eclipse.jetty.util.BufferUtil;
import org.eclipse.jetty.util.Callback;
import org.eclipse.jetty.util.IteratingCallback;
import org.eclipse.jetty.util.Promise;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * 

HttpSender abstracts the algorithm to send HTTP requests, so that subclasses only * implement the transport-specific code to send requests over the wire, implementing * {@link #sendHeaders(HttpExchange, ByteBuffer, boolean, Callback)} and * {@link #sendContent(HttpExchange, ByteBuffer, boolean, Callback)}.

*

HttpSender governs the request state machines, which is updated as the various * steps of sending a request are executed, see {@code RequestState}. * At any point in time, a user thread may abort the request, which may (if the request * has not been completely sent yet) move the request state machine to {@code RequestState#FAILURE}. * The request state machine guarantees that the request steps are executed (by I/O threads) * only if the request has not been failed already.

* * @see HttpReceiver */ public abstract class HttpSender { private static final Logger LOG = LoggerFactory.getLogger(HttpSender.class); private final ContentSender contentSender = new ContentSender(); private final AtomicReference requestState = new AtomicReference<>(RequestState.QUEUED); private final AtomicReference failure = new AtomicReference<>(); private final HttpChannel channel; protected HttpSender(HttpChannel channel) { this.channel = channel; } protected HttpChannel getHttpChannel() { return channel; } protected HttpExchange getHttpExchange() { return channel.getHttpExchange(); } public boolean isFailed() { return requestState.get() == RequestState.FAILURE; } public void send(HttpExchange exchange) { if (!queuedToBegin(exchange)) return; if (!beginToHeaders(exchange)) return; contentSender.iterate(); } protected boolean expects100Continue(Request request) { return request.getHeaders().contains(HttpHeader.EXPECT, HttpHeaderValue.CONTINUE.asString()); } protected boolean queuedToBegin(HttpExchange exchange) { if (!updateRequestState(RequestState.QUEUED, RequestState.TRANSIENT)) return false; HttpRequest request = exchange.getRequest(); if (LOG.isDebugEnabled()) LOG.debug("Request begin {}", request); request.notifyBegin(); contentSender.exchange = exchange; contentSender.expect100 = expects100Continue(request); if (updateRequestState(RequestState.TRANSIENT, RequestState.BEGIN)) return true; abortRequest(exchange); return false; } protected boolean beginToHeaders(HttpExchange exchange) { if (!updateRequestState(RequestState.BEGIN, RequestState.TRANSIENT)) return false; HttpRequest request = exchange.getRequest(); if (LOG.isDebugEnabled()) LOG.debug("Request headers {}{}{}", request, System.lineSeparator(), request.getHeaders().toString().trim()); request.notifyHeaders(); if (updateRequestState(RequestState.TRANSIENT, RequestState.HEADERS)) return true; abortRequest(exchange); return false; } protected boolean headersToCommit(HttpExchange exchange) { if (!updateRequestState(RequestState.HEADERS, RequestState.TRANSIENT)) return false; HttpRequest request = exchange.getRequest(); if (LOG.isDebugEnabled()) LOG.debug("Request committed {}", request); request.notifyCommit(); if (updateRequestState(RequestState.TRANSIENT, RequestState.COMMIT)) return true; abortRequest(exchange); return false; } protected boolean someToContent(HttpExchange exchange, ByteBuffer content) { RequestState current = requestState.get(); return switch (current) { case COMMIT, CONTENT -> { if (!updateRequestState(current, RequestState.TRANSIENT)) yield false; HttpRequest request = exchange.getRequest(); if (LOG.isDebugEnabled()) LOG.debug("Request content {}{}{}", request, System.lineSeparator(), BufferUtil.toDetailString(content)); request.notifyContent(content); if (updateRequestState(RequestState.TRANSIENT, RequestState.CONTENT)) yield true; abortRequest(exchange); yield false; } default -> false; }; } protected boolean someToSuccess(HttpExchange exchange) { RequestState current = requestState.get(); return switch (current) { case COMMIT, CONTENT -> { // Mark atomically the request as completed, with respect // to concurrency between request success and request failure. if (!exchange.requestComplete(null)) yield false; requestState.set(RequestState.QUEUED); // Reset to be ready for another request. reset(); HttpRequest request = exchange.getRequest(); if (LOG.isDebugEnabled()) LOG.debug("Request success {}", request); request.notifySuccess(); // Mark atomically the request as terminated, with // respect to concurrency between request and response. Result result = exchange.terminateRequest(); terminateRequest(exchange, null, result); yield true; } default -> false; }; } private boolean failRequest(Throwable failure) { HttpExchange exchange = getHttpExchange(); if (exchange == null) return false; if (LOG.isDebugEnabled()) LOG.debug("Request failure {}, response {}", exchange.getRequest(), exchange.getResponse(), failure); // Mark atomically the request as completed, with respect // to concurrency between request success and request failure. return exchange.requestComplete(failure); } private void executeAbort(HttpExchange exchange, Throwable failure) { try { Executor executor = getHttpChannel().getHttpDestination().getHttpClient().getExecutor(); executor.execute(() -> abort(exchange, failure, Promise.noop())); } catch (RejectedExecutionException x) { if (LOG.isDebugEnabled()) LOG.debug("Exchange aborted {}", exchange, x); abort(exchange, failure, Promise.noop()); } } private void abortRequest(HttpExchange exchange) { Throwable failure = this.failure.get(); HttpRequest request = exchange.getRequest(); Content.Source content = request.getBody(); if (content != null) content.fail(failure); dispose(); if (LOG.isDebugEnabled()) LOG.debug("Request abort {} {} on {}", request, exchange, getHttpChannel(), failure); request.notifyFailure(failure); // Mark atomically the request as terminated, with // respect to concurrency between request and response. Result result = exchange.terminateRequest(); terminateRequest(exchange, failure, result); } private void terminateRequest(HttpExchange exchange, Throwable failure, Result result) { HttpRequest request = exchange.getRequest(); if (LOG.isDebugEnabled()) LOG.debug("Terminating request {}", request); if (result == null) { if (failure != null) { if (exchange.responseComplete(failure)) { if (LOG.isDebugEnabled()) LOG.debug("Response failure from request {} {}", request, exchange); getHttpChannel().abortResponse(exchange, failure, Promise.noop()); } } } else { result = channel.exchangeTerminating(exchange, result); HttpDestination destination = getHttpChannel().getHttpDestination(); boolean ordered = destination.getHttpClient().isStrictEventOrdering(); if (!ordered) channel.exchangeTerminated(exchange, result); if (LOG.isDebugEnabled()) LOG.debug("Request/Response {}: {}", failure == null ? "succeeded" : "failed", result); exchange.getConversation().getResponseListeners().notifyComplete(result); if (ordered) channel.exchangeTerminated(exchange, result); } } /** *

Implementations should send the HTTP headers over the wire, possibly with some content, * in a single write, and notify the given {@code callback} of the result of this operation.

*

If there is more content to send, then {@link #sendContent(HttpExchange, ByteBuffer, boolean, Callback)} * will be invoked.

* * @param exchange the exchange * @param contentBuffer the content to send * @param lastContent whether the content is the last content to send * @param callback the callback to notify */ protected abstract void sendHeaders(HttpExchange exchange, ByteBuffer contentBuffer, boolean lastContent, Callback callback); /** *

Implementations should send the given HTTP content over the wire.

* * @param exchange the exchange * @param contentBuffer the content to send * @param lastContent whether the content is the last content to send * @param callback the callback to notify */ protected abstract void sendContent(HttpExchange exchange, ByteBuffer contentBuffer, boolean lastContent, Callback callback); protected void reset() { contentSender.reset(); } protected void dispose() { } public void proceed(HttpExchange exchange, Runnable proceedAction, Throwable failure) { // Received a 100 Continue, although the Expect header was not sent. if (!contentSender.expect100) return; // Write the fields in this order, since the reader of // these fields will read them in the opposite order. contentSender.proceedAction = proceedAction; contentSender.expect100 = false; if (failure == null) { contentSender.iterate(); } else { if (failRequest(failure)) executeAbort(exchange, failure); } } public void abort(HttpExchange exchange, Throwable failure, Promise promise) { externalAbort(failure, promise); } private boolean anyToFailure(Throwable failure) { // Store only the first failure. this.failure.compareAndSet(null, failure); // Update the state to avoid more request processing. boolean abort; while (true) { RequestState current = requestState.get(); if (current == RequestState.FAILURE) { abort = false; break; } else { if (updateRequestState(current, RequestState.FAILURE)) { abort = current != RequestState.TRANSIENT; break; } } } return abort; } private void externalAbort(Throwable failure, Promise promise) { boolean abort = anyToFailure(failure); if (abort) { contentSender.abort = promise; contentSender.abort(this.failure.get()); } else { if (LOG.isDebugEnabled()) LOG.debug("Concurrent failure: request termination skipped, performed by helpers"); promise.succeeded(false); } } private void internalAbort(Throwable failure) { HttpExchange exchange = getHttpExchange(); if (exchange == null) return; anyToFailure(failure); abortRequest(exchange); } private boolean updateRequestState(RequestState from, RequestState to) { boolean updated = requestState.compareAndSet(from, to); if (!updated && LOG.isDebugEnabled()) LOG.debug("RequestState update failed: {} -> {}: {}", from, to, requestState.get()); return updated; } protected String relativize(String path) { try { String result = path; URI uri = URI.create(result); if (uri.isAbsolute()) result = uri.getPath(); return result.isEmpty() ? "/" : result; } catch (Throwable x) { if (LOG.isDebugEnabled()) LOG.debug("Could not relativize {}", path); return path; } } @Override public String toString() { return String.format("%s@%x(req=%s,failure=%s)", getClass().getSimpleName(), hashCode(), requestState, failure); } /** * The request states {@link HttpSender} goes through when sending a request. */ private enum RequestState { /** * One of the state transition methods is being executed. */ TRANSIENT, /** * The request is queued, the initial state */ QUEUED, /** * The request has been dequeued */ BEGIN, /** * The request headers (and possibly some content) is about to be sent */ HEADERS, /** * The request headers (and possibly some content) have been sent */ COMMIT, /** * The request content is being sent */ CONTENT, /** * The request is failed */ FAILURE } private class ContentSender extends IteratingCallback { // Fields that are set externally. private volatile HttpExchange exchange; private volatile Runnable proceedAction; private volatile boolean expect100; // Fields only used internally. private Content.Chunk chunk; private ByteBuffer contentBuffer; private boolean committed; private boolean success; private boolean complete; private Promise abort; private boolean demanded; @Override public boolean reset() { exchange = null; proceedAction = null; expect100 = false; chunk = null; contentBuffer = null; committed = false; success = false; complete = false; abort = null; demanded = false; return super.reset(); } @Override protected Action process() throws Throwable { HttpExchange exchange = this.exchange; if (complete) { if (success) someToSuccess(exchange); return Action.IDLE; } HttpRequest request = exchange.getRequest(); Content.Source content = request.getBody(); boolean expect100 = this.expect100; if (expect100) { // If the request was sent already, wait for // the 100 response before sending the content. if (committed) return Action.IDLE; // Do not send any content yet. chunk = null; } else { // Run the proceed action first, which likely will provide // content after having received the 100 Continue response. Runnable action = proceedAction; proceedAction = null; if (action != null) action.run(); // Read the request content. chunk = content != null ? content.read() : Content.Chunk.EOF; } if (LOG.isDebugEnabled()) LOG.debug("Content {} for {}", chunk, request); if (chunk == null) { if (committed) { // No content after the headers, demand. demanded = true; assert content != null; content.demand(this::succeeded); return Action.SCHEDULED; } else { // Normalize to avoid null checks. chunk = Content.Chunk.EMPTY; } } if (Content.Chunk.isFailure(chunk)) { Content.Chunk failure = chunk; chunk = Content.Chunk.next(failure); throw failure.getFailure(); } ByteBuffer buffer = chunk.getByteBuffer(); contentBuffer = buffer.asReadOnlyBuffer(); boolean last = chunk.isLast(); if (committed) sendContent(exchange, buffer, last, this); else sendHeaders(exchange, buffer, last, this); return Action.SCHEDULED; } @Override protected void onSuccess() { if (demanded) { // Content is now available, reset // the demand and iterate again. demanded = false; } else { boolean proceed = true; if (committed) { if (contentBuffer.hasRemaining()) proceed = someToContent(exchange, contentBuffer); } else { committed = true; proceed = headersToCommit(exchange); if (proceed) { // Was any content sent while committing? if (contentBuffer.hasRemaining()) proceed = someToContent(exchange, contentBuffer); } } boolean last = chunk.isLast(); chunk.release(); chunk = null; if (proceed) { if (last) { success = true; complete = true; } } else { // There was some concurrent error, terminate. complete = true; } } } @Override protected void onCompleteFailure(Throwable x) { if (chunk != null) { chunk.release(); chunk = Content.Chunk.next(chunk); } failRequest(x); internalAbort(x); Promise promise = abort; if (promise != null) promise.succeeded(true); } @Override public InvocationType getInvocationType() { return InvocationType.NON_BLOCKING; } } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy