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

com.yahoo.vespa.http.server.ClientFeederV3 Maven / Gradle / Ivy

There is a newer version: 8.441.21
Show newest version
// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
package com.yahoo.vespa.http.server;

import com.yahoo.container.jdisc.HttpRequest;
import com.yahoo.container.jdisc.HttpResponse;
import com.yahoo.document.DocumentTypeManager;
import com.yahoo.jdisc.Metric;
import com.yahoo.jdisc.ReferencedResource;
import com.yahoo.jdisc.ResourceReference;
import com.yahoo.messagebus.Message;
import com.yahoo.messagebus.ReplyHandler;
import com.yahoo.messagebus.Result;
import com.yahoo.messagebus.shared.SharedSourceSession;
import com.yahoo.net.HostName;
import com.yahoo.vespaxmlparser.FeedOperation;
import com.yahoo.yolean.Exceptions;

import java.io.IOException;
import java.io.InputStream;
import java.time.Duration;
import java.time.Instant;
import java.util.Optional;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * An instance of this class handles all requests from one client using VespaHttpClient.
 *
 * The implementation is based on the code from V2, but the object model is rewritten to simplify the logic and
 * avoid using a threadpool that has no effect with all the extra that comes with it. V2 has one instance per thread
 * on the client, while this is one instance for all threads.
 *
 * @author Haakon Dybdahl
 */
class ClientFeederV3 {

    protected static final Logger log = Logger.getLogger(ClientFeederV3.class.getName());
    // This is for all clients on this gateway, for load balancing from client.
    private final static AtomicInteger outstandingOperations = new AtomicInteger(0);
    private final BlockingQueue feedReplies = new LinkedBlockingQueue<>();
    private final ReferencedResource sourceSession;
    private final String clientId;
    private final ReplyHandler feedReplyHandler;
    private final Metric metric;
    private Instant prevOpsPerSecTime = Instant.now();
    private double operationsForOpsPerSec = 0d;
    private final Object monitor = new Object();
    private final StreamReaderV3 streamReaderV3;
    private final AtomicInteger ongoingRequests = new AtomicInteger(0);
    private final String hostName;

    ClientFeederV3(ReferencedResource sourceSession,
                   FeedReaderFactory feedReaderFactory,
                   DocumentTypeManager docTypeManager,
                   String clientId,
                   Metric metric,
                   ReplyHandler feedReplyHandler) {
        this.sourceSession = sourceSession;
        this.clientId = clientId;
        this.feedReplyHandler = feedReplyHandler;
        this.metric = metric;
        this.streamReaderV3 = new StreamReaderV3(feedReaderFactory, docTypeManager);
        this.hostName = HostName.getLocalhost();
    }

    boolean timedOut() {
        synchronized (monitor) {
            return Instant.now().isAfter(prevOpsPerSecTime.plusSeconds(6000)) && ongoingRequests.get() == 0;
        }
    }

    void kill() {
        try (ResourceReference ignored = sourceSession.getReference()) {
            // No new requests should be sent to this object, but there can be old one, even though this is very unlikely.
            while (ongoingRequests.get() > 0) {
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    return;
                }
            }
        } catch (Exception e) {
            log.log(Level.WARNING, "Failed to close reference to source session", e);
        }
    }

    private void transferPreviousRepliesToResponse(BlockingQueue operations) throws InterruptedException {
        OperationStatus status = feedReplies.poll();
        while (status != null) {
            outstandingOperations.decrementAndGet();
            operations.put(status);
            status = feedReplies.poll();
        }
    }

    HttpResponse handleRequest(HttpRequest request) throws IOException {
        ongoingRequests.incrementAndGet();
        try {
            FeederSettings feederSettings = new FeederSettings(request);
            InputStream inputStream = StreamReaderV3.unzipStreamIfNeeded(request);
            BlockingQueue replies = new LinkedBlockingQueue<>();
            try {
                feed(feederSettings, inputStream, replies);
                synchronized (monitor) {
                    // Handshake requests do not have DATA_FORMAT, we do not want to give responses to
                    // handshakes as it won't be processed by the client.
                    if (request.getJDiscRequest().headers().get(Headers.DATA_FORMAT) != null) {
                        transferPreviousRepliesToResponse(replies);
                    }
                }
            } catch (InterruptedException e) {
                log.log(Level.FINE, e, () -> "Feed handler was interrupted: " + e.getMessage());
                // NOP, just terminate
            } catch (Throwable e) {
                log.log(Level.WARNING, "Unhandled exception while feeding: " + Exceptions.toMessageString(e), e);
            } finally {
                replies.add(createOperationStatus("-", "-", ErrorCode.END_OF_FEED, null));
            }
            return new FeedResponse(200, replies, 3, clientId, outstandingOperations.get(), hostName);
        } finally {
            ongoingRequests.decrementAndGet();
        }
    }

    private Optional pullMessageFromRequest(FeederSettings settings,
                                                                        InputStream requestInputStream,
                                                                        BlockingQueue repliesFromOldMessages) {
        while (true) {
            Optional operationId;
            try {
                operationId = streamReaderV3.getNextOperationId(requestInputStream);
                if (operationId.isEmpty()) return Optional.empty();
            } catch (IOException ioe) {
                log.log(Level.FINE, () -> Exceptions.toMessageString(ioe));
                return Optional.empty();
            }

            try {
                DocumentOperationMessageV3 message = getNextMessage(operationId.get(), requestInputStream, settings);
                if (message != null)
                    setRoute(message, settings);
                return Optional.ofNullable(message);
            } catch (Exception e) {
                log.log(Level.WARNING, () -> Exceptions.toMessageString(e));
                metric.add(MetricNames.PARSE_ERROR, 1, null);

                repliesFromOldMessages.add(new OperationStatus(Exceptions.toMessageString(e),
                                                               operationId.get(),
                                                               ErrorCode.ERROR,
                                                               false,
                                                               ""));
            }
        }
    }

    private Result sendMessage(DocumentOperationMessageV3 msg) throws InterruptedException {
        msg.getMessage().pushHandler(feedReplyHandler);
        return sourceSession.getResource().sendMessageBlocking(msg.getMessage());
    }

    private void feed(FeederSettings settings,
                      InputStream requestInputStream,
                      BlockingQueue repliesFromOldMessages) throws InterruptedException {
        while (true) {
            Optional message = pullMessageFromRequest(settings,
                                                                                  requestInputStream,
                                                                                  repliesFromOldMessages);

            if (message.isEmpty()) break;
            setMessageParameters(message.get(), settings);

            Result result;
            try {
                result = sendMessage(message.get());

            } catch  (RuntimeException e) {
                repliesFromOldMessages.add(createOperationStatus(message.get().getOperationId(),
                                                                 Exceptions.toMessageString(e),
                                                                 ErrorCode.ERROR,
                                                                 message.get().getMessage()));
                continue;
            }

            if (result.isAccepted()) {
                outstandingOperations.incrementAndGet();
                updateOpsPerSec();
                log(Level.FINE, "Sent message successfully, document id: ", message.get().getOperationId());
            } else {
                var err = result.getError();
                var msg = message.get();
                repliesFromOldMessages.add(
                        createOperationStatus(
                                msg.getOperationId(), err.getMessage(), ErrorCode.fromBusError(err), msg.getMessage()));
            }
        }
    }

    private OperationStatus createOperationStatus(String id, String message, ErrorCode code, Message msg) {
        String traceMessage = msg != null && msg.getTrace() != null &&  msg.getTrace().getLevel() > 0
                ? msg.getTrace().toString()
                : "";
        return new OperationStatus(message, id, code, false, traceMessage);
    }

    // protected for mocking
    /** Returns the next message in the stream, or null if none */
    protected DocumentOperationMessageV3 getNextMessage(String operationId,
                                                        InputStream requestInputStream,
                                                        FeederSettings settings) throws Exception {
        FeedOperation operation = streamReaderV3.getNextOperation(requestInputStream, settings);

        // This is a bit hard to set up while testing, so we accept that things are not perfect.
        if (sourceSession.getResource().session() != null) {
            metric.set(MetricNames.PENDING, (double) sourceSession.getResource().session().getPendingCount(), null);
        }

        DocumentOperationMessageV3 message = DocumentOperationMessageV3.create(operation, operationId, metric);
        if (message == null) {
            // typical end of feed
            return null;
        }
        metric.add(MetricNames.NUM_OPERATIONS, 1, null);
        log(Level.FINE, "Successfully deserialized document id: ", message.getOperationId());
        return message;
    }

    private void setMessageParameters(DocumentOperationMessageV3 msg, FeederSettings settings) {
        msg.getMessage().setContext(new ReplyContext(msg.getOperationId(), feedReplies));
        if (settings.traceLevel != null) {
            msg.getMessage().getTrace().setLevel(settings.traceLevel);
        }
    }

    private void setRoute(DocumentOperationMessageV3 msg, FeederSettings settings) {
        if (settings.route != null) {
            msg.getMessage().setRoute(settings.route);
        }
    }

    protected final void log(Level level, Object... msgParts) {
        if (!log.isLoggable(level)) return;

        StringBuilder s = new StringBuilder();
        for (Object part : msgParts)
            s.append(part.toString());
        log.log(level, s.toString());
    }

    private void updateOpsPerSec() {
        Instant now = Instant.now();
        synchronized (monitor) {
            if (now.plusSeconds(1).isAfter(prevOpsPerSecTime)) {
                Duration duration = Duration.between(now, prevOpsPerSecTime);
                double opsPerSec = operationsForOpsPerSec / (duration.toMillis() / 1000.);
                metric.set(MetricNames.OPERATIONS_PER_SEC, opsPerSec, null);
                operationsForOpsPerSec = 1.0d;
                prevOpsPerSecTime = now;
            } else {
                operationsForOpsPerSec += 1.0d;
            }
        }
    }

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy