[net] quic: send ECN feedback to peers

0 views
Skip to first unread message

Rhys Hiltner (Gerrit)

unread,
6:47 PM (5 hours ago) 6:47 PM
to goph...@pubsubhelper.golang.org, Rhys Hiltner, golang-co...@googlegroups.com

Rhys Hiltner has uploaded the change for review

Commit message

quic: send ECN feedback to peers

Track the total number of ECT(0), ECT(1), and ECN-CE state of packets we
process in each packet number space. Send it back to the peer in each
ACK frame (unless it's all zeros).

"Even if an endpoint does not set an ECT field in packets it sends, the
endpoint MUST provide feedback about ECN markings it receives, if these
are accessible."
https://www.rfc-editor.org/rfc/rfc9000#section-13.4.1-2

For golang/go#58547
Change-Id: I3ce5be6c536198eaa711f527402503b0567fc7a5

Change diff

diff --git a/internal/quic/cmd/interop/main.go b/internal/quic/cmd/interop/main.go
index 65142ad..682cc77 100644
--- a/internal/quic/cmd/interop/main.go
+++ b/internal/quic/cmd/interop/main.go
@@ -95,6 +95,9 @@
basicTest(ctx, config, urls)
return
}
+ case "ecn":
+ // TODO: We give ECN feedback to the sender, but we don't add our own
+ // ECN marks to outgoing packets.
case "transfer":
// "The client should use small initial flow control windows
// for both stream- and connection-level flow control
diff --git a/quic/acks.go b/quic/acks.go
index d4ac449..90f82be 100644
--- a/quic/acks.go
+++ b/quic/acks.go
@@ -25,6 +25,15 @@

// The number of ack-eliciting packets in seen that we have not yet acknowledged.
unackedAckEliciting int
+
+ // Total ECN counters for this packet number space.
+ ecn ecnCounts
+}
+
+type ecnCounts struct {
+ t0 int
+ t1 int
+ ce int
}

// shouldProcess reports whether a packet should be handled or discarded.
@@ -43,10 +52,10 @@
}

// receive records receipt of a packet.
-func (acks *ackState) receive(now time.Time, space numberSpace, num packetNumber, ackEliciting bool) {
+func (acks *ackState) receive(now time.Time, space numberSpace, num packetNumber, ackEliciting bool, ecn ecnBits) {
if ackEliciting {
acks.unackedAckEliciting++
- if acks.mustAckImmediately(space, num) {
+ if acks.mustAckImmediately(space, num, ecn) {
acks.nextAck = now
} else if acks.nextAck.IsZero() {
// This packet does not need to be acknowledged immediately,
@@ -70,6 +79,15 @@
acks.maxRecvTime = now
}

+ switch ecn {
+ case ecnECT0:
+ acks.ecn.t0++
+ case ecnECT1:
+ acks.ecn.t1++
+ case ecnCE:
+ acks.ecn.ce++
+ }
+
// Limit the total number of ACK ranges by dropping older ranges.
//
// Remembering more ranges results in larger ACK frames.
@@ -92,7 +110,7 @@

// mustAckImmediately reports whether an ack-eliciting packet must be acknowledged immediately,
// or whether the ack may be deferred.
-func (acks *ackState) mustAckImmediately(space numberSpace, num packetNumber) bool {
+func (acks *ackState) mustAckImmediately(space numberSpace, num packetNumber, ecn ecnBits) bool {
// https://www.rfc-editor.org/rfc/rfc9000.html#section-13.2.1
if space != appDataSpace {
// "[...] all ack-eliciting Initial and Handshake packets [...]"
@@ -128,6 +146,12 @@
// there are no gaps. If it does not, there must be a gap.
return true
}
+ // "[...] packets marked with the ECN Congestion Experienced (CE) codepoint
+ // in the IP header SHOULD be acknowledged immediately [...]"
+ // https://www.rfc-editor.org/rfc/rfc9000.html#section-13.2.1-9
+ if ecn == ecnCE {
+ return true
+ }
// "[...] SHOULD send an ACK frame after receiving at least two ack-eliciting packets."
// https://www.rfc-editor.org/rfc/rfc9000.html#section-13.2.2
//
diff --git a/quic/acks_test.go b/quic/acks_test.go
index 7fca561..2abdc31 100644
--- a/quic/acks_test.go
+++ b/quic/acks_test.go
@@ -17,7 +17,7 @@
receive := []packetNumber{0, 1, 2, 4, 7, 6, 9}
seen := map[packetNumber]bool{}
for i, pnum := range receive {
- acks.receive(now, appDataSpace, pnum, true)
+ acks.receive(now, appDataSpace, pnum, true, ecnNotECT)
seen[pnum] = true
for ppnum := packetNumber(0); ppnum < 11; ppnum++ {
if got, want := acks.shouldProcess(ppnum), !seen[ppnum]; got != want {
@@ -32,7 +32,7 @@
acks := ackState{}
now := time.Now()
for pnum := packetNumber(0); ; pnum += 2 {
- acks.receive(now, appDataSpace, pnum, true)
+ acks.receive(now, appDataSpace, pnum, true, ecnNotECT)
send, _ := acks.acksToSend(now)
for ppnum := packetNumber(0); ppnum < packetNumber(send.min()); ppnum++ {
if acks.shouldProcess(ppnum) {
@@ -158,13 +158,13 @@
start := time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC)
for _, p := range test.ackedPackets {
t.Logf("receive %v.%v, ack-eliciting=%v", test.space, p.pnum, p.ackEliciting)
- acks.receive(start, test.space, p.pnum, p.ackEliciting)
+ acks.receive(start, test.space, p.pnum, p.ackEliciting, ecnNotECT)
}
t.Logf("send an ACK frame")
acks.sentAck()
for _, p := range test.packets {
t.Logf("receive %v.%v, ack-eliciting=%v", test.space, p.pnum, p.ackEliciting)
- acks.receive(start, test.space, p.pnum, p.ackEliciting)
+ acks.receive(start, test.space, p.pnum, p.ackEliciting, ecnNotECT)
}
switch {
case len(test.wantAcks) == 0:
@@ -208,13 +208,13 @@
func TestAcksDiscardAfterAck(t *testing.T) {
acks := ackState{}
now := time.Now()
- acks.receive(now, appDataSpace, 0, true)
- acks.receive(now, appDataSpace, 2, true)
- acks.receive(now, appDataSpace, 4, true)
- acks.receive(now, appDataSpace, 5, true)
- acks.receive(now, appDataSpace, 6, true)
+ acks.receive(now, appDataSpace, 0, true, ecnNotECT)
+ acks.receive(now, appDataSpace, 2, true, ecnNotECT)
+ acks.receive(now, appDataSpace, 4, true, ecnNotECT)
+ acks.receive(now, appDataSpace, 5, true, ecnNotECT)
+ acks.receive(now, appDataSpace, 6, true, ecnNotECT)
acks.handleAck(6) // discards all ranges prior to the one containing packet 6
- acks.receive(now, appDataSpace, 7, true)
+ acks.receive(now, appDataSpace, 7, true, ecnNotECT)
got, _ := acks.acksToSend(now)
if len(got) != 1 {
t.Errorf("acks.acksToSend contains ranges prior to last acknowledged ack; got %v, want 1 range", got)
@@ -224,9 +224,9 @@
func TestAcksLargestSeen(t *testing.T) {
acks := ackState{}
now := time.Now()
- acks.receive(now, appDataSpace, 0, true)
- acks.receive(now, appDataSpace, 4, true)
- acks.receive(now, appDataSpace, 1, true)
+ acks.receive(now, appDataSpace, 0, true, ecnNotECT)
+ acks.receive(now, appDataSpace, 4, true, ecnNotECT)
+ acks.receive(now, appDataSpace, 1, true, ecnNotECT)
if got, want := acks.largestSeen(), packetNumber(4); got != want {
t.Errorf("acks.largestSeen() = %v, want %v", got, want)
}
diff --git a/quic/conn_loss.go b/quic/conn_loss.go
index 06761e3..bc6d106 100644
--- a/quic/conn_loss.go
+++ b/quic/conn_loss.go
@@ -32,7 +32,7 @@
switch f := sent.next(); f {
default:
panic(fmt.Sprintf("BUG: unhandled acked/lost frame type %x", f))
- case frameTypeAck:
+ case frameTypeAck, frameTypeAckECN:
// Unlike most information, loss of an ACK frame does not trigger
// retransmission. ACKs are sent in response to ack-eliciting packets,
// and always contain the latest information available.
diff --git a/quic/conn_recv.go b/quic/conn_recv.go
index e4ef23b..2a7204c 100644
--- a/quic/conn_recv.go
+++ b/quic/conn_recv.go
@@ -124,7 +124,7 @@
}
c.connIDState.handlePacket(c, p.ptype, p.srcConnID)
ackEliciting := c.handleFrames(now, dgram, ptype, space, p.payload)
- c.acks[space].receive(now, space, p.num, ackEliciting)
+ c.acks[space].receive(now, space, p.num, ackEliciting, dgram.ecn)
if p.ptype == packetTypeHandshake && c.side == serverSide {
c.loss.validateClientAddress()

@@ -174,7 +174,7 @@
c.log1RTTPacketReceived(p, buf)
}
ackEliciting := c.handleFrames(now, dgram, packetType1RTT, appDataSpace, p.payload)
- c.acks[appDataSpace].receive(now, appDataSpace, p.num, ackEliciting)
+ c.acks[appDataSpace].receive(now, appDataSpace, p.num, ackEliciting, dgram.ecn)
return len(buf)
}

@@ -420,12 +420,15 @@

func (c *Conn) handleAckFrame(now time.Time, space numberSpace, payload []byte) int {
c.loss.receiveAckStart()
- largest, ackDelay, n := consumeAckFrame(payload, func(rangeIndex int, start, end packetNumber) {
+ largest, ackDelay, ecn, n := consumeAckFrame(payload, func(rangeIndex int, start, end packetNumber) {
if err := c.loss.receiveAckRange(now, space, rangeIndex, start, end, c.handleAckOrLoss); err != nil {
c.abort(now, err)
return
}
})
+ // TODO: Make use of ECN feedback.
+ // https://www.rfc-editor.org/rfc/rfc9000.html#section-19.3.2
+ _ = ecn
// Prior to receiving the peer's transport parameters, we cannot
// interpret the ACK Delay field because we don't know the ack_delay_exponent
// to apply.
diff --git a/quic/conn_send.go b/quic/conn_send.go
index d6fb149..3e8cf52 100644
--- a/quic/conn_send.go
+++ b/quic/conn_send.go
@@ -374,7 +374,7 @@
return false
}
d := unscaledAckDelayFromDuration(delay, ackDelayExponent)
- return c.w.appendAckFrame(seen, d)
+ return c.w.appendAckFrame(seen, d, c.acks[space].ecn)
}

func (c *Conn) appendConnectionCloseFrame(now time.Time, space numberSpace, err error) {
diff --git a/quic/frame_debug.go b/quic/frame_debug.go
index 7cf03fa..8d8fd54 100644
--- a/quic/frame_debug.go
+++ b/quic/frame_debug.go
@@ -136,11 +136,12 @@
type debugFrameAck struct {
ackDelay unscaledAckDelay
ranges []i64range[packetNumber]
+ ecn ecnCounts
}

func parseDebugFrameAck(b []byte) (f debugFrameAck, n int) {
f.ranges = nil
- _, f.ackDelay, n = consumeAckFrame(b, func(_ int, start, end packetNumber) {
+ _, f.ackDelay, f.ecn, n = consumeAckFrame(b, func(_ int, start, end packetNumber) {
f.ranges = append(f.ranges, i64range[packetNumber]{
start: start,
end: end,
@@ -159,11 +160,15 @@
for _, r := range f.ranges {
s += fmt.Sprintf(" [%v,%v)", r.start, r.end)
}
+
+ if (f.ecn != ecnCounts{}) {
+ s += fmt.Sprintf(" ECN=[%d,%d,%d]", f.ecn.t0, f.ecn.t1, f.ecn.ce)
+ }
return s
}

func (f debugFrameAck) write(w *packetWriter) bool {
- return w.appendAckFrame(rangeset[packetNumber](f.ranges), f.ackDelay)
+ return w.appendAckFrame(rangeset[packetNumber](f.ranges), f.ackDelay, f.ecn)
}

func (f debugFrameAck) LogValue() slog.Value {
diff --git a/quic/packet_codec_test.go b/quic/packet_codec_test.go
index be335d7..4ae22b3 100644
--- a/quic/packet_codec_test.go
+++ b/quic/packet_codec_test.go
@@ -264,6 +264,65 @@
0x0e, // ACK Range Length (i)
},
}, {
+ s: "ACK Delay=10 [0,16) [17,32) ECN=[1,2,3]",
+ j: `"error: debugFrameAck should not appear as a slog Value"`,
+ f: debugFrameAck{
+ ackDelay: 10,
+ ranges: []i64range[packetNumber]{
+ {0x00, 0x10},
+ {0x11, 0x20},
+ },
+ ecn: ecnCounts{1, 2, 3},
+ },
+ b: []byte{
+ 0x03, // TYPE (i) = 0x3
+ 0x1f, // Largest Acknowledged (i)
+ 10, // ACK Delay (i)
+ 0x01, // ACK Range Count (i)
+ 0x0e, // First ACK Range (i)
+ 0x00, // Gap (i)
+ 0x0f, // ACK Range Length (i)
+ 0x01, // ECT0 Count (i)
+ 0x02, // ECT1 Count (i)
+ 0x03, // ECN-CE Count (i)
+ },
+ truncated: []byte{
+ 0x03, // TYPE (i) = 0x3
+ 0x1f, // Largest Acknowledged (i)
+ 10, // ACK Delay (i)
+ 0x00, // ACK Range Count (i)
+ 0x0e, // First ACK Range (i)
+ 0x01, // ECT0 Count (i)
+ 0x02, // ECT1 Count (i)
+ 0x03, // ECN-CE Count (i)
+ },
+ }, {
+ s: "ACK Delay=10 [17,32) ECN=[1,2,3]",
+ j: `"error: debugFrameAck should not appear as a slog Value"`,
+ f: debugFrameAck{
+ ackDelay: 10,
+ ranges: []i64range[packetNumber]{
+ {0x11, 0x20},
+ },
+ ecn: ecnCounts{1, 2, 3},
+ },
+ b: []byte{
+ 0x03, // TYPE (i) = 0x3
+ 0x1f, // Largest Acknowledged (i)
+ 10, // ACK Delay (i)
+ 0x00, // ACK Range Count (i)
+ 0x0e, // First ACK Range (i)
+ 0x01, // ECT0 Count (i)
+ 0x02, // ECT1 Count (i)
+ 0x03, // ECN-CE Count (i)
+ },
+ // Downgrading to a type 0x2 ACK frame is not allowed: "Even if an
+ // endpoint does not set an ECT field in packets it sends, the endpoint
+ // MUST provide feedback about ECN markings it receives, if these are
+ // accessible."
+ // https://www.rfc-editor.org/rfc/rfc9000.html#section-13.4.1-2
+ truncated: nil,
+ }, {
s: "RESET_STREAM ID=1 Code=2 FinalSize=3",
j: `{"frame_type":"reset_stream","stream_id":1,"final_size":3}`,
f: debugFrameResetStream{
@@ -675,6 +734,7 @@
ranges: []i64range[packetNumber]{
{0, 1},
},
+ ecn: ecnCounts{1, 2, 3},
},
b: []byte{
0x03, // TYPE (i) = 0x02..0x03
diff --git a/quic/packet_parser.go b/quic/packet_parser.go
index eadf14f..265c4ae 100644
--- a/quic/packet_parser.go
+++ b/quic/packet_parser.go
@@ -157,25 +157,25 @@
// which includes both general parse failures and specific violations of frame
// constraints.

-func consumeAckFrame(frame []byte, f func(rangeIndex int, start, end packetNumber)) (largest packetNumber, ackDelay unscaledAckDelay, n int) {
+func consumeAckFrame(frame []byte, f func(rangeIndex int, start, end packetNumber)) (largest packetNumber, ackDelay unscaledAckDelay, ecn ecnCounts, n int) {
b := frame[1:] // type

largestAck, n := quicwire.ConsumeVarint(b)
if n < 0 {
- return 0, 0, -1
+ return 0, 0, ecnCounts{}, -1
}
b = b[n:]

v, n := quicwire.ConsumeVarintInt64(b)
if n < 0 {
- return 0, 0, -1
+ return 0, 0, ecnCounts{}, -1
}
b = b[n:]
ackDelay = unscaledAckDelay(v)

ackRangeCount, n := quicwire.ConsumeVarint(b)
if n < 0 {
- return 0, 0, -1
+ return 0, 0, ecnCounts{}, -1
}
b = b[n:]

@@ -183,12 +183,12 @@
for i := uint64(0); ; i++ {
rangeLen, n := quicwire.ConsumeVarint(b)
if n < 0 {
- return 0, 0, -1
+ return 0, 0, ecnCounts{}, -1
}
b = b[n:]
rangeMin := rangeMax - packetNumber(rangeLen)
if rangeMin < 0 || rangeMin > rangeMax {
- return 0, 0, -1
+ return 0, 0, ecnCounts{}, -1
}
f(int(i), rangeMin, rangeMax+1)

@@ -198,7 +198,7 @@

gap, n := quicwire.ConsumeVarint(b)
if n < 0 {
- return 0, 0, -1
+ return 0, 0, ecnCounts{}, -1
}
b = b[n:]

@@ -206,32 +206,30 @@
}

if frame[0] != frameTypeAckECN {
- return packetNumber(largestAck), ackDelay, len(frame) - len(b)
+ return packetNumber(largestAck), ackDelay, ecnCounts{}, len(frame) - len(b)
}

ect0Count, n := quicwire.ConsumeVarint(b)
if n < 0 {
- return 0, 0, -1
+ return 0, 0, ecnCounts{}, -1
}
b = b[n:]
ect1Count, n := quicwire.ConsumeVarint(b)
if n < 0 {
- return 0, 0, -1
+ return 0, 0, ecnCounts{}, -1
}
b = b[n:]
ecnCECount, n := quicwire.ConsumeVarint(b)
if n < 0 {
- return 0, 0, -1
+ return 0, 0, ecnCounts{}, -1
}
b = b[n:]

- // TODO: Make use of ECN feedback.
- // https://www.rfc-editor.org/rfc/rfc9000.html#section-19.3.2
- _ = ect0Count
- _ = ect1Count
- _ = ecnCECount
+ ecn.t0 = int(ect0Count)
+ ecn.t1 = int(ect1Count)
+ ecn.ce = int(ecnCECount)

- return packetNumber(largestAck), ackDelay, len(frame) - len(b)
+ return packetNumber(largestAck), ackDelay, ecn, len(frame) - len(b)
}

func consumeResetStreamFrame(b []byte) (id streamID, code uint64, finalSize int64, n int) {
diff --git a/quic/packet_writer.go b/quic/packet_writer.go
index 3560ebb..f446521 100644
--- a/quic/packet_writer.go
+++ b/quic/packet_writer.go
@@ -262,7 +262,7 @@
// to the peer potentially failing to receive an acknowledgement
// for an older packet during a period of high packet loss or
// reordering. This may result in unnecessary retransmissions.
-func (w *packetWriter) appendAckFrame(seen rangeset[packetNumber], delay unscaledAckDelay) (added bool) {
+func (w *packetWriter) appendAckFrame(seen rangeset[packetNumber], delay unscaledAckDelay, ecn ecnCounts) (added bool) {
if len(seen) == 0 {
return false
}
@@ -270,10 +270,20 @@
largest = uint64(seen.max())
firstRange = uint64(seen[len(seen)-1].size() - 1)
)
- if w.avail() < 1+quicwire.SizeVarint(largest)+quicwire.SizeVarint(uint64(delay))+1+quicwire.SizeVarint(firstRange) {
+ var ecnLen int
+ ackType := byte(frameTypeAck)
+ if (ecn != ecnCounts{}) {
+ // "Even if an endpoint does not set an ECT field in packets it sends,
+ // the endpoint MUST provide feedback about ECN markings it receives, if
+ // these are accessible."
+ // https://www.rfc-editor.org/rfc/rfc9000.html#section-13.4.1-2
+ ecnLen = quicwire.SizeVarint(uint64(ecn.ce)) + quicwire.SizeVarint(uint64(ecn.t0)) + quicwire.SizeVarint(uint64(ecn.t1))
+ ackType = frameTypeAckECN
+ }
+ if w.avail() < 1+quicwire.SizeVarint(largest)+quicwire.SizeVarint(uint64(delay))+1+quicwire.SizeVarint(firstRange)+ecnLen {
return false
}
- w.b = append(w.b, frameTypeAck)
+ w.b = append(w.b, ackType)
w.b = quicwire.AppendVarint(w.b, largest)
w.b = quicwire.AppendVarint(w.b, uint64(delay))
// The range count is technically a varint, but we'll reserve a single byte for it
@@ -285,7 +295,7 @@
for i := len(seen) - 2; i >= 0; i-- {
gap := uint64(seen[i+1].start - seen[i].end - 1)
size := uint64(seen[i].size() - 1)
- if w.avail() < quicwire.SizeVarint(gap)+quicwire.SizeVarint(size) || rangeCount > 62 {
+ if w.avail() < quicwire.SizeVarint(gap)+quicwire.SizeVarint(size)+ecnLen || rangeCount > 62 {
break
}
w.b = quicwire.AppendVarint(w.b, gap)
@@ -293,7 +303,12 @@
rangeCount++
}
w.b[rangeCountOff] = rangeCount
- w.sent.appendNonAckElicitingFrame(frameTypeAck)
+ if ackType == frameTypeAckECN {
+ w.b = quicwire.AppendVarint(w.b, uint64(ecn.t0))
+ w.b = quicwire.AppendVarint(w.b, uint64(ecn.t1))
+ w.b = quicwire.AppendVarint(w.b, uint64(ecn.ce))
+ }
+ w.sent.appendNonAckElicitingFrame(ackType)
w.sent.appendInt(uint64(seen.max()))
return true
}

Change information

Files:
  • M internal/quic/cmd/interop/main.go
  • M quic/acks.go
  • M quic/acks_test.go
  • M quic/conn_loss.go
  • M quic/conn_recv.go
  • M quic/conn_send.go
  • M quic/frame_debug.go
  • M quic/packet_codec_test.go
  • M quic/packet_parser.go
  • M quic/packet_writer.go
Change size: M
Delta: 10 files changed, 153 insertions(+), 45 deletions(-)
Open in Gerrit

Related details

Attention set is empty
Submit Requirements:
  • requirement is not satisfiedCode-Review
  • requirement satisfiedNo-Unresolved-Comments
  • requirement is not satisfiedReview-Enforcement
  • requirement is not satisfiedTryBots-Pass
Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. DiffyGerrit
Gerrit-MessageType: newchange
Gerrit-Project: net
Gerrit-Branch: master
Gerrit-Change-Id: I3ce5be6c536198eaa711f527402503b0567fc7a5
Gerrit-Change-Number: 712280
Gerrit-PatchSet: 1
Gerrit-Owner: Rhys Hiltner <rhys.h...@gmail.com>
unsatisfied_requirement
satisfied_requirement
open
diffy

Rhys Hiltner (Gerrit)

unread,
6:48 PM (5 hours ago) 6:48 PM
to Rhys Hiltner, goph...@pubsubhelper.golang.org, golang-co...@googlegroups.com

Rhys Hiltner voted

Auto-Submit+1
Commit-Queue+1
Open in Gerrit

Related details

Attention set is empty
Submit Requirements:
  • requirement is not satisfiedCode-Review
  • requirement satisfiedNo-Unresolved-Comments
  • requirement is not satisfiedReview-Enforcement
  • requirement is not satisfiedTryBots-Pass
Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. DiffyGerrit
Gerrit-MessageType: comment
Gerrit-Project: net
Gerrit-Branch: master
Gerrit-Change-Id: I3ce5be6c536198eaa711f527402503b0567fc7a5
Gerrit-Change-Number: 712280
Gerrit-PatchSet: 1
Gerrit-Owner: Rhys Hiltner <rhys.h...@gmail.com>
Gerrit-Reviewer: Rhys Hiltner <rhys.h...@gmail.com>
Gerrit-Comment-Date: Wed, 15 Oct 2025 22:48:35 +0000
Gerrit-HasComments: No
Gerrit-Has-Labels: Yes
unsatisfied_requirement
satisfied_requirement
open
diffy

Damien Neil (Gerrit)

unread,
7:33 PM (4 hours ago) 7:33 PM
to Rhys Hiltner, goph...@pubsubhelper.golang.org, Ian Lance Taylor, Gopher Robot, Go LUCI, golang-co...@googlegroups.com
Attention needed from Ian Lance Taylor and Rhys Hiltner

Damien Neil voted and added 1 comment

Votes added by Damien Neil

Auto-Submit+1
Code-Review+2

1 comment

Patchset-level comments
File-level comment, Patchset 1 (Latest):
Damien Neil . resolved

This is an extremely clean CL. Thank you!

Open in Gerrit

Related details

Attention is currently required from:
  • Ian Lance Taylor
  • Rhys Hiltner
Submit Requirements:
  • requirement satisfiedCode-Review
  • requirement satisfiedNo-Unresolved-Comments
  • requirement is not satisfiedReview-Enforcement
  • requirement satisfiedTryBots-Pass
Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. DiffyGerrit
Gerrit-MessageType: comment
Gerrit-Project: net
Gerrit-Branch: master
Gerrit-Change-Id: I3ce5be6c536198eaa711f527402503b0567fc7a5
Gerrit-Change-Number: 712280
Gerrit-PatchSet: 1
Gerrit-Owner: Rhys Hiltner <rhys.h...@gmail.com>
Gerrit-Reviewer: Damien Neil <dn...@google.com>
Gerrit-Reviewer: Ian Lance Taylor <ia...@golang.org>
Gerrit-Reviewer: Rhys Hiltner <rhys.h...@gmail.com>
Gerrit-CC: Gopher Robot <go...@golang.org>
Gerrit-Attention: Ian Lance Taylor <ia...@golang.org>
Gerrit-Attention: Rhys Hiltner <rhys.h...@gmail.com>
Gerrit-Comment-Date: Wed, 15 Oct 2025 23:33:23 +0000
Gerrit-HasComments: Yes
Gerrit-Has-Labels: Yes
satisfied_requirement
unsatisfied_requirement
open
diffy
Reply all
Reply to author
Forward
0 new messages