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

org.nustaq.fastcast.impl.PacketSendBuffer Maven / Gradle / Ivy

/*
 * Copyright 2014 Ruediger Moeller.
 *
 * 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 org.nustaq.fastcast.impl;

import org.nustaq.fastcast.api.FCPublisher;
import org.nustaq.fastcast.config.PhysicalTransportConf;
import org.nustaq.fastcast.config.PublisherConf;
import org.nustaq.fastcast.transport.PhysicalTransport;
import org.nustaq.fastcast.util.FCLog;
import org.nustaq.offheap.bytez.ByteSource;
import org.nustaq.offheap.bytez.Bytez;
import org.nustaq.offheap.bytez.malloc.MallocBytez;
import org.nustaq.offheap.bytez.malloc.MallocBytezAllocator;
import org.nustaq.offheap.bytez.onheap.HeapBytez;
import org.nustaq.offheap.structs.FSTStruct;
import org.nustaq.offheap.structs.FSTStructAllocator;
import org.nustaq.offheap.structs.structtypes.StructArray;
import org.nustaq.serialization.util.FSTUtil;

import java.io.IOException;
import java.lang.reflect.Field;
import java.net.DatagramPacket;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.locks.LockSupport;

/**
 * Created with IntelliJ IDEA.
 * User: ruedi
 * Date: 8/10/13
 * Time: 11:42 PM
 * To change this template use File | Settings | File Templates.
 */

/**
 * packet buffer backed by binary struct array. Can be used as sender with ring buffered history.
 */
public class PacketSendBuffer implements FCPublisher {

    // if more than this packets are sent in bulk (large messages), start checking packet rate and stall
    public static long CHECK_PACKET_RATE_BULKSEND_THRESHOLD = 10;

    public static boolean RETRANSDEBUG = false;
    public static final String KEEP_SUBS_NODEID = "KEEPRECEIVER";

    public static boolean DEBUG_LAT = false;
    public static int RETRANS_MEM = 10000; // retransrequest history to accumulate identical retrans requests
    private static final int TAG_BUFF = 4;

    final PhysicalTransport trans;

    FSTStructAllocator offheapAllocator;
    FSTStructAllocator heapAllocator;
    ConcurrentLinkedQueue retransRequests = new ConcurrentLinkedQueue();


    String nodeId;
    int payMaxLen;
    int currentAvail; // number of bytes avaiable in current package
    int topic;

    FSTStruct currentPacketBytePointer; // points to currently written packet

    // as long nextSendMsg == currentSequence, packet is in.write and cannot be sent
    long currentSequence = 1; // putmsg sequence
    long nextSendMsg = 1;     // first sequence avaiable to send (must be < currentSequence)

    // send history ringbuffer
    StructArray history;
    int historySize;

    ControlPacket dropMsg; // prepared message for drop
    DataPacket template;   // template for new data packet

    ByteBuffer tmpSend;

    Topic topicEntry;
    boolean isUnordered;

    // configured rate limits
    int packetRateLimit;
    int packetRateLimitWindowDivisor;
    // computed =>
    int pps;
    int ppsWindowNanos;

    int packetCounter;
    long lastPpsRateCheckNanos;
    String currentReceiver = null;
    Bytez heartbeat;
    boolean batchOnLimit = true;

    public PacketSendBuffer(PhysicalTransport trans, String nodeId, Topic entry ) {
        this.trans = trans;
        this.topic = entry.getTopicId();
        topicEntry = entry;
        this.nodeId = nodeId;
        this.hbInvtervalMS = entry.getPublisherConf().getHeartbeatInterval();

        FCLog.log( "init send buffer for topic "+entry.getTopicId() );

        final PhysicalTransportConf conf = trans.getConf();
        template = DataPacket.getTemplate(conf.getDgramsize());
        payMaxLen = template.data.length;

        template.getSender().setString(nodeId);
        template.setTopic(topic);

        offheapAllocator = new FSTStructAllocator(0, new MallocBytezAllocator());
        heapAllocator = new FSTStructAllocator(0);

        final PublisherConf publisherConf = topicEntry.getPublisherConf();
        int hSize = publisherConf.getNumPacketHistory();
        if ( ((long)hSize*conf.getDgramsize()) > Integer.MAX_VALUE-2*conf.getDgramsize() ) {
            final int newHist = (Integer.MAX_VALUE - 2 * conf.getDgramsize()) / conf.getDgramsize();
            publisherConf.numPacketHistory(newHist);
            FCLog.get().warn("int overflow, degrading history size from "+hSize+" to "+newHist);
            hSize = newHist;
        }
        history = offheapAllocator.newArray(hSize, template);
        FCLog.log("allocating send buffer for topic " + topicEntry.getTopicId() + " of " + history.getByteSize() / 1024 / 1024 + " MByte");
        historySize = history.size();

        setUnordered(topicEntry.isUnordered());

        initDropMsgPacket(nodeId);

        DataPacket curP = getPacketAt(currentSequence);
        currentPacketBytePointer = curP.detach();
        curP.setSeqNo(currentSequence);
        curP.dataPointer(currentPacketBytePointer);
        currentAvail = payMaxLen-TAG_BUFF; // room for tags
        try {
            initTmpBBuf();
        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
        packetRateLimitWindowDivisor = publisherConf.getPpsWindow();
        setPacketRateLimit(publisherConf.getPps());
        heartbeat = new HeapBytez(new byte[]{ControlPacket.HEARTBEAT});
        flush(); // enforce immediate heartbeat
    }

    private void initTmpBBuf() throws NoSuchFieldException, IllegalAccessException {
        // patch MallocBytes such that it points to a DirectByteBuffer (allows zerocopy using fst bytez utils)
        tmpSend = ByteBuffer.allocateDirect(0);
        Field address = null;
        Field capacity = null;

        List fields = new ArrayList<>();
        FSTUtil.getAllFields(fields, tmpSend.getClass());
        for (int i = 0; i < fields.size(); i++) {
            Field field = fields.get(i);
            if ( field.getName().equals("address") ) {
                address = field;
            } else if ( field.getName().equals("capacity") ) {
                capacity = field;
            }
        }
        address.setAccessible(true);
        capacity.setAccessible(true);

        MallocBytez base = (MallocBytez) history.getBase();
        address.setLong(tmpSend, base.getBaseAdress() + history.getOffset());
        capacity.setInt(tmpSend, history.getByteSize());
    }

    private void moveBuff(DataPacket packet) {
        tmpSend.limit((int) (packet.getOffset() + packet.getDGramSize()));
        tmpSend.position((int) packet.getOffset());
    }

    protected void initDropMsgPacket(String nodeId) {
        dropMsg = new ControlPacket();
        dropMsg.getSender().setString(nodeId);
        dropMsg.setTopic(topic);
        dropMsg.setType(ControlPacket.DROPPED);
        dropMsg = heapAllocator.newStruct(dropMsg);
    }

    public void free() {
        offheapAllocator.free();
    }

    public Topic getTopicEntry() {
        return topicEntry;
    }

    public boolean isUnordered() {
        return isUnordered;
    }

    public void setUnordered(boolean unordered) {
        isUnordered = unordered;
    }

    private DataPacket getPacketAt(long seq) {
        return history.get(getIndexFromSequence(seq));
    }

    private int getIndexFromSequence(long seq) {
        return (int) (seq%historySize);
    }

    private boolean putMessage(int tag, ByteSource b, long offset, int len, boolean tryPut) {
        putMessageRecursive(tag, b, offset, len);
        return true;
    }

    private void putMessageRecursive(int tag, ByteSource b, long offset, int len) {
        while(true) {
            // payheader is type 2, len 2
            if ( currentAvail > len+DataPacket.HEADERLEN+2 )  // 2 byte needed for eop
            {
                // message fits into avaiable paylen
                putInternal(tag, DataPacket.COMPLETE, b, offset, len);
                return;
            } else {
                if ( isUnordered() ) {
                    if ( len > payMaxLen-DataPacket.HEADERLEN-2 ) {
                        throw new RuntimeException("unordered message size must not exceed packet size");
                    }
                    fire();
                    putMessageRecursive(tag, b,offset,len);
                    return;
                }
                // message must be chained, new packet required
                int sendlen = currentAvail - DataPacket.HEADERLEN - 8; //
                if ( sendlen <= 8 ) { // don't chain if only few bytes left
                    fire();
//                    if ( rec != null )
//                        getPacketAt(currentSequence.get()).getReceiver().setString(rec);
//                    putMessageRecursive(tag, b, offset, len, rec); stackoverflow
//                    return;
                } else
                {
                    // put chunk
                    putInternal(tag, DataPacket.CHAINED, b, offset, sendlen);
                    fire();
                    tag = -1; offset = offset+sendlen; len = len - sendlen;
                }
            }
        }
    }

    private void putInternal(int tag, short code, ByteSource b, long offset, int len) {
        int off = 0;
        if ( tag >= 0 ) {
            off = 1;
        }
        currentPacketBytePointer.setShort(code);
        currentPacketBytePointer.next(2);
        currentPacketBytePointer.setShort((short) (len + off));
        currentPacketBytePointer.next(2);
        if ( tag>=0 ) {
            currentPacketBytePointer.setByte((byte) tag);
            currentPacketBytePointer.next(off);
        }
        currentPacketBytePointer.setBytes(b, offset, len);
        currentPacketBytePointer.next(len);
        currentAvail= currentAvail-(len+off+DataPacket.HEADERLEN);
    }


    // finishes current packet, and allocs a new one so packet can be sent
    private void fire() {
        if (DEBUG_LAT)
            FCLog.get().debug("fire " + System.currentTimeMillis());
        if (isCurrentPacketEmpty()) // no message yet in packet
            return;
        currentPacketBytePointer.setShort(DataPacket.EOP);
        long curSeq = currentSequence;
        currentAvail-=2; // for EOP
        if ( currentAvail < 0 )
            throw new RuntimeException("negative bytes left "+currentAvail);
        getPacketAt(curSeq).setBytesLeft(currentAvail);

        long newSeq = curSeq + 1;
        DataPacket newPack = getPacketAt(newSeq);
        newPack.dataPointer(currentPacketBytePointer);
        newPack.setSeqNo(newSeq);
        currentAvail = payMaxLen-TAG_BUFF; // safe to always put a tag
        newPack.getReceiver().setString(currentReceiver);
        newPack.setRetrans(false);

        currentSequence++; // publish packet
    }

    /**
     * @return true if current packet does not contain any payload
     */
    private boolean isCurrentPacketEmpty() {
        return currentAvail == payMaxLen - TAG_BUFF;
    }

    /**
     * send pending packets to transport. Concurrent adding threads are assumed to not overtake.
     *
     * @throws IOException
     */
    private boolean sendPendingPackets() throws IOException {
        if ( currentSequence <= nextSendMsg ) {
            return false;
        }
        sendPackets(nextSendMsg, currentSequence, false, 0);
        return true;
    }

    long sentRetransSeq[] = new long[RETRANS_MEM];
    long sentRetransTimes[] = new long[RETRANS_MEM];
    private boolean sendPendingRetrans() throws IOException {
        // send retransmission
        if ( retransRequests.peek() != null ) {
            ArrayList curRetrans = new ArrayList<>();
            RetransPacket poll = null;
            do {
                poll = retransRequests.poll();
                curRetrans.add(poll);
            } while ( poll != null );
            mergeRetransmissions(curRetrans);
            return true;
        }
        return false;
    }

    private void putRetransSent(long sequence, long tim ) {
        int index = (int) (sequence % RETRANS_MEM);
        sentRetransSeq[index] = sequence;
        sentRetransTimes[index] = tim;
    }

    private long getLastRetransmitted(long sequence) {
        int index = (int) (sequence % RETRANS_MEM);
        if ( sentRetransSeq[index] == sequence )
            return sentRetransTimes[index];
        return 0;
    }

    private void mergeRetransmissions(ArrayList curRetrans) throws IOException {
        long now = System.currentTimeMillis();
        long maxTo = 0;
        for (int i = 0; i < curRetrans.size(); i++) {
            RetransPacket retransPacket = curRetrans.get(i);
            if ( retransPacket != null ) {
                maxTo = sendRetransmissionResponse(maxTo,retransPacket, now);
            }
        }
    }

    int maxRetransAge = 0;
    private long sendRetransmissionResponse(long maxTo, RetransPacket retransPacket, long now) throws IOException {
        for ( int ii = 0; ii < retransPacket.getRetransIndex(); ii++ ) {
            RetransEntry en = retransPacket.retransEntries(ii);
            long fromSeqNo = getPacketAt(en.getFrom()).getSeqNo();
            // note 'from' is oldest, so if from exists, all following exist also !
            if (fromSeqNo != en.getFrom() ) // not in on heap history ?
            {
                fromSeqNo = en.getFrom();
                if ( currentSequence-fromSeqNo > maxRetransAge ) {
                    maxRetransAge = (int) (currentSequence-fromSeqNo);
                    FCLog.get().warn("old retransmission from " + retransPacket.getSender() + " age " + maxRetransAge + " requested:" + fromSeqNo + " curseq " + currentSequence + " topic " + topicEntry.getTopicId());
                }
                dropMsg.setReceiver(retransPacket.getSender());
                dropMsg.setSeqNo(en.getFrom());
                FCLog.get().warn("Sending Drop " + dropMsg + " requestedSeq " + fromSeqNo + " on service " + getTopicEntry().getTopicId() + " currentSeq " + currentSequence + " age: " + (currentSequence - en.getFrom()));
                packetCounter++;
                trans.send(new DatagramPacket(dropMsg.getBase().toBytes((int) dropMsg.getOffset(), dropMsg.getByteSize()), 0,dropMsg.getByteSize()));
            } else {
                long from = en.getFrom();
                long to = en.getTo();
                if ( from >= maxTo || to > maxTo ) {
                    from = Math.max(from,maxTo);
//                    if ( RETRANSDEBUG ) {
//                        FCLog.get().net( System.currentTimeMillis()+" retransmitting " + en);
//                    }
                    maxTo = Math.max(to,maxTo);
                    sendPackets(from, to, true, now);
                }
            }
        }
        return maxTo;
    }

    int suppressedRetransCount = 0;
    ThreadLocal msgBytes = new ThreadLocal<>();
    private void sendPackets(long sendStart, long sendEnd, boolean retrans, long now /*only set for retrans !*/) throws IOException {
        final long len = sendEnd - sendStart;
        long nanosAtStart = 0;
        if ( ! retrans && len > CHECK_PACKET_RATE_BULKSEND_THRESHOLD ) {
            nanosAtStart = System.nanoTime();
        }
        for ( long i = sendStart; i < sendEnd; i++ ) {
            DataPacket dataPacket = getPacketAt(i);
            if ( retrans ) {
//                if ( now - getLastRetransmitted(i) < MaxRetransRepeatIntervalMS ) {
//                    suppressedRetransCount++;
//                    continue;
//                } else
                {
                    putRetransSent(i,now);
                }
            } else {
                if ( dataPacket.getSeqNo() != i )
                {
                    FCLog.get().fatal("FATAL error, current seq:"+currentSequence+" expected Seq:["+i+"] real read:"+dataPacket.getSeqNo());
                    FCLog.get().fatal("current put seq "+currentSequence);
                    FCLog.get().fatal("current send seq "+nextSendMsg);
                    FCLog.get().fatal("current pointer and currentpackpointer "+dataPacket.___offset+" "+currentPacketBytePointer.___offset);
                    FCLog.get().fatal(null,new Exception("stack trace"));
                    for (int ii = 0; ii < 20; ii++)
                        FCLog.get().fatal("  =>"+ getPacketAt(i + ii).getSeqNo());
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        FCLog.log(e);  //To change body of catch statement use File | Settings | File Templates.
                    }
                    System.exit(2);
                }
            }
            dataPacket.setRetrans(retrans);
            moveBuff(dataPacket);
//            if (!retrans)
            {
                packetCounter++;
//                System.out.println("send packet to '"+dataPacket.getReceiver()+"'");
            }
            trans.send(tmpSend);
            if ( len > CHECK_PACKET_RATE_BULKSEND_THRESHOLD && ! retrans ) { // for large messages
                long maxAllowedPackets = 2 * pps * ((System.nanoTime() - nanosAtStart) / ppsWindowNanos);
                while ( i-sendStart > maxAllowedPackets)
                {
                    sendPendingRetrans();
                    Thread.yield();
                    maxAllowedPackets = 2 * pps * ((System.nanoTime() - nanosAtStart) / ppsWindowNanos);
                }
            }
        }
        if ( ! retrans ) {
            nextSendMsg = sendEnd;
        }
    }

    void addRetransmissionRequest(RetransPacket retransPacket, PhysicalTransport trans) throws IOException {
        RetransPacket copy = (RetransPacket) retransPacket.createCopy();
        if ( RETRANSDEBUG )
            FCLog.get().info("received retrans request and add to Q " + copy);
        retransRequests.add(copy);
        flush();
    }

    AtomicBoolean sendLock = new AtomicBoolean(false);
    private void lock() {
        while( ! sendLock.compareAndSet(false,true) ) {
        }
    }

    private void unlock() {
        sendLock.set(false);
    }
    long hbInvtervalMS = 1000; // FIXME: use settings

    public volatile long lastMsgFlush = System.currentTimeMillis();

    protected boolean offerNoLock(String receiverNodeId, ByteSource msg, long start, int len, boolean doFlush) {
        long now = System.nanoTime();
        // if receiver changes, current packet must be fired
        if ( receiverNodeId != KEEP_SUBS_NODEID ) {
            setReceiver(receiverNodeId);
        }
        // verify rate is kept
        if ( now-lastPpsRateCheckNanos > ppsWindowNanos ) {
            packetCounter = Math.max(0,packetCounter-pps);
            lastPpsRateCheckNanos = now;
        }
        if (msg != null ) {
            // deny if sent packets > 2 * allowed packet per pps window
            if ( packetCounter > pps*2 )
                return false;
            else if (packetCounter > pps) { // else enforce batching
                if ( ! batchOnLimit )
                    return false;
                doFlush = false;
            }
        }
        boolean res;
        if ( msg == null ) {
            res = true;
        } else {
            res = putMessage(-1,msg,start,len, true);
        }
        if ( now-lastMsgFlush > hbInvtervalMS*1000*1000 ) {
            long prevFlush = lastMsgFlush;
            lastMsgFlush = now;
            if ( ! offerNoLock(null, heartbeat,0,1,false) ) {
                lastMsgFlush = prevFlush;
            }
            return res; // recursion already has triggered flsh in case
        }
        if ( doFlush ) {
            if ( ! isCurrentPacketEmpty() ) {
                lastMsgFlush = now;
                fire();
            }
        }
        try {
            if ( ! sendPendingRetrans() ) {
                if ( sendPendingPackets() ) {
                    lastMsgFlush = now;
                }
            }
//            else //
//                res = false;
        } catch (IOException e) {
            e.printStackTrace();
        }
        return res;
    }

    /////////////////////////////////////////////////////////////////////////////////////////////////
    //
    // publisher interface
    //

    HeapBytez hp = new HeapBytez(null,0,0);
    @Override
    public boolean offer(String receiverNodeId, byte[] b, int start, int len, boolean doFlush) {
        hp.setBase(b,start,len);
        return offer(receiverNodeId,hp,doFlush);
    }

    @Override
    public boolean offer(String receiverNodeId, ByteSource msg, long start, int len, boolean doFlush) {
        try {
            lock();
//            synchronized (this)
            {
                return offerNoLock(receiverNodeId, msg, start, len, doFlush);
            }
        } finally {
            unlock();
        }
    }

    @Override
    public boolean offer(String subscriberNodeId, ByteSource msg, boolean doFlush) {
        try {
            lock();
//            synchronized (this)
            {
                return offerNoLock(subscriberNodeId, msg, 0, (int) msg.length(), doFlush);
            }
        } finally {
            unlock();
        }
    }

    @Override
    public int getTopicId() {
        return topicEntry.getTopicId();
    }

    private void setReceiver(String receiverNodeId) {
        if ( receiverNodeId == null ) {
            if ( currentReceiver != null ) {
                currentReceiver = null;
                updateCurrentReceiver();
            } else {
                return; // nothing changed
            }
        } else if ( receiverNodeId.equals(currentReceiver) ) {
            return;
        } else {
            currentReceiver = receiverNodeId;
            updateCurrentReceiver();
        }
    }

    private void updateCurrentReceiver() {
        if ( isCurrentPacketEmpty() ) {
            getPacketAt(currentSequence).getReceiver().setString(currentReceiver);
        } else {
            fire();
            updateCurrentReceiver();
        }
    }

    @Override
    public void setPacketRateLimit(int limit) {
        packetRateLimit = limit;
        pps = packetRateLimit/packetRateLimitWindowDivisor;
        ppsWindowNanos = (1000*1000*1000)/packetRateLimitWindowDivisor;
    }

    @Override
    public int getPacketRateLimit() {
        return packetRateLimit;
    }

    @Override
    public FCPublisher batchOnLimit(boolean doBatch) {
        batchOnLimit = doBatch;
        return this;
    }

    @Override
    public boolean isBatchOnLimit(boolean doBatch) {
        return doBatch;
    }

    @Override
    public void flush() {
        offer( KEEP_SUBS_NODEID, (ByteSource)null, 0, 0, true );
    }

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy