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

io.trino.operator.exchange.LocalExchangeSource Maven / Gradle / Ivy

There is a newer version: 468
Show newest version
/*
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package io.trino.operator.exchange;

import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.SettableFuture;
import com.google.errorprone.annotations.ThreadSafe;
import com.google.errorprone.annotations.concurrent.GuardedBy;
import io.trino.operator.WorkProcessor;
import io.trino.operator.WorkProcessor.ProcessState;
import io.trino.spi.Page;
import jakarta.annotation.Nullable;

import java.util.ArrayDeque;
import java.util.Queue;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
import java.util.function.Consumer;

import static com.google.common.base.Preconditions.checkState;
import static com.google.common.util.concurrent.Futures.immediateVoidFuture;
import static java.util.Objects.requireNonNull;

@ThreadSafe
public class LocalExchangeSource
{
    private static final ListenableFuture NOT_BLOCKED = immediateVoidFuture();

    private final LocalExchangeMemoryManager memoryManager;
    private final Consumer onFinish;

    @GuardedBy("this")
    private final Queue buffer = new ArrayDeque<>();

    private final AtomicLong bufferedBytes = new AtomicLong();
    private final AtomicInteger bufferedPages = new AtomicInteger();

    @Nullable
    @GuardedBy("this")
    private SettableFuture notEmptyFuture; // null indicates no callback has been registered

    private volatile boolean finishing;

    public LocalExchangeSource(LocalExchangeMemoryManager memoryManager, Consumer onFinish)
    {
        this.memoryManager = requireNonNull(memoryManager, "memoryManager is null");
        this.onFinish = requireNonNull(onFinish, "onFinish is null");
    }

    public LocalExchangeBufferInfo getBufferInfo()
    {
        // This must be lock free to assure task info creation is fast
        // Note: the stats my be internally inconsistent
        return new LocalExchangeBufferInfo(bufferedBytes.get(), bufferedPages.get());
    }

    void addPage(Page page)
    {
        assertNotHoldsLock();

        boolean added = false;
        SettableFuture notEmptyFuture = null;
        long retainedSizeInBytes = page.getRetainedSizeInBytes();
        synchronized (this) {
            // ignore pages after finish
            if (!finishing) {
                // buffered bytes must be updated before adding to the buffer to assure
                // the count does not go negative
                bufferedBytes.addAndGet(retainedSizeInBytes);
                bufferedPages.incrementAndGet();
                buffer.add(page);
                added = true;
            }

            // we just added a page (or we are finishing) so we are not empty
            if (this.notEmptyFuture != null) {
                notEmptyFuture = this.notEmptyFuture;
                this.notEmptyFuture = null;
            }
        }

        if (!added) {
            memoryManager.updateMemoryUsage(-retainedSizeInBytes);
        }

        // notify readers outside of lock since this may result in a callback
        if (notEmptyFuture != null) {
            notEmptyFuture.set(null);
        }
    }

    public WorkProcessor pages()
    {
        return WorkProcessor.create(() -> {
            Page page = removePage();
            if (page == null) {
                if (isFinished()) {
                    return ProcessState.finished();
                }

                ListenableFuture blocked = waitForReading();
                if (!blocked.isDone()) {
                    return ProcessState.blocked(blocked);
                }

                return ProcessState.yielded();
            }

            return ProcessState.ofResult(page);
        });
    }

    public Page removePage()
    {
        assertNotHoldsLock();

        // NOTE: there is no need to acquire a lock here. The buffer is concurrent
        // and buffered bytes is not expected to be consistent with the buffer (only
        // best effort).
        Page page;
        synchronized (this) {
            page = buffer.poll();
        }
        if (page == null) {
            return null;
        }

        // dereference the page outside of lock, since may trigger a callback
        long retainedSizeInBytes = page.getRetainedSizeInBytes();
        memoryManager.updateMemoryUsage(-retainedSizeInBytes);
        bufferedBytes.addAndGet(-retainedSizeInBytes);
        bufferedPages.decrementAndGet();

        checkFinished();

        return page;
    }

    public ListenableFuture waitForReading()
    {
        assertNotHoldsLock();
        // Fast path, definitely not blocked
        if (finishing || bufferedPages.get() > 0) {
            return NOT_BLOCKED;
        }

        synchronized (this) {
            // re-check after synchronizing
            if (finishing || bufferedPages.get() > 0) {
                return NOT_BLOCKED;
            }
            // if we need to block readers, and the current future is complete, create a new one
            if (notEmptyFuture == null) {
                notEmptyFuture = SettableFuture.create();
            }
            return notEmptyFuture;
        }
    }

    public boolean isFinished()
    {
        // Common case fast-path without synchronizing
        if (!finishing) {
            return false;
        }
        synchronized (this) {
            // Synchronize to ensure effects of an in-flight close() or finish() are observed
            return finishing && bufferedPages.get() == 0;
        }
    }

    public void finish()
    {
        assertNotHoldsLock();

        SettableFuture notEmptyFuture;
        synchronized (this) {
            if (finishing) {
                return;
            }
            finishing = true;

            // Unblock any waiters
            notEmptyFuture = this.notEmptyFuture;
            this.notEmptyFuture = null;
        }

        // notify readers outside of lock since this may result in a callback
        if (notEmptyFuture != null) {
            notEmptyFuture.set(null);
        }

        checkFinished();
    }

    public void close()
    {
        assertNotHoldsLock();

        int remainingPagesCount = 0;
        long remainingPagesRetainedSizeInBytes = 0;
        SettableFuture notEmptyFuture;
        synchronized (this) {
            finishing = true;

            for (Page page : buffer) {
                remainingPagesCount++;
                remainingPagesRetainedSizeInBytes += page.getRetainedSizeInBytes();
            }
            buffer.clear();
            bufferedBytes.addAndGet(-remainingPagesRetainedSizeInBytes);
            bufferedPages.addAndGet(-remainingPagesCount);

            notEmptyFuture = this.notEmptyFuture;
            this.notEmptyFuture = null;
        }

        // free all the remaining pages
        memoryManager.updateMemoryUsage(-remainingPagesRetainedSizeInBytes);

        // notify readers outside of lock since this may result in a callback
        if (notEmptyFuture != null) {
            notEmptyFuture.set(null);
        }

        // this will always fire the finished event
        checkState(isFinished(), "Expected buffer to be finished");
        checkFinished();
    }

    private void checkFinished()
    {
        assertNotHoldsLock();

        if (isFinished()) {
            // notify finish listener outside of lock, since it may make a callback
            // NOTE: due the race in this method, the onFinish may be called multiple times
            // it is expected that the implementer handles this (which is why this source
            // is passed to the function)
            onFinish.accept(this);
        }
    }

    @SuppressWarnings("checkstyle:IllegalToken")
    private void assertNotHoldsLock()
    {
        assert !Thread.holdsLock(this) : "Cannot execute this method while holding the lock";
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy