Skip to content

Commit cb021c7

Browse files
committed
Remove Http3StreamType
1 parent 4512adb commit cb021c7

28 files changed

Lines changed: 472 additions & 321 deletions
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
# Protocol-Agnostic QUIC Transport Layer
2+
3+
## Goal
4+
5+
Remove all HTTP/3 protocol knowledge from the QUIC transport layer. Replace `QuicStreamKind` enum with opaque `long streamTypeValue` flowing through transport. The transport distinguishes only bidirectional (request) vs unidirectional (typed) streams. Protocol interpretation happens exclusively in `Http30ConnectionStage`.
6+
7+
## Core Concepts
8+
9+
- **Request streams**: bidirectional, identified by `streamTypeValue = -1` (sentinel)
10+
- **Typed streams**: unidirectional, identified by their wire byte value (opaque `long`)
11+
- **TypedStreamDescriptor**: configuration record passed to transport at construction — `(long StreamTypeValue, long SyntheticStreamId)`
12+
- Transport opens typed streams eagerly from configuration, without knowing what the values mean
13+
14+
## New Transport Type
15+
16+
```csharp
17+
internal readonly record struct TypedStreamDescriptor(long StreamTypeValue, long SyntheticStreamId);
18+
```
19+
20+
Http3 layer provides at construction:
21+
```csharp
22+
[new(0x00, -2), new(0x02, -3)] // Control, QpackEncoder — transport doesn't know names
23+
```
24+
25+
## Typed Stream State
26+
27+
Replaces the six hardcoded fields (`_controlHandle`, `_encoderHandle`, `_pendingControlItems`, `_pendingEncoderItems`, `_controlStreamId`, `_encoderStreamId`):
28+
29+
```csharp
30+
private sealed class TypedStreamState
31+
{
32+
public ConnectionHandle? Handle;
33+
public readonly Queue<NetworkBuffer> PendingItems = new();
34+
public long StreamId;
35+
}
36+
```
37+
38+
Stored in `Dictionary<long, TypedStreamState> _typedStreams` keyed by `streamTypeValue`.
39+
40+
## File-by-File Changes
41+
42+
### Delete
43+
44+
- `QuicStreamKind.cs` — enum and `QuicStreamKindMapper` removed entirely
45+
46+
### QuicConnectionHandle
47+
48+
- `OpenStreamAsLeaseAsync(bool bidirectional)` — no stream type knowledge, just direction
49+
- `InboundStream(ConnectionLease, long StreamTypeValue, long StreamId)` — raw wire value
50+
- `AcceptInboundStreamAsLeaseAsync` — reads wire byte, returns as `long`, no interpretation, accepts all streams
51+
- Remove `MapStreamKind` — replace with `bidirectional ? Bidirectional+GetStream : WriteOnly+GetUnidirectional`
52+
53+
### IQuicTransportEvent
54+
55+
- `TypedLeaseAcquired(ConnectionLease, long StreamTypeValue, long StreamId)`
56+
- `InboundStreamReady` carries `InboundStream` which now has `long StreamTypeValue`
57+
58+
### QuicPumpManager
59+
60+
- `StartInboundPump(handle, long streamTypeValue, key, gen, streamId)`
61+
- `PumpAsync`: sets `h3Buf.StreamTypeValue = streamTypeValue` instead of `ApplyToBuffer`
62+
- Close signal: only for request streams (`streamTypeValue < 0`)
63+
64+
### QuicStreamRouter
65+
66+
- `RouteTaggedItem(buffer, long streamTypeValue, Dictionary<long, TypedStreamState> typedStreams)` — looks up by value, falls through to request routing for unknown/request type
67+
- Remove `QuicStreamKind` from all method signatures
68+
69+
### QuicTransportStateMachine
70+
71+
- Constructor receives `TypedStreamDescriptor[]`, initializes `_typedStreams` dictionary
72+
- Remove constants `ControlStreamSyntheticId`, `QpackEncoderStreamSyntheticId`, `QpackDecoderStreamSyntheticId`
73+
- `OnRequestLeaseAcquired` — iterates descriptors to open typed streams
74+
- `OnTypedLeaseAcquired(lease, long streamTypeValue, long streamId)` — looks up in `_typedStreams`
75+
- `OnInboundStreamReady` — maps `streamTypeValue` to synthetic ID via descriptors (or real stream ID for unconfigured types)
76+
- `HandlePush` — reads `StreamTypeValue` from buffer for routing instead of `Http3StreamType`
77+
78+
### Http3NetworkBuffer (Internal/Messages.cs)
79+
80+
- Add `public long StreamTypeValue { get; set; } = -1;`
81+
- `Http3StreamType StreamType` stays as plain settable property (no auto-conversion)
82+
- Transport only touches `StreamTypeValue`; protocol layer uses both
83+
84+
### Http30ConnectionStage (Protocol Layer)
85+
86+
- **Inbound**: maps `StreamTypeValue` to `Http3StreamType` in `HandleTaggedStreamData`
87+
- **Outbound**: sets `StreamTypeValue` on buffers (0x00 for Control, 0x02 for Encoder, 0x03 for Decoder)
88+
- This is the single place where wire values get protocol meaning
89+
90+
### QpackStreamHandler
91+
92+
- Sets `StreamTypeValue` on outbound buffers (instead of / alongside `StreamType`)
93+
94+
## What Stays the Same
95+
96+
- `Http3StreamType` enum stays (protocol-internal concern)
97+
- `Http3NetworkBuffer.StreamType` stays (used by protocol layer)
98+
- Synthetic stream IDs stay (configured instead of hardcoded)
99+
- All buffering/flush logic stays (same patterns, keyed by `long` instead of enum)
100+
101+
## Test Impact
102+
103+
- Transport specs (`QuicPumpManagerSpec`, `QuicStreamRouterSpec`, `QuicStreamRouterEnhancedSpec`, `QuicTransportStateMachineSpec`, `QuicTransportStateMachineLifecycleSpec`, `QuicConnectionHandleSpec`, `QuicConnectionManagerSpec`) — update to use `long` values instead of `QuicStreamKind`
104+
- Protocol specs using `Http3StreamType` — unchanged

src/Servus.Akka/Servus.Akka.csproj

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<TargetFramework>net10.0</TargetFramework>
5+
<ImplicitUsings>enable</ImplicitUsings>
6+
<Nullable>enable</Nullable>
7+
</PropertyGroup>
8+
9+
<ItemGroup>
10+
<Folder Include="IO\QUIC\" />
11+
<Folder Include="IO\TCP\" />
12+
</ItemGroup>
13+
14+
</Project>

src/TurboHTTP.StreamTests/Http3/Http30ConnectionConcurrencySpec.cs

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,7 @@ private IEnumerable<IInputItem> BuildResponseSequence(params long[] streamIds)
3030
var buf = Http3NetworkBuffer.Rent(headersBytes.Length);
3131
headersBytes.AsSpan().CopyTo(buf.FullMemory.Span);
3232
buf.Length = headersBytes.Length;
33-
buf.StreamType = Http3StreamType.Request;
3433
buf.StreamId = streamId;
35-
3634
yield return buf;
3735
yield return new QuicCloseItem(QuicCloseKind.RequestStreamComplete, streamId);
3836
}
@@ -44,7 +42,7 @@ private static Http3NetworkBuffer BuildControlSettings()
4442
var buf = Http3NetworkBuffer.Rent(settingsBytes.Length);
4543
settingsBytes.AsSpan().CopyTo(buf.FullMemory.Span);
4644
buf.Length = settingsBytes.Length;
47-
buf.StreamType = Http3StreamType.Control;
45+
buf.StreamTypeValue = (long)StreamType.Control;
4846
return buf;
4947
}
5048

@@ -93,10 +91,10 @@ private static List<long> ExtractRequestStreamIds(IReadOnlyList<IOutputItem> ite
9391
var result = new List<long>();
9492
foreach (var item in items)
9593
{
96-
if (item is Http3NetworkBuffer { StreamType: Http3StreamType.Request, StreamId: >= 0 } tagged
97-
&& seen.Add(tagged.StreamId))
94+
if (item is Http3NetworkBuffer { StreamTypeValue: null, StreamId: not null } tagged
95+
&& seen.Add(tagged.StreamId.Value))
9896
{
99-
result.Add(tagged.StreamId);
97+
result.Add(tagged.StreamId.Value);
10098
}
10199
}
102100

src/TurboHTTP.StreamTests/Transport/QuicPumpManagerSpec.cs

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ public void StartInboundPump_should_not_throw()
3131
var handle = CreateTestHandle();
3232

3333
// Should complete without throwing
34-
pumpMgr.StartInboundPump(handle, Http3StreamType.Request, TestEndpoint, connectionGen: 0, streamId: 1);
34+
pumpMgr.StartInboundPump(handle, -1, TestEndpoint, connectionGen: 0, streamId: 1);
3535

3636
pumpMgr.StopAll();
3737
}
@@ -53,8 +53,8 @@ public void StopAll_should_cancel_pumps()
5353
var handle1 = CreateTestHandle();
5454
var handle2 = CreateTestHandle();
5555

56-
pumpMgr.StartInboundPump(handle1, Http3StreamType.Request, TestEndpoint, connectionGen: 0, streamId: 1);
57-
pumpMgr.StartInboundPump(handle2, Http3StreamType.Request, TestEndpoint, connectionGen: 0, streamId: 2);
56+
pumpMgr.StartInboundPump(handle1, -1, TestEndpoint, connectionGen: 0, streamId: 1);
57+
pumpMgr.StartInboundPump(handle2, -1, TestEndpoint, connectionGen: 0, streamId: 2);
5858

5959
// Stop all should complete without throwing
6060
pumpMgr.StopAll();
@@ -71,7 +71,7 @@ public void Multiple_pumps_can_be_started()
7171
for (var i = 0; i < 5; i++)
7272
{
7373
var handle = CreateTestHandle();
74-
pumpMgr.StartInboundPump(handle, Http3StreamType.Request, TestEndpoint, connectionGen: 0, streamId: i);
74+
pumpMgr.StartInboundPump(handle, -1, TestEndpoint, connectionGen: 0, streamId: i);
7575
}
7676

7777
// StopAll should handle all pumps
@@ -84,7 +84,7 @@ public void Control_stream_pump_should_not_throw()
8484
var pumpMgr = new QuicPumpManager(ActorRefs.Nobody);
8585
var handle = CreateTestHandle();
8686

87-
pumpMgr.StartInboundPump(handle, Http3StreamType.Control, TestEndpoint, connectionGen: 0);
87+
pumpMgr.StartInboundPump(handle, 0x00, TestEndpoint, connectionGen: 0, streamId: -2);
8888

8989
pumpMgr.StopAll();
9090
}
@@ -95,19 +95,18 @@ public void Encoder_stream_pump_should_not_throw()
9595
var pumpMgr = new QuicPumpManager(ActorRefs.Nobody);
9696
var handle = CreateTestHandle();
9797

98-
pumpMgr.StartInboundPump(handle, Http3StreamType.QpackEncoder, TestEndpoint, connectionGen: 0);
98+
pumpMgr.StartInboundPump(handle, 0x02, TestEndpoint, connectionGen: 0, streamId: -3);
9999

100100
pumpMgr.StopAll();
101101
}
102102

103103
[Fact(Timeout = 5000)]
104-
public void StartInboundPump_without_stream_id_should_work()
104+
public void StartInboundPump_with_explicit_stream_id_should_work()
105105
{
106106
var pumpMgr = new QuicPumpManager(ActorRefs.Nobody);
107107
var handle = CreateTestHandle();
108108

109-
// Default streamId = -1 for connection-level streams
110-
pumpMgr.StartInboundPump(handle, Http3StreamType.Control, TestEndpoint, connectionGen: 0);
109+
pumpMgr.StartInboundPump(handle, 0x00, TestEndpoint, connectionGen: 0, streamId: -2);
111110

112111
pumpMgr.StopAll();
113112
}
@@ -118,7 +117,7 @@ public void StopAll_can_be_called_multiple_times()
118117
var pumpMgr = new QuicPumpManager(ActorRefs.Nobody);
119118
var handle = CreateTestHandle();
120119

121-
pumpMgr.StartInboundPump(handle, Http3StreamType.Request, TestEndpoint, connectionGen: 0, streamId: 1);
120+
pumpMgr.StartInboundPump(handle, -1, TestEndpoint, connectionGen: 0, streamId: 1);
122121

123122
pumpMgr.StopAll();
124123
pumpMgr.StopAll();
@@ -127,4 +126,4 @@ public void StopAll_can_be_called_multiple_times()
127126
// Should not throw
128127
Assert.True(true);
129128
}
130-
}
129+
}

src/TurboHTTP.StreamTests/Transport/QuicStreamRouterEnhancedSpec.cs

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
using System.Threading.Channels;
33
using Akka.Actor;
44
using TurboHTTP.Internal;
5+
using TurboHTTP.Protocol.Http3;
56
using TurboHTTP.Tests.Shared;
67
using TurboHTTP.Transport.Connection;
78
using TurboHTTP.Transport.Quic;
@@ -38,15 +39,16 @@ private static (ConnectionHandle Handle, ChannelReader<NetworkBuffer> OutboundRe
3839
public void RouteTaggedItem_should_route_encoder_to_pending_when_no_handle()
3940
{
4041
var (router, ops) = CreateRouter();
41-
var pendingEncoder = new Queue<NetworkBuffer>();
4242

4343
var encoderData = Http3NetworkBuffer.Rent(4);
44-
encoderData.StreamType = Http3StreamType.QpackEncoder;
44+
encoderData.StreamTypeValue = (long)StreamType.QpackEncoder;
4545
encoderData.Length = 3;
4646

47-
router.RouteTaggedItem(encoderData, null, new Queue<NetworkBuffer>(), null, pendingEncoder);
47+
var encoderState = new TypedStreamState { StreamId = -3 };
48+
var typedStreams = new Dictionary<long, TypedStreamState> { [0x02] = encoderState };
49+
router.RouteTaggedItem(encoderData, 0x02, typedStreams);
4850

49-
Assert.Single(pendingEncoder);
51+
Assert.Single(encoderState.PendingItems);
5052
Assert.True(ops.PullInputCount > 0);
5153
}
5254

@@ -57,11 +59,12 @@ public void RouteTaggedItem_should_write_encoder_to_handle_when_available()
5759
var (encoderHandle, encoderReader) = CreateTestHandle();
5860

5961
var encoderData = Http3NetworkBuffer.Rent(4);
60-
encoderData.StreamType = Http3StreamType.QpackEncoder;
62+
encoderData.StreamTypeValue = (long)StreamType.QpackEncoder;
6163
encoderData.Length = 3;
6264

63-
router.RouteTaggedItem(encoderData, null, new Queue<NetworkBuffer>(), encoderHandle,
64-
new Queue<NetworkBuffer>());
65+
var encoderState = new TypedStreamState { Handle = encoderHandle, StreamId = -3 };
66+
var typedStreams = new Dictionary<long, TypedStreamState> { [0x02] = encoderState };
67+
router.RouteTaggedItem(encoderData, 0x02, typedStreams);
6568

6669
Assert.True(encoderReader.TryRead(out _));
6770
}
@@ -299,12 +302,11 @@ public void RouteTaggedItem_request_with_wrong_stream_id_should_handle_gracefull
299302
ctx.Handle = handle;
300303

301304
var dataItem = Http3NetworkBuffer.Rent(4);
302-
dataItem.StreamType = Http3StreamType.Request;
303305
dataItem.StreamId = 999; // Different from expected
304306
dataItem.Length = 3;
305307

306-
// Should not throw - routing handles mismatched stream IDs gracefully
307-
router.RouteTaggedItem(dataItem, null, new Queue<NetworkBuffer>(), null, new Queue<NetworkBuffer>());
308+
var typedStreams = new Dictionary<long, TypedStreamState>();
309+
router.RouteTaggedItem(dataItem, -1, typedStreams);
308310

309311
// Verify the operation completed without error
310312
Assert.NotNull(router);

src/TurboHTTP.StreamTests/Transport/QuicStreamRouterSpec.cs

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
using System.Threading.Channels;
33
using Akka.Actor;
44
using TurboHTTP.Internal;
5+
using TurboHTTP.Protocol.Http3;
56
using TurboHTTP.Tests.Shared;
67
using TurboHTTP.Transport.Connection;
78
using TurboHTTP.Transport.Quic;
@@ -102,11 +103,11 @@ public void RouteTaggedItem_should_write_to_handle_for_known_request_stream()
102103
ctx.Handle = handle;
103104

104105
var dataItem = Http3NetworkBuffer.Rent(4);
105-
dataItem.StreamType = Http3StreamType.Request;
106106
dataItem.StreamId = 1;
107107
dataItem.Length = 3;
108108

109-
router.RouteTaggedItem(dataItem, null, new Queue<NetworkBuffer>(), null, new Queue<NetworkBuffer>());
109+
var typedStreams = new Dictionary<long, TypedStreamState>();
110+
router.RouteTaggedItem(dataItem, -1, typedStreams);
110111

111112
Assert.True(outboundReader.TryRead(out _));
112113
}
@@ -118,11 +119,11 @@ public void RouteTaggedItem_should_enqueue_when_handle_not_ready()
118119
router.GetOrCreateContext(1);
119120

120121
var dataItem = Http3NetworkBuffer.Rent(4);
121-
dataItem.StreamType = Http3StreamType.Request;
122122
dataItem.StreamId = 1;
123123
dataItem.Length = 3;
124124

125-
router.RouteTaggedItem(dataItem, null, new Queue<NetworkBuffer>(), null, new Queue<NetworkBuffer>());
125+
var typedStreams = new Dictionary<long, TypedStreamState>();
126+
router.RouteTaggedItem(dataItem, -1, typedStreams);
126127

127128
Assert.Single(router.RequestStreams[1].PendingWrites);
128129
Assert.True(ops.PullInputCount > 0);
@@ -132,15 +133,16 @@ public void RouteTaggedItem_should_enqueue_when_handle_not_ready()
132133
public void RouteTaggedItem_should_route_control_to_pending_queue_when_no_handle()
133134
{
134135
var (router, ops) = CreateRouter();
135-
var pendingControl = new Queue<NetworkBuffer>();
136+
var controlState = new TypedStreamState { StreamId = -2 };
137+
var typedStreams = new Dictionary<long, TypedStreamState> { [0x00] = controlState };
136138

137139
var dataItem = Http3NetworkBuffer.Rent(4);
138-
dataItem.StreamType = Http3StreamType.Control;
140+
dataItem.StreamTypeValue = (long)StreamType.Control;
139141
dataItem.Length = 3;
140142

141-
router.RouteTaggedItem(dataItem, null, pendingControl, null, new Queue<NetworkBuffer>());
143+
router.RouteTaggedItem(dataItem, 0x00, typedStreams);
142144

143-
Assert.Single(pendingControl);
145+
Assert.Single(controlState.PendingItems);
144146
Assert.True(ops.PullInputCount > 0);
145147
}
146148

@@ -149,12 +151,14 @@ public void RouteTaggedItem_should_write_control_to_handle_when_available()
149151
{
150152
var (router, _) = CreateRouter();
151153
var (controlHandle, controlReader) = CreateTestHandle();
154+
var controlState = new TypedStreamState { Handle = controlHandle, StreamId = -2 };
155+
var typedStreams = new Dictionary<long, TypedStreamState> { [0x00] = controlState };
152156

153157
var dataItem = Http3NetworkBuffer.Rent(4);
154-
dataItem.StreamType = Http3StreamType.Control;
158+
dataItem.StreamTypeValue = (long)StreamType.Control;
155159
dataItem.Length = 3;
156160

157-
router.RouteTaggedItem(dataItem, controlHandle, new Queue<NetworkBuffer>(), null, new Queue<NetworkBuffer>());
161+
router.RouteTaggedItem(dataItem, 0x00, typedStreams);
158162

159163
Assert.True(controlReader.TryRead(out _));
160164
}

0 commit comments

Comments
 (0)