// 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();
}
}
}