// This file is provided under The MIT License as part of RiptideNetworking. // Copyright (c) Tom Weiland // For additional information please see the included LICENSE.md file or view it on GitHub: // https://github.com/RiptideNetworking/Riptide/blob/main/LICENSE.md using Riptide.Transports; using Riptide.Utils; using System; using System.Collections.Generic; namespace Riptide { /// Represents a currently pending reliably sent message whose delivery has not been acknowledged yet. internal class PendingMessage { /// The time of the latest send attempt. internal long LastSendTime { get; private set; } /// The multiplier used to determine how long to wait before resending a pending message. private const float RetryTimeMultiplier = 1.2f; /// A pool of reusable instances. private static readonly List pool = new List(); /// The to use to send (and resend) the pending message. private Connection connection; /// The contents of the message. private readonly byte[] data; /// The length in bytes of the message. private int size; /// How many send attempts have been made so far. private byte sendAttempts; /// Whether the pending message has been cleared or not. private bool wasCleared; /// Handles initial setup. internal PendingMessage() { data = new byte[Message.MaxSize]; } #region Pooling /// Retrieves a instance and initializes it. /// The sequence ID of the message. /// The message that is being sent reliably. /// The to use to send (and resend) the pending message. /// An intialized instance. internal static PendingMessage Create(ushort sequenceId, Message message, Connection connection) { PendingMessage pendingMessage = RetrieveFromPool(); pendingMessage.connection = connection; message.SetBits(sequenceId, sizeof(ushort) * Converter.BitsPerByte, Message.HeaderBits); pendingMessage.size = message.BytesInUse; Buffer.BlockCopy(message.Data, 0, pendingMessage.data, 0, pendingMessage.size); pendingMessage.sendAttempts = 0; pendingMessage.wasCleared = false; return pendingMessage; } /// Retrieves a instance from the pool. If none is available, a new instance is created. /// A instance. private static PendingMessage RetrieveFromPool() { PendingMessage message; if (pool.Count > 0) { message = pool[0]; pool.RemoveAt(0); } else message = new PendingMessage(); return message; } /// Empties the pool. Does not affect instances which are actively pending and therefore not in the pool. public static void ClearPool() { pool.Clear(); } /// Returns the instance to the pool so it can be reused. private void Release() { if (!pool.Contains(this)) pool.Add(this); // Only add it if it's not already in the list, otherwise this method being called twice in a row for whatever reason could cause *serious* issues // TODO: consider doing something to decrease pool capacity if there are far more // available instance than are needed, which could occur if a large burst of // messages has to be sent for some reason } #endregion /// Resends the message. internal void RetrySend() { if (!wasCleared) { long time = connection.Peer.CurrentTime; if (LastSendTime + (connection.SmoothRTT < 0 ? 25 : connection.SmoothRTT / 2) <= time) // Avoid triggering a resend if the latest resend was less than half a RTT ago TrySend(); else connection.Peer.ExecuteLater(connection.SmoothRTT < 0 ? 50 : (long)Math.Max(10, connection.SmoothRTT * RetryTimeMultiplier), new ResendEvent(this, time)); } } /// Attempts to send the message. internal void TrySend() { if (sendAttempts >= connection.MaxSendAttempts && connection.CanQualityDisconnect) { RiptideLogger.Log(LogType.Info, connection.Peer.LogName, $"Could not guarantee delivery of a {(MessageHeader)(data[0] & Message.HeaderBitmask)} message after {sendAttempts} attempts! Disconnecting..."); connection.Peer.Disconnect(connection, DisconnectReason.PoorConnection); return; } connection.Send(data, size); connection.Metrics.SentReliable(size); LastSendTime = connection.Peer.CurrentTime; sendAttempts++; connection.Peer.ExecuteLater(connection.SmoothRTT < 0 ? 50 : (long)Math.Max(10, connection.SmoothRTT * RetryTimeMultiplier), new ResendEvent(this, connection.Peer.CurrentTime)); } /// Clears the message. internal void Clear() { connection.Metrics.RollingReliableSends.Add(sendAttempts); wasCleared = true; Release(); } } }