Skip to content

[API Proposal]: QUIC stream priority support #90281

@alexrp

Description

@alexrp

QUIC Stream Priority

Background and motivation

  • RFC does not specify in what way prioritization should be exposed. It's not transmitted over the wire, it only serves internally to prioritize the order of sent data.
  • Every implementation exposes this in a different way. From byte to ushort, with smaller values meaning lower prio in most but also higher prio in some. Default values also differ, but in most cases it's the middle value from the available range.
  • MsQuic itself uses ushort with 0x7FFF as default, higher value means higher prio.

API Proposal

QuicStream settable property:

  • Makes QuicStream mutable.
  • Setter that will end up in P/Invoke, contrary to the expectation that setters are lightweight.
  • Thread-safety: in this particular case it's handled by MsQuic.
  • Lightweight solution, optional usage, doesn't add any overloads.
  • Similar issue dotnet/runtime#121910 will be probably solved the same way, i.e. by a settable property

Priority type and range:

  • To use a lowest common denominator, byte should be used.
  • To follow the behavior of majority, middle value 0x7F should be the default, 0x00 lowest priority, 0xFF highest.
  • It will need to be translated into MsQuic values, for example leaving it as the "higher" bit:
    • 0x00 = 0x00FF
    • 0x10 = 0x10FF
    • 0x7F = 0x7FFF
    • 0xFF = 0xFFFF
namespace System.Net.Quic;

public class QuicStream
{
    // Existing property
    public long Id { get; }

    // The setter implementation will call: SetParam(QUIC_PARAM_STREAM_PRIORITY, (ushort)((value << 8) | 0xFF));
    // The getter will return immediately (value backed by a private field).
+    public byte Priority { get; set; }
}

API Usage

QuicStream stream = await connection.OpenOutboundStreamAsync(QuicStreamType.Bidirectional);
Console.WriteLine($"Stream {stream} priority is {stream.Priority}");
byte originalPriority = stream.Priority;

stream.Priority = byte.MaxValue;  // highest priority
Console.WriteLine($"Stream {stream} priority is {stream.Priority}");
...
stream.Priority = 0;  // lowest priority
Console.WriteLine($"Stream {stream} priority is {stream.Priority}");
...
stream.Priority = originalPriority;  // back to the default

Alternative designs

Enum instead of byte

Optionally, an enum like ThreadPriority can be used, or just a set of constants exposed, or an enum backed by byte to define the lowest, default and highest priority

namespace System.Net.Quic;

+public enum QuicStreamPriority : byte
+{
+    Lowest = 0x00,
+    Default = 0x7F,
+    Highest = 0xFF
+}

public class QuicStream
{
    // Existing property
    public long Id { get; }

    // Note that you can set any byte value to the setter.
+    public QuicStreamPriority Priority { get; set; } = QuicStreamPriority.Default;
}

// Usage:
stream.Priority = QuicStreamPriority.Highest;
stream.Priority = 0xAA;
stream.Priority = 0x01;
stream.Priority = QuicStreamPriority.Lowest;
Console.WriteLine($"Stream {stream} priority is {stream.Priority}");

QuicStreamOptions

  • Follows the same pattern as QuicConnectionOptions.
  • Must be provided before creating the stream a parameter to:
  • The caller doesn't know yet what stream it is accepting, e.g. in H/3 wanting to change priority of the control stream, but the first accepted stream might be request stream.
  • Incurs an extra allocation.
  • Does not allow solving dotnet/runtime#121910, i.e. changing default error code after the stream was created and even read from.
  • Proposal from the original issue:
namespace System.Net.Quic;

+public sealed class QuicStreamOptions
+{
+    // Should probably be non-nullable with default value, see "Priority type and range" bellow.
+    public byte? Priority { get; set; }
+}

public sealed class QuicConnection
{
    // Existing
    public ValueTask<QuicStream> AcceptInboundStreamAsync(CancellationToken cancellationToken = default);
    // New
+    public ValueTask<QuicStream> AcceptInboundStreamAsync(QuicStreamOptions options, CancellationToken cancellationToken = default);
    // Existing
    public ValueTask<QuicStream> OpenOutboundStreamAsync(QuicStreamType type, CancellationToken cancellationToken = default);
    // New
+    public ValueTask<QuicStream> OpenOutboundStreamAsync(QuicStreamType type, QuicStreamOptions options, CancellationToken cancellationToken = default);
}

Original proposal:

Details

MsQuic has support for stream prioritization: https://github.com/microsoft/msquic/blob/main/docs/Settings.md#stream-parameters (QUIC_PARAM_STREAM_PRIORITY)

This is something I'd like to use in my application, so I wanted to first check if there are already existing plans to support this in System.Net.Quic? (I saw there's discussion about adding more options in #72984, but that seems to be specifically about connection-level options.)

If there are no existing plans for this, I can turn this issue into an actual API proposal.


API proposal after discussion

+public sealed class QuicStreamOptions
+{
+    public byte? Priority { get; set; }
+}

 public sealed class QuicConnection
 {
     public ValueTask<QuicStream> AcceptInboundStreamAsync(CancellationToken cancellationToken = default);
+    public ValueTask<QuicStream> AcceptInboundStreamAsync(QuicStreamOptions options, CancellationToken cancellationToken = default);
     public ValueTask<QuicStream> OpenOutboundStreamAsync(QuicStreamType type, CancellationToken cancellationToken = default);
+    public ValueTask<QuicStream> OpenOutboundStreamAsync(QuicStreamType type, QuicStreamOptions options, CancellationToken cancellationToken = default);
}

Considerations:

  • While MsQuic supports ushort priority values, quiche only supports byte, hence Priority being typed as the latter.
    • Alternatively, we could make it an int and just limit the range until such a time that we might want to allow a broader range of values.
    • Another approach would be to use a QuicStreamPriority enum, akin to ThreadPriority, but perhaps this is too constraining.
  • Should it be possible to change the stream priority after stream creation? I think both MsQuic and quiche support this.

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions