Skip to content

Commit d009f2b

Browse files
committed
Merge branch 'develop'
* develop: chore(release): prepare for 0.0.16 fix: Implement robust ACK handling and retransmission
2 parents 46f28a4 + f4cba9a commit d009f2b

7 files changed

Lines changed: 911 additions & 64 deletions

File tree

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
# Changelog
22

3+
## [0.0.16] - 2026-03-10
4+
5+
### <!-- 1 -->🐛 Bug Fixes
6+
7+
- Implement robust ACK handling and retransmission
8+
39
## [0.0.15] - 2026-03-04
410

511
### <!-- 3 -->📚 Documentation

PROJECT

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
11
libfast
2-
0.0.15
2+
0.0.16

build.zig.zon

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
.{
22
.name = .libfast,
3-
.version = "0.0.15",
3+
.version = "0.0.16",
44
.dependencies = .{
55
.libsafe = .{
6-
.url = "git+https://github.com/libzig/libsafe?ref=0.0.7",
6+
.url = "git+https://github.com/libzig/libsafe#0.0.7",
77
.hash = "libsafe-0.0.7-ucZASCGdAwA9tuwbOrMk1dipWU9BfjIorGYIvasBxfaZ",
88
// .path = "../libsafe",
99
},

lib/connection.zig

Lines changed: 130 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,11 @@ pub const Connection = struct {
1818
/// Packet number tracking
1919
next_packet_number: u32,
2020
largest_received_packet: u32,
21-
largest_acked_packet: u32,
21+
next_expected_packet: u32,
22+
last_ack_sent: ?u32,
23+
largest_peer_acked_packet: ?u32,
24+
pending_ack: bool,
25+
received_packets: std.AutoHashMap(u32, void),
2226

2327
/// Connection IDs
2428
local_conn_id: []const u8,
@@ -50,14 +54,21 @@ pub const Connection = struct {
5054

5155
try streams.put(0, stream0);
5256

57+
var received_packets = std.AutoHashMap(u32, void).init(allocator);
58+
errdefer received_packets.deinit();
59+
5360
return Self{
5461
.allocator = allocator,
5562
.is_server = is_server,
5663
.streams = streams,
5764
.next_stream_id = initial_stream_id,
5865
.next_packet_number = 0,
5966
.largest_received_packet = 0,
60-
.largest_acked_packet = 0,
67+
.next_expected_packet = 0,
68+
.last_ack_sent = null,
69+
.largest_peer_acked_packet = null,
70+
.pending_ack = false,
71+
.received_packets = received_packets,
6172
.local_conn_id = local_conn_id,
6273
.remote_conn_id = remote_conn_id,
6374
};
@@ -71,6 +82,7 @@ pub const Connection = struct {
7182
self.allocator.destroy(stream.*);
7283
}
7384
self.streams.deinit();
85+
self.received_packets.deinit();
7486
}
7587

7688
/// Open a new bidirectional stream
@@ -146,17 +158,40 @@ pub const Connection = struct {
146158
return pn;
147159
}
148160

149-
/// Record received packet number
150-
pub fn recordReceivedPacket(self: *Self, packet_number: u32) void {
161+
/// Record received packet number.
162+
///
163+
/// Returns true if the packet number is new.
164+
pub fn recordReceivedPacket(self: *Self, packet_number: u32) !bool {
151165
if (packet_number > self.largest_received_packet) {
152166
self.largest_received_packet = packet_number;
153167
}
168+
169+
if (packet_number < self.next_expected_packet) {
170+
return false;
171+
}
172+
173+
const gop = try self.received_packets.getOrPut(packet_number);
174+
if (gop.found_existing) {
175+
return false;
176+
}
177+
gop.value_ptr.* = {};
178+
179+
while (self.received_packets.remove(self.next_expected_packet)) {
180+
self.next_expected_packet += 1;
181+
}
182+
183+
return true;
184+
}
185+
186+
/// Schedule an ACK for transmission.
187+
pub fn scheduleAck(self: *Self) void {
188+
self.pending_ack = true;
154189
}
155190

156-
/// Record ACKed packet
157-
pub fn recordAckedPacket(self: *Self, packet_number: u32) void {
158-
if (packet_number > self.largest_acked_packet) {
159-
self.largest_acked_packet = packet_number;
191+
/// Record the largest packet number the peer has ACKed from us.
192+
pub fn recordPeerAckedPacket(self: *Self, packet_number: u32) void {
193+
if (self.largest_peer_acked_packet == null or packet_number > self.largest_peer_acked_packet.?) {
194+
self.largest_peer_acked_packet = packet_number;
160195
}
161196
}
162197

@@ -179,13 +214,22 @@ pub const Connection = struct {
179214

180215
/// Check if connection needs to send ACK
181216
pub fn needsAck(self: *const Self) bool {
182-
// Send ACK if we've received packets that haven't been acked
183-
return self.largest_received_packet > self.largest_acked_packet;
217+
return self.pending_ack and (self.next_expected_packet > 0 or self.received_packets.count() > 0);
184218
}
185219

186220
/// Get packet number to acknowledge
187221
pub fn getAckNumber(self: *const Self) u32 {
188-
return self.largest_received_packet;
222+
std.debug.assert(self.next_expected_packet > 0);
223+
return self.next_expected_packet - 1;
224+
}
225+
226+
pub fn markAckSent(self: *Self) void {
227+
self.last_ack_sent = if (self.next_expected_packet > 0) self.getAckNumber() else null;
228+
self.pending_ack = false;
229+
}
230+
231+
pub fn getLargestPeerAckedPacket(self: *const Self) ?u32 {
232+
return self.largest_peer_acked_packet;
189233
}
190234
};
191235

@@ -255,11 +299,83 @@ test "Connection - packet number tracking" {
255299
try testing.expectEqual(@as(u32, 2), conn.nextPacketNumber());
256300

257301
// Record received packets
258-
conn.recordReceivedPacket(5);
259-
conn.recordReceivedPacket(10);
260-
conn.recordReceivedPacket(7); // Out of order
302+
try testing.expect(try conn.recordReceivedPacket(5));
303+
try testing.expect(try conn.recordReceivedPacket(10));
304+
try testing.expect(try conn.recordReceivedPacket(7)); // Out of order
261305

262306
try testing.expectEqual(@as(u32, 10), conn.largest_received_packet);
307+
try testing.expect(!conn.needsAck());
308+
}
309+
310+
test "Connection - ACK scheduling is explicit" {
311+
const testing = std.testing;
312+
const allocator = testing.allocator;
313+
314+
var conn = try Connection.init(
315+
allocator,
316+
"local",
317+
"remote",
318+
false,
319+
);
320+
defer conn.deinit();
321+
322+
try testing.expect(try conn.recordReceivedPacket(0));
323+
try testing.expect(!conn.needsAck());
324+
325+
conn.scheduleAck();
326+
try testing.expect(conn.needsAck());
327+
}
328+
329+
test "Connection - cumulative ACK tracking handles gaps" {
330+
const testing = std.testing;
331+
const allocator = testing.allocator;
332+
333+
var conn = try Connection.init(
334+
allocator,
335+
"local",
336+
"remote",
337+
false,
338+
);
339+
defer conn.deinit();
340+
341+
try testing.expect(try conn.recordReceivedPacket(0));
342+
conn.scheduleAck();
343+
try testing.expect(conn.needsAck());
344+
try testing.expectEqual(@as(u32, 0), conn.getAckNumber());
345+
conn.markAckSent();
346+
try testing.expect(!conn.needsAck());
347+
348+
try testing.expect(try conn.recordReceivedPacket(2));
349+
conn.scheduleAck();
350+
try testing.expect(conn.needsAck());
351+
try testing.expectEqual(@as(u32, 0), conn.getAckNumber());
352+
conn.markAckSent();
353+
354+
try testing.expect(try conn.recordReceivedPacket(1));
355+
conn.scheduleAck();
356+
try testing.expect(conn.needsAck());
357+
try testing.expectEqual(@as(u32, 2), conn.getAckNumber());
358+
}
359+
360+
test "Connection - peer ACK state is separate from outbound ACK generation" {
361+
const testing = std.testing;
362+
const allocator = testing.allocator;
363+
364+
var conn = try Connection.init(
365+
allocator,
366+
"local",
367+
"remote",
368+
false,
369+
);
370+
defer conn.deinit();
371+
372+
conn.recordPeerAckedPacket(99);
373+
try testing.expectEqual(@as(?u32, 99), conn.getLargestPeerAckedPacket());
374+
375+
try testing.expect(try conn.recordReceivedPacket(0));
376+
conn.scheduleAck();
377+
try testing.expect(conn.needsAck());
378+
try testing.expectEqual(@as(u32, 0), conn.getAckNumber());
263379
}
264380

265381
test "Connection - getOrCreateStream" {

lib/frame.zig

Lines changed: 97 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -196,13 +196,23 @@ pub const MaxStreamDataFrame = struct {
196196
}
197197
};
198198

199-
/// ACK Frame (simplified - just largest acknowledged)
199+
/// ACK Frame (simplified QUIC ACK ranges without ECN)
200200
pub const AckFrame = struct {
201201
largest_acknowledged: u64,
202+
first_ack_range: u64 = 0,
203+
ack_ranges: []const AckRange = &.{},
202204

203-
pub fn encode(self: AckFrame, buffer: []u8) !usize {
204-
if (buffer.len < 10) return error.BufferTooSmall;
205+
pub const AckRange = struct {
206+
gap: u64,
207+
ack_range_length: u64,
208+
};
209+
210+
pub const DecodeResult = struct {
211+
frame: AckFrame,
212+
consumed: usize,
213+
};
205214

215+
pub fn encode(self: AckFrame, buffer: []u8) !usize {
206216
var offset: usize = 0;
207217

208218
// Type
@@ -215,22 +225,71 @@ pub const AckFrame = struct {
215225
// ACK Delay (simplified - set to 0)
216226
offset += encodeVarInt(0, buffer[offset..]);
217227

218-
// ACK Range Count (simplified - 0 ranges)
219-
offset += encodeVarInt(0, buffer[offset..]);
228+
// ACK Range Count
229+
offset += encodeVarInt(self.ack_ranges.len, buffer[offset..]);
230+
231+
// First ACK Range
232+
offset += encodeVarInt(self.first_ack_range, buffer[offset..]);
233+
234+
// ACK Ranges
235+
for (self.ack_ranges) |ack_range| {
236+
offset += encodeVarInt(ack_range.gap, buffer[offset..]);
237+
offset += encodeVarInt(ack_range.ack_range_length, buffer[offset..]);
238+
}
220239

221240
return offset;
222241
}
223242

224-
pub fn decode(buffer: []const u8) !AckFrame {
243+
pub fn decode(buffer: []const u8, ack_ranges_out: []AckRange) !DecodeResult {
225244
if (buffer.len < 2) return error.BufferTooSmall;
226245

227-
const offset: usize = 1; // Skip type byte
246+
var offset: usize = 0;
247+
if (buffer[offset] != @intFromEnum(FrameType.ack)) {
248+
return error.InvalidFrameType;
249+
}
250+
offset += 1;
228251

229-
// Largest Acknowledged
230252
const largest_ack_result = try decodeVarInt(buffer[offset..]);
253+
offset += largest_ack_result.bytes;
254+
255+
const ack_delay_result = try decodeVarInt(buffer[offset..]);
256+
offset += ack_delay_result.bytes;
257+
258+
const ack_range_count_result = try decodeVarInt(buffer[offset..]);
259+
offset += ack_range_count_result.bytes;
260+
if (ack_range_count_result.value > ack_ranges_out.len) {
261+
return error.BufferTooSmall;
262+
}
231263

232-
return AckFrame{
233-
.largest_acknowledged = largest_ack_result.value,
264+
const first_ack_range_result = try decodeVarInt(buffer[offset..]);
265+
offset += first_ack_range_result.bytes;
266+
if (first_ack_range_result.value > largest_ack_result.value) {
267+
return error.InvalidData;
268+
}
269+
270+
var parsed_ranges: usize = 0;
271+
var i: u64 = 0;
272+
while (i < ack_range_count_result.value) : (i += 1) {
273+
const gap_result = try decodeVarInt(buffer[offset..]);
274+
offset += gap_result.bytes;
275+
276+
const range_len_result = try decodeVarInt(buffer[offset..]);
277+
offset += range_len_result.bytes;
278+
279+
ack_ranges_out[parsed_ranges] = .{
280+
.gap = gap_result.value,
281+
.ack_range_length = range_len_result.value,
282+
};
283+
parsed_ranges += 1;
284+
}
285+
286+
return .{
287+
.frame = AckFrame{
288+
.largest_acknowledged = largest_ack_result.value,
289+
.first_ack_range = first_ack_range_result.value,
290+
.ack_ranges = ack_ranges_out[0..parsed_ranges],
291+
},
292+
.consumed = offset,
234293
};
235294
}
236295
};
@@ -332,6 +391,34 @@ test "StreamFrame - encode and decode" {
332391
try testing.expectEqualSlices(u8, original.data, decoded.data);
333392
}
334393

394+
test "AckFrame - encode and decode with ranges" {
395+
const testing = std.testing;
396+
397+
const original = AckFrame{
398+
.largest_acknowledged = 12,
399+
.first_ack_range = 2,
400+
.ack_ranges = &[_]AckFrame.AckRange{
401+
.{ .gap = 0, .ack_range_length = 4 },
402+
.{ .gap = 1, .ack_range_length = 1 },
403+
},
404+
};
405+
406+
var buffer: [128]u8 = undefined;
407+
const encoded_len = try original.encode(&buffer);
408+
409+
var ack_ranges: [4]AckFrame.AckRange = undefined;
410+
const decoded = try AckFrame.decode(buffer[0..encoded_len], &ack_ranges);
411+
412+
try testing.expectEqual(original.largest_acknowledged, decoded.frame.largest_acknowledged);
413+
try testing.expectEqual(original.first_ack_range, decoded.frame.first_ack_range);
414+
try testing.expectEqual(@as(usize, 2), decoded.frame.ack_ranges.len);
415+
try testing.expectEqual(original.ack_ranges[0].gap, decoded.frame.ack_ranges[0].gap);
416+
try testing.expectEqual(original.ack_ranges[0].ack_range_length, decoded.frame.ack_ranges[0].ack_range_length);
417+
try testing.expectEqual(original.ack_ranges[1].gap, decoded.frame.ack_ranges[1].gap);
418+
try testing.expectEqual(original.ack_ranges[1].ack_range_length, decoded.frame.ack_ranges[1].ack_range_length);
419+
try testing.expectEqual(encoded_len, decoded.consumed);
420+
}
421+
335422
test "varInt encoding" {
336423
const testing = std.testing;
337424

0 commit comments

Comments
 (0)