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

net.luminis.quic.send.SendRequestQueue Maven / Gradle / Ivy

/*
 * Copyright © 2020, 2021, 2022, 2023, 2024 Peter Doornbosch
 *
 * This file is part of Kwik, an implementation of the QUIC protocol in Java.
 *
 * Kwik is free software: you can redistribute it and/or modify it under
 * the terms of the GNU Lesser General Public License as published by the
 * Free Software Foundation, either version 3 of the License, or (at your option)
 * any later version.
 *
 * Kwik is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for
 * more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program. If not, see .
 */
package net.luminis.quic.send;


import net.luminis.quic.frame.PathResponseFrame;
import net.luminis.quic.frame.PingFrame;
import net.luminis.quic.frame.QuicFrame;
import net.luminis.quic.common.EncryptionLevel;

import java.time.Clock;
import java.time.Duration;
import java.time.Instant;
import java.util.*;
import java.util.concurrent.ConcurrentLinkedDeque;
import java.util.function.Consumer;
import java.util.function.Function;

public class SendRequestQueue {

    public static final int MAX_QUEUED_PATH_RESPONSE_FRAMES = 256;
    private final Clock clock;
    private final EncryptionLevel encryptionLevel;
    private Deque requestQueue = new ConcurrentLinkedDeque<>();
    private Deque> probeQueue = new ConcurrentLinkedDeque<>();
    private final Object ackLock = new Object();
    private Instant nextAckTime;
    private volatile boolean cleared;

    public SendRequestQueue(EncryptionLevel level) {
        this(Clock.systemUTC(), level);
    }

    public SendRequestQueue(Clock clock, EncryptionLevel level) {
        this.clock = clock;
        encryptionLevel = level;
    }

    public void addRequest(QuicFrame fixedFrame, Consumer lostCallback) {
        if (fixedFrame instanceof PathResponseFrame) {
            // Defense against Path Challenge DOS attack: prevent number of path response frames that can be queued is unbound.
            long pathResponseFrameCount = requestQueue.stream()
                    .filter(request -> request instanceof FixedFrameSendRequest)
                    .filter(request -> ((FixedFrameSendRequest) request).getFrameType().equals(PathResponseFrame.class))
                    .count();
            if (pathResponseFrameCount >= MAX_QUEUED_PATH_RESPONSE_FRAMES) {
                return;
            }
        }

        requestQueue.addLast(new FixedFrameSendRequest(fixedFrame, lostCallback));
    }

    public void addAckRequest() {
        synchronized (ackLock) {
            nextAckTime = clock.instant();
        }
    }

    public void addAckRequest(int delay) {
        Instant requestedAckTime = clock.instant().plusMillis(delay);
        synchronized (ackLock) {
            if (nextAckTime == null || requestedAckTime.isBefore(nextAckTime)) {
                nextAckTime = requestedAckTime;
            }
        }
    }

    public void addProbeRequest() {
        probeQueue.addLast(Collections.emptyList());
    }

    public void addProbeRequest(List frames) {
        probeQueue.addLast(frames);
    }

    public boolean hasProbe() {
        return !probeQueue.isEmpty();
    }

    public boolean hasProbeWithData() {
        List firstProbe = probeQueue.peekFirst();
        return firstProbe != null && !firstProbe.isEmpty();
    }

    public List getProbe() {
        List probe = probeQueue.pollFirst();
        if (probe != null) {
            return probe;
        }
        else {
            // Even when client first checks for a probe, this might happen due to race condition with clear().
            // (and don't bother too much about the chance of an unnecessary probe)
            return List.of(new PingFrame());
        }
    }

    public boolean mustSendAck() {
        Instant now = clock.instant();
        synchronized (ackLock) {
            return nextAckTime != null && (now.isAfter(nextAckTime) || Duration.between(now, nextAckTime).toMillis() < 1);
        }
    }

    /**
     * Check on whether an ack must be sent and promise to do so: if this method returns true the flag that indicates (when)
     * an explicit ack must be sent will be reset, so if the caller does not actually send the ack, the notion it had to be
     * sent will be lost.
     * The reason for this uncommon behaviour is to avoid race conditions: when checking and changing status is not done
     * transactionally, a next change might be lost.
     * @return
     */
    public boolean mustAndWillSendAck() {
        Instant now = clock.instant();
        synchronized (ackLock) {
            boolean must = nextAckTime != null && (now.isAfter(nextAckTime) || Duration.between(now, nextAckTime).toMillis() < 1);
            if (must) {
                nextAckTime = null;
            }
            return must;
        }
    }

    public Instant getAck() {
        synchronized (ackLock) {
            try {
                return nextAckTime;
            } finally {
                nextAckTime = null;
            }
        }
    }

    public Instant nextDelayedSend() {
        synchronized (ackLock) {
            return nextAckTime;
        }
    }

    /**
     * @param frameSupplier
     * @param estimatedSize   The minimum size of the frame that the supplier can produce. When the supplier is
     *                        requested to produce a frame of that size, it must return a frame of the size or smaller.
     *                        This leaves room for the caller to handle uncertainty of how large the frame will be,
     *                        for example due to a var-length int value that may be larger at the moment the frame
     * @param lostCallback
     */
    public void addRequest(Function frameSupplier, int estimatedSize, Consumer lostCallback) {
        requestQueue.addLast(new FrameSupplierSendRequest(estimatedSize, frameSupplier, lostCallback));
    }

    public boolean hasRequests() {
        return !requestQueue.isEmpty();
    }
    
    public Optional next(int maxFrameLength) {
        if (maxFrameLength < 1) {  // Minimum frame size is 1: some frames (e.g. ping) are just a type field.
            // Forget it
            return Optional.empty();
        }

        try {
            for (Iterator iterator = requestQueue.iterator(); iterator.hasNext(); ) {
                SendRequest next = iterator.next();
                if (next.getEstimatedSize() <= maxFrameLength) {
                    iterator.remove();
                    return Optional.of(next);
                }
            }
            // Couldn't find one.
            return Optional.empty();
        }
        catch (ConcurrentModificationException concurrentModificationException) {
            if (cleared) {
                // Caused by concurrent clear, don't bother
                return Optional.empty();
            }
            else {
                throw concurrentModificationException;
            }
        }
    }

    public void clear() {
        clear(true);
    }

    public void clear(boolean dropAcks) {
        cleared = true;
        requestQueue.clear();
        probeQueue.clear();
        if (dropAcks) {
            synchronized (ackLock) {
                nextAckTime = null;
            }
        }
    }

    public boolean isEmpty() {
        return isEmpty(false);
    }

    public boolean isEmpty(boolean ignoreAcks) {
        if (ignoreAcks) {
            return requestQueue.isEmpty();
        }
        else {
            synchronized (ackLock) {
                return requestQueue.isEmpty() && nextAckTime == null;
            }
        }
    }

    @Override
    public String toString() {
        return "SendRequestQueue[" + encryptionLevel + "]";
    }

}





© 2015 - 2024 Weber Informatics LLC | Privacy Policy