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

org.yamcs.cfdp.CfdpOutgoingTransfer Maven / Gradle / Ivy

There is a newer version: 5.10.9
Show newest version
package org.yamcs.cfdp;

import static org.yamcs.cfdp.CfdpService.ETYPE_EOF_LIMIT_REACHED;
import static org.yamcs.cfdp.CfdpService.ETYPE_TRANSFER_FINISHED;
import static org.yamcs.cfdp.CfdpService.ETYPE_TRANSFER_META;
import static org.yamcs.cfdp.CfdpService.ETYPE_TRANSFER_RESUMED;
import static org.yamcs.cfdp.CfdpService.ETYPE_TRANSFER_SUSPENDED;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Queue;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

import org.yamcs.YConfiguration;
import org.yamcs.cfdp.pdu.AckPacket;
import org.yamcs.cfdp.pdu.AckPacket.FileDirectiveSubtypeCode;
import org.yamcs.cfdp.pdu.AckPacket.TransactionStatus;
import org.yamcs.cfdp.pdu.CfdpHeader;
import org.yamcs.cfdp.pdu.CfdpPacket;
import org.yamcs.cfdp.pdu.ConditionCode;
import org.yamcs.cfdp.pdu.EofPacket;
import org.yamcs.cfdp.pdu.FileDataPacket;
import org.yamcs.cfdp.pdu.FileDirectiveCode;
import org.yamcs.cfdp.pdu.FinishedPacket;
import org.yamcs.cfdp.pdu.KeepAlivePacket;
import org.yamcs.cfdp.pdu.MetadataPacket;
import org.yamcs.cfdp.pdu.NakPacket;
import org.yamcs.cfdp.pdu.SegmentRequest;
import org.yamcs.cfdp.pdu.TLV;
import org.yamcs.events.EventProducer;
import org.yamcs.filetransfer.TransferMonitor;
import org.yamcs.protobuf.TransferDirection;
import org.yamcs.protobuf.TransferState;
import org.yamcs.utils.StringConverter;
import org.yamcs.yarch.Bucket;
import org.yamcs.yarch.Stream;

public class CfdpOutgoingTransfer extends OngoingCfdpTransfer {

    private enum OutTxState {
        /**
         * Initial state. Going to SENDING_DATA in the first sendPdu step.
         */
        START,
        /**
         * Sending data and EOF. Going to FINISHED as soon as the Finished PDU is received.
         */
        SENDING_DATA,
        /**
         * Sending CANCEL EOF Going to COMPLETED as soon as the CANCEL EOF ACK is received
         */
        CANCELING,
        /**
         * End state. Still sending FINISHED ACK in return of Finished PDUs.
         */
        COMPLETED,
    }

    private final boolean withCrc = false; // no CRCs are used
    private final CfdpHeader directiveHeader, dataHeader;
    final Timer eofTimer;
    private final int entityIdLength;
    private final int seqNrSize;
    private Bucket bucket;
    private final int maxDataSize;
    private final int sleepBetweenPdus;
    private final boolean closureRequested;
    private final List sentFileDataPackets = new ArrayList<>();
    private Queue toResend;

    private OutTxState outTxState;
    private long transferred;

    private long offset = 0;
    private long end = 0;

    private boolean suspended = false;
    private final ChecksumType checksumType = ChecksumType.MODULAR;

    private PutRequest request;
    private ScheduledFuture pduSendingSchedule;
    FinishedPacket finishedPacket;

    boolean resendMetadata = false;
    boolean eofSent = false;
    boolean eofAckReceived = false;

    EofPacket eofPacket;
    MetadataPacket metadata;
    ConditionCode reasonForCancellation;

    /**
     * Create a new CFDP outgoing (uplink) transfer.
     * 

* The transfer has to be started with the {@link #start()} method. * * @param yamcsInstance * - yamcsInstance where this transfer is running. It is used for log configuration and to get the time * service. * @param initiatorEntityId * @param id * - unique identifier. The least significant number of bits (according to the CFDP sequence length) of * this id will be used to make the CFDP transaction id. * @param creationTime * - time when the transaction has been created * @param executor * - the CFDP state machine is serialized in this executor. It is also used to schedule timeouts. * @param request * - the request containing the file to be sent. * @param cfdpOut * - the stream where the outgoing PDUs are placed. * @param config * - the configuration of various settings (see yamcs manual) * @param bucket * - bucket from/to which the file should be * @param customPduSize * - if not null, size to overwrite the config maxPduSize * @param customPduDelay * - if not null, delay to overwrite the config sleepBetweenPdus * @param eventProducer * - used to send events when important things happen * @param monitor * - will be notified when transaction status changes * @param faultHandlerActions * - can be used to change behaviour in case of timeouts or failures. Can be null, in which case the * default behaviour to cancel the transaction will be used. */ public CfdpOutgoingTransfer(String yamcsInstance, long initiatorEntityId, long id, long creationTime, ScheduledThreadPoolExecutor executor, PutRequest request, Stream cfdpOut, YConfiguration config, Bucket bucket, Integer customPduSize, Integer customPduDelay, EventProducer eventProducer, TransferMonitor monitor, Map faultHandlerActions) { super(yamcsInstance, id, creationTime, executor, config, makeTransactionId(initiatorEntityId, config, id), request.getDestinationCfdpEntityId(), cfdpOut, eventProducer, monitor, faultHandlerActions); this.request = request; this.metadata = request.getMetadata(); transferType = getTransferType(metadata); this.bucket = bucket; entityIdLength = config.getInt("entityIdLength"); seqNrSize = config.getInt("sequenceNrLength"); int maxPduSize = customPduSize != null && customPduSize > 0 ? customPduSize : config.getInt("maxPduSize", 512); maxDataSize = maxPduSize - 4 - 2 * entityIdLength - seqNrSize - 4; long eofAckTimeout = config.getInt("eofAckTimeout", 10000); int eofAckLimit = config.getInt("eofAckLimit", 5); eofTimer = new Timer(executor, eofAckLimit, eofAckTimeout); acknowledged = request.isAcknowledged(); outTxState = OutTxState.START; this.sleepBetweenPdus = customPduDelay != null && customPduDelay > 0 ? customPduDelay : config.getInt("sleepBetweenPdus", 500); this.closureRequested = request.isClosureRequested(); if (request.getHeader() != null) { directiveHeader = request.getHeader().copy(true); dataHeader = request.getHeader().copy(false); } else { // create header for all file directive PDUs directiveHeader = new CfdpHeader( true, // it's a file directive false, // it's sent towards the receiver acknowledged, withCrc, // no CRC entityIdLength, seqNrSize, cfdpTransactionId.getInitiatorEntity(), // my Entity Id request.getDestinationCfdpEntityId(), // the id of the target cfdpTransactionId.getSequenceNumber()); dataHeader = new CfdpHeader( false, // it's file data false, // it's sent towards the receiver acknowledged, withCrc, // no CRC entityIdLength, seqNrSize, getTransactionId().getInitiatorEntity(), // my Entity Id request.getDestinationCfdpEntityId(), // the id of the target this.cfdpTransactionId.getSequenceNumber()); } } private static CfdpTransactionId makeTransactionId(long sourceId, YConfiguration config, long id) { int seqNrSize = config.getInt("sequenceNrLength"); long seqNum = id & ((1l << seqNrSize * 8) - 1); return new CfdpTransactionId(sourceId, seqNum); } /** * Start the transfer */ public void start() { pduSendingSchedule = executor.scheduleAtFixedRate(this::sendPDU, 0, sleepBetweenPdus, TimeUnit.MILLISECONDS); } private void sendPDU() { if (suspended) { return; } switch (outTxState) { case START: if (metadata == null) { metadata = getMetadataPacket(); transferType = getTransferType(metadata); } sendInfoEvent(ETYPE_TRANSFER_META, "Sending metadata: " + toEventMsg(metadata)); sendPacket(metadata); this.outTxState = OutTxState.SENDING_DATA; offset = 0; // first file data packet starts at the start of the data end = Math.min(maxDataSize, request.getFileLength()); monitor.stateChanged(this); break; case SENDING_DATA: if (resendMetadata) { sendPacket(metadata); resendMetadata = false; } else { if (offset == request.getFileLength()) { if (toResend != null && !toResend.isEmpty()) { FileDataPacket fdp = toResend.poll(); sendPacket(fdp); } else if (!eofSent) { sendEof(ConditionCode.NO_ERROR); } } else { end = Math.min(offset + maxDataSize, request.getFileLength()); FileDataPacket nextPacket = getNextFileDataPacket(); sentFileDataPackets.add(nextPacket); sendPacket(nextPacket); transferred += (end - offset); offset = end; } } monitor.stateChanged(this); break; case COMPLETED: pduSendingSchedule.cancel(true); cancelInactivityTimer(); break; default: throw new IllegalStateException("unknown/illegal state"); } } private void sendEof(ConditionCode code) { // remember the EOF sent, we need to resend it in case of suspend/resume eofPacket = getEofPacket(code); sendEof(); } private void sendEof() { sendPacket(eofPacket); eofSent = true; if (!acknowledged && !metadata.closureRequested()) { complete(ConditionCode.NO_ERROR); } else { eofTimer.start(() -> sendPacket(eofPacket), () -> { sendWarnEvent(ETYPE_EOF_LIMIT_REACHED, "Resend attempts (" + eofTimer.maxNumAttempts + ") of EOF reached"); handleFault(ConditionCode.ACK_LIMIT_REACHED); }); } } @Override public void processPacket(CfdpPacket packet) { executor.submit(() -> doProcessPacket(packet)); } private void doProcessPacket(CfdpPacket packet) { if (log.isDebugEnabled()) { log.debug("TXID{} state:{}, received PDU: {}", cfdpTransactionId, state, packet); log.trace("{}", StringConverter.arrayToHexString(packet.toByteArray(), true)); } if (state == TransferState.COMPLETED || state == TransferState.FAILED) { if (packet instanceof FinishedPacket) { sendPacket(getAckPacket(((FinishedPacket) packet).getConditionCode())); } else { log.info("Ignoring PDU {} for finished transaction {}", packet, cfdpTransactionId); } return; } if (eofAckReceived) { rescheduleInactivityTimer(); } if (packet instanceof AckPacket) { processAckPacket((AckPacket) packet); } else if (packet instanceof FinishedPacket) { processFinishedPacket((FinishedPacket) packet); } else if (packet instanceof NakPacket) { toResend = new LinkedList<>(); for (SegmentRequest segment : ((NakPacket) packet).getSegmentRequests()) { if (segment.isMetadata()) { resendMetadata = true; } else { toResend.addAll(sentFileDataPackets.stream() .filter(x -> segment.isInRange(x.getOffset())) .collect(Collectors.toList())); } } } else if (packet instanceof KeepAlivePacket) { log.info("TXID{} Ignoring Keep Alive PDU: {}", cfdpTransactionId, packet); // Handling not implemented } else { log.warn("TXID{} unexpected packet {} ", cfdpTransactionId, packet); } } private void processAckPacket(AckPacket ackPacket) { if (ackPacket.getDirectiveCode() != FileDirectiveCode.EOF) { log.info("TXID{} received bogus non EOF ACK packet: {}", cfdpTransactionId, ackPacket); } if (!eofSent) { log.info("TXID{} received unexpected ACK packet (EOF not sent): {}", cfdpTransactionId, ackPacket); return; } eofTimer.cancel(); if (outTxState == OutTxState.CANCELING) { complete(reasonForCancellation); } } private void processFinishedPacket(FinishedPacket finishedPacket) { eofTimer.cancel(); sendPacket(getAckPacket(finishedPacket.getConditionCode())); if (outTxState == OutTxState.COMPLETED) { return; } // depending on the conditioncode, the transfer was a success or a failure if (finishedPacket.getConditionCode() != ConditionCode.NO_ERROR) { complete(finishedPacket.getConditionCode()); } else { if (eofSent) { complete(ConditionCode.NO_ERROR); } else { log.warn("TXID{} received Finished PDU before sending the EOF: {}", cfdpTransactionId, finishedPacket); } } } /** * The inactivity timer is active after the EOF ACK has been received */ @Override protected void onInactivityTimerExpiration() { log.warn("TXID{} Inactivity timeout while in {} state; transaction failed", cfdpTransactionId, outTxState); handleFault(ConditionCode.INACTIVITY_DETECTED); } @Override protected void suspend() { if (outTxState == OutTxState.COMPLETED) { log.info("TXID{} transfer finished, suspend ignored", cfdpTransactionId); return; } sendInfoEvent(ETYPE_TRANSFER_SUSPENDED, "transfer suspended"); log.info("TXID{} suspending transfer", cfdpTransactionId); eofTimer.cancel(); pduSendingSchedule.cancel(true); cancelInactivityTimer(); suspended = true; changeState(TransferState.PAUSED); } @Override protected void resume() { if (!suspended) { log.info("TXID{} resume called while not suspended, ignoring", cfdpTransactionId); return; } if (outTxState == OutTxState.COMPLETED) { // it is possible the transfer has finished while being suspended log.info("TXID{} transfer finished, suspend ignored", cfdpTransactionId); return; } log.info("TXID{} resuming transfer", cfdpTransactionId); sendInfoEvent(ETYPE_TRANSFER_RESUMED, "transfer resumed"); pduSendingSchedule = executor.scheduleAtFixedRate(this::sendPDU, 0, sleepBetweenPdus, TimeUnit.MILLISECONDS); if (expectingAck()) { sendEof(); } if (outTxState == OutTxState.SENDING_DATA && eofAckReceived) { rescheduleInactivityTimer(); } changeState(TransferState.RUNNING); suspended = false; } private boolean expectingAck() { return (outTxState == OutTxState.SENDING_DATA || outTxState == OutTxState.CANCELING) && eofSent && !eofAckReceived; } public OutTxState getCfdpState() { return this.outTxState; } private void complete(ConditionCode conditionCode) { if (outTxState == OutTxState.COMPLETED) { return; } outTxState = OutTxState.COMPLETED; long duration = (System.currentTimeMillis() - wallclockStartTime) / 1000; String eventMessageSuffix; if (metadata.getFileLength() > 0 || directiveHeader.isLargeFile()) { eventMessageSuffix = request.getSourceFileName() + " -> " + request.getDestinationFileName(); } else { eventMessageSuffix = "Fileless transfer (metadata options: \n" + metadata.getOptions().stream().map(TLV::toString).collect(Collectors.joining(",\n")) + "\n)"; } if (conditionCode == ConditionCode.NO_ERROR) { changeState(TransferState.COMPLETED); sendInfoEvent(ETYPE_TRANSFER_FINISHED, "transfer finished successfully in " + duration + " seconds: " + eventMessageSuffix); } else { failTransfer(conditionCode.toString()); sendWarnEvent(ETYPE_TRANSFER_FINISHED, "transfer finished with error in " + duration + " seconds: " + eventMessageSuffix + ", error: " + finishedPacket.getConditionCode()); } } @Override protected void cancel(ConditionCode conditionCode) { log.debug("TXID{} Cancelling with code {}", cfdpTransactionId, conditionCode); switch (outTxState) { case START: case SENDING_DATA: reasonForCancellation = conditionCode; suspended = false; // wake up if sleeping outTxState = OutTxState.CANCELING; changeState(TransferState.CANCELLING); sendEof(conditionCode); break; case CANCELING: case COMPLETED: break; } } private void handleFault(ConditionCode conditionCode) { log.debug("TXID{} Handling fault {}", cfdpTransactionId, conditionCode); if (outTxState == OutTxState.CANCELING) { complete(conditionCode); } else { FaultHandlingAction action = getFaultHandlingAction(conditionCode); switch (action) { case ABANDON: complete(conditionCode); break; case CANCEL: cancel(conditionCode); break; case SUSPEND: suspend(); } } } @Override public TransferDirection getDirection() { return TransferDirection.UPLOAD; } @Override public long getTotalSize() { return this.request.getFileLength(); } @Override public String getBucketName() { return bucket != null ? bucket.getName() : null; } @Override public String getObjectName() { return request.getSourceFileName(); } @Override public String getRemotePath() { return request.getDestinationFileName(); } @Override public long getTransferredSize() { return this.transferred; } private EofPacket getEofPacket(ConditionCode code) { long checksum; long filesize; TLV tlv; if (code == ConditionCode.NO_ERROR) { checksum = request.getChecksum(); filesize = request.getFileLength(); tlv = null; } else { filesize = getTransferredSize(); checksum = ChecksumCalculator.calculateChecksum(request.getFileData(), 0l, filesize); tlv = TLV.getEntityIdTLV(cfdpTransactionId.getInitiatorEntity(), entityIdLength); } return new EofPacket(code, checksum, filesize, tlv, directiveHeader); } private MetadataPacket getMetadataPacket() { return new MetadataPacket( closureRequested, checksumType, request.getFileLength(), request.getSourceFileName(), request.getDestinationFileName(), null, directiveHeader); } private FileDataPacket getNextFileDataPacket() { return new FileDataPacket(Arrays.copyOfRange(request.getFileData(), (int) offset, (int) end), offset, dataHeader); } private AckPacket getAckPacket(ConditionCode code) { return new AckPacket( FileDirectiveCode.FINISHED, FileDirectiveSubtypeCode.FINISHED_BY_END_SYSTEM, code, TransactionStatus.TERMINATED, directiveHeader); } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy