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

org.elasticsearch.compute.operator.AsyncOperator Maven / Gradle / Ivy

/*
 * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
 * or more contributor license agreements. Licensed under the Elastic License
 * 2.0; you may not use this file except in compliance with the Elastic License
 * 2.0.
 */

package org.elasticsearch.compute.operator;

import org.elasticsearch.ExceptionsHelper;
import org.elasticsearch.TransportVersion;
import org.elasticsearch.TransportVersions;
import org.elasticsearch.action.ActionListener;
import org.elasticsearch.action.support.SubscribableListener;
import org.elasticsearch.common.Strings;
import org.elasticsearch.common.io.stream.NamedWriteableRegistry;
import org.elasticsearch.common.io.stream.StreamInput;
import org.elasticsearch.common.io.stream.StreamOutput;
import org.elasticsearch.common.util.concurrent.ConcurrentCollections;
import org.elasticsearch.compute.data.Page;
import org.elasticsearch.core.TimeValue;
import org.elasticsearch.index.seqno.LocalCheckpointTracker;
import org.elasticsearch.index.seqno.SequenceNumbers;
import org.elasticsearch.xcontent.XContentBuilder;

import java.io.IOException;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.atomic.LongAdder;

/**
 * {@link AsyncOperator} performs an external computation specified in {@link #performAsync(Page, ActionListener)}.
 * This operator acts as a client and operates on a per-page basis to reduce communication overhead.
 * @see #performAsync(Page, ActionListener)
 */
public abstract class AsyncOperator implements Operator {

    private volatile SubscribableListener blockedFuture;

    private final Map buffers = ConcurrentCollections.newConcurrentMap();
    private final FailureCollector failureCollector = new FailureCollector();
    private final DriverContext driverContext;

    private final int maxOutstandingRequests;
    private final LongAdder totalTimeInNanos = new LongAdder();
    private boolean finished = false;
    private volatile boolean closed = false;

    /*
     * The checkpoint tracker is used to maintain the order of emitted pages after passing through this async operator.
     * - Generates a new sequence number for each incoming page
     * - Uses the processed checkpoint for pages that have completed this computation
     * - Uses the persisted checkpoint for pages that have already been emitted to the next operator
     */
    private final LocalCheckpointTracker checkpoint = new LocalCheckpointTracker(
        SequenceNumbers.NO_OPS_PERFORMED,
        SequenceNumbers.NO_OPS_PERFORMED
    );

    /**
     * Create an operator that performs an external computation
     *
     * @param maxOutstandingRequests the maximum number of outstanding requests
     */
    public AsyncOperator(DriverContext driverContext, int maxOutstandingRequests) {
        this.driverContext = driverContext;
        this.maxOutstandingRequests = maxOutstandingRequests;
    }

    @Override
    public boolean needsInput() {
        final long outstandingPages = checkpoint.getMaxSeqNo() - checkpoint.getPersistedCheckpoint();
        return outstandingPages < maxOutstandingRequests;
    }

    @Override
    public void addInput(Page input) {
        if (failureCollector.hasFailure()) {
            input.releaseBlocks();
            return;
        }
        final long seqNo = checkpoint.generateSeqNo();
        driverContext.addAsyncAction();
        boolean success = false;
        try {
            final ActionListener listener = ActionListener.wrap(output -> {
                buffers.put(seqNo, output);
                onSeqNoCompleted(seqNo);
            }, e -> {
                releasePageOnAnyThread(input);
                failureCollector.unwrapAndCollect(e);
                onSeqNoCompleted(seqNo);
            });
            final long startNanos = System.nanoTime();
            performAsync(input, ActionListener.runAfter(listener, () -> {
                driverContext.removeAsyncAction();
                totalTimeInNanos.add(System.nanoTime() - startNanos);
            }));
            success = true;
        } finally {
            if (success == false) {
                driverContext.removeAsyncAction();
            }
        }
    }

    private void releasePageOnAnyThread(Page page) {
        page.allowPassingToDifferentDriver();
        page.releaseBlocks();
    }

    /**
     * Performs an external computation and notify the listener when the result is ready.
     *
     * @param inputPage the input page
     * @param listener  the listener
     */
    protected abstract void performAsync(Page inputPage, ActionListener listener);

    protected abstract void doClose();

    private void onSeqNoCompleted(long seqNo) {
        checkpoint.markSeqNoAsProcessed(seqNo);
        if (checkpoint.getPersistedCheckpoint() < checkpoint.getProcessedCheckpoint()) {
            notifyIfBlocked();
        }
        if (closed || failureCollector.hasFailure()) {
            discardPages();
        }
    }

    private void notifyIfBlocked() {
        if (blockedFuture != null) {
            final SubscribableListener future;
            synchronized (this) {
                future = blockedFuture;
                this.blockedFuture = null;
            }
            if (future != null) {
                future.onResponse(null);
            }
        }
    }

    private void checkFailure() {
        Exception e = failureCollector.getFailure();
        if (e != null) {
            discardPages();
            throw ExceptionsHelper.convertToRuntime(e);
        }
    }

    private void discardPages() {
        long nextCheckpoint;
        while ((nextCheckpoint = checkpoint.getPersistedCheckpoint() + 1) <= checkpoint.getProcessedCheckpoint()) {
            Page page = buffers.remove(nextCheckpoint);
            checkpoint.markSeqNoAsPersisted(nextCheckpoint);
            if (page != null) {
                releasePageOnAnyThread(page);
            }
        }
    }

    @Override
    public final void close() {
        finish();
        closed = true;
        discardPages();
        doClose();
    }

    @Override
    public void finish() {
        finished = true;
    }

    @Override
    public boolean isFinished() {
        if (finished && checkpoint.getPersistedCheckpoint() == checkpoint.getMaxSeqNo()) {
            checkFailure();
            return true;
        } else {
            return false;
        }
    }

    @Override
    public Page getOutput() {
        checkFailure();
        long persistedCheckpoint = checkpoint.getPersistedCheckpoint();
        if (persistedCheckpoint < checkpoint.getProcessedCheckpoint()) {
            persistedCheckpoint++;
            Page page = buffers.remove(persistedCheckpoint);
            checkpoint.markSeqNoAsPersisted(persistedCheckpoint);
            return page;
        } else {
            return null;
        }
    }

    @Override
    public SubscribableListener isBlocked() {
        // TODO: Add an exchange service between async operation instead?
        if (finished) {
            return Operator.NOT_BLOCKED;
        }
        long persistedCheckpoint = checkpoint.getPersistedCheckpoint();
        if (persistedCheckpoint == checkpoint.getMaxSeqNo() || persistedCheckpoint < checkpoint.getProcessedCheckpoint()) {
            return Operator.NOT_BLOCKED;
        }
        synchronized (this) {
            persistedCheckpoint = checkpoint.getPersistedCheckpoint();
            if (persistedCheckpoint == checkpoint.getMaxSeqNo() || persistedCheckpoint < checkpoint.getProcessedCheckpoint()) {
                return Operator.NOT_BLOCKED;
            }
            if (blockedFuture == null) {
                blockedFuture = new SubscribableListener<>();
            }
            return blockedFuture;
        }
    }

    @Override
    public final Operator.Status status() {
        return status(
            Math.max(0L, checkpoint.getMaxSeqNo()),
            Math.max(0L, checkpoint.getProcessedCheckpoint()),
            TimeValue.timeValueNanos(totalTimeInNanos.sum()).millis()
        );
    }

    protected Operator.Status status(long receivedPages, long completedPages, long totalTimeInMillis) {
        return new Status(receivedPages, completedPages, totalTimeInMillis);
    }

    public static class Status implements Operator.Status {
        public static final NamedWriteableRegistry.Entry ENTRY = new NamedWriteableRegistry.Entry(
            Operator.Status.class,
            "async_operator",
            Status::new
        );

        final long receivedPages;
        final long completedPages;
        final long totalTimeInMillis;

        protected Status(long receivedPages, long completedPages, long totalTimeInMillis) {
            this.receivedPages = receivedPages;
            this.completedPages = completedPages;
            this.totalTimeInMillis = totalTimeInMillis;
        }

        protected Status(StreamInput in) throws IOException {
            this.receivedPages = in.readVLong();
            this.completedPages = in.readVLong();
            this.totalTimeInMillis = in.readVLong();
        }

        @Override
        public void writeTo(StreamOutput out) throws IOException {
            out.writeVLong(receivedPages);
            out.writeVLong(completedPages);
            out.writeVLong(totalTimeInMillis);
        }

        public long receivedPages() {
            return receivedPages;
        }

        public long completedPages() {
            return completedPages;
        }

        public long totalTimeInMillis() {
            return totalTimeInMillis;
        }

        @Override
        public String getWriteableName() {
            return ENTRY.name;
        }

        @Override
        public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
            builder.startObject();
            innerToXContent(builder);
            return builder.endObject();
        }

        protected final XContentBuilder innerToXContent(XContentBuilder builder) throws IOException {
            builder.field("received_pages", receivedPages);
            builder.field("completed_pages", completedPages);
            builder.field("total_time_in_millis", totalTimeInMillis);
            if (totalTimeInMillis >= 0) {
                builder.field("total_time", TimeValue.timeValueMillis(totalTimeInMillis));
            }
            return builder;
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (o == null || getClass() != o.getClass()) return false;
            Status status = (Status) o;
            return receivedPages == status.receivedPages
                && completedPages == status.completedPages
                && totalTimeInMillis == status.totalTimeInMillis;
        }

        @Override
        public int hashCode() {
            return Objects.hash(receivedPages, completedPages, totalTimeInMillis);
        }

        @Override
        public String toString() {
            return Strings.toString(this);
        }

        @Override
        public TransportVersion getMinimalSupportedVersion() {
            return TransportVersions.ESQL_ENRICH_OPERATOR_STATUS;
        }
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy