Skip to content

Commit 11e53f4

Browse files
committed
Improved HTTP2Connection dump information
Signed-off-by: znight1020 <[email protected]>
1 parent 7e4a9be commit 11e53f4

File tree

3 files changed

+273
-2
lines changed

3 files changed

+273
-2
lines changed

jetty-core/jetty-http2/jetty-http2-common/src/main/java/org/eclipse/jetty/http2/HTTP2Connection.java

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,14 +41,15 @@
4141
import org.eclipse.jetty.util.BufferUtil;
4242
import org.eclipse.jetty.util.Callback;
4343
import org.eclipse.jetty.util.TypeUtil;
44+
import org.eclipse.jetty.util.component.Dumpable;
4445
import org.eclipse.jetty.util.component.LifeCycle;
4546
import org.eclipse.jetty.util.thread.AutoLock;
4647
import org.eclipse.jetty.util.thread.ExecutionStrategy;
4748
import org.eclipse.jetty.util.thread.strategy.AdaptiveExecutionStrategy;
4849
import org.slf4j.Logger;
4950
import org.slf4j.LoggerFactory;
5051

51-
public class HTTP2Connection extends AbstractConnection implements Parser.Listener, Connection.UpgradeTo
52+
public class HTTP2Connection extends AbstractConnection implements Parser.Listener, Connection.UpgradeTo, Dumpable
5253
{
5354
private static final Logger LOG = LoggerFactory.getLogger(HTTP2Connection.class);
5455

@@ -333,6 +334,12 @@ public String toConnectionString()
333334
return "%s@%x[%s]".formatted(TypeUtil.toShortName(getClass()), hashCode(), strategy);
334335
}
335336

337+
@Override
338+
public void dump(Appendable out, String indent) throws IOException
339+
{
340+
Dumpable.dumpObjects(out, indent, this, session);
341+
}
342+
336343
protected class HTTP2Producer implements ExecutionStrategy.Producer
337344
{
338345
private static final RetainableByteBuffer.Mutable STOPPED = new RetainableByteBuffer.NonRetainableByteBuffer(BufferUtil.EMPTY_BUFFER);

jetty-core/jetty-http2/jetty-http2-common/src/main/java/org/eclipse/jetty/http2/HTTP2Session.java

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import java.util.Collection;
2424
import java.util.Collections;
2525
import java.util.EventListener;
26+
import java.util.HashMap;
2627
import java.util.Iterator;
2728
import java.util.List;
2829
import java.util.Map;
@@ -34,6 +35,7 @@
3435
import java.util.concurrent.TimeoutException;
3536
import java.util.concurrent.atomic.AtomicInteger;
3637
import java.util.concurrent.atomic.AtomicLong;
38+
import java.util.concurrent.atomic.AtomicReference;
3739
import java.util.function.BiConsumer;
3840
import java.util.function.Consumer;
3941
import java.util.function.Predicate;
@@ -77,6 +79,7 @@
7779
import org.eclipse.jetty.util.component.AbstractLifeCycle;
7880
import org.eclipse.jetty.util.component.Dumpable;
7981
import org.eclipse.jetty.util.component.DumpableCollection;
82+
import org.eclipse.jetty.util.component.DumpableMap;
8083
import org.eclipse.jetty.util.thread.AutoLock;
8184
import org.eclipse.jetty.util.thread.Invocable;
8285
import org.eclipse.jetty.util.thread.Scheduler;
@@ -94,6 +97,8 @@ public abstract class HTTP2Session extends AbstractLifeCycle implements Session,
9497
private final Set<Integer> priorityStreams = ConcurrentHashMap.newKeySet();
9598
private final List<FrameListener> frameListeners = new CopyOnWriteArrayList<>();
9699
private final List<LifeCycleListener> lifeCycleListeners = new CopyOnWriteArrayList<>();
100+
private final AtomicReference<Map<Integer, Integer>> localSettingsSnapshot = new AtomicReference<>(Map.of());
101+
private final AtomicReference<Map<Integer, Integer>> remoteSettingsSnapshot = new AtomicReference<>(Map.of());
97102
private final AtomicLong streamsOpened = new AtomicLong();
98103
private final AtomicLong streamsClosed = new AtomicLong();
99104
private final StreamsState streamsState = new StreamsState();
@@ -299,6 +304,26 @@ public long getBytesWritten()
299304
return bytesWritten.get();
300305
}
301306

307+
/**
308+
* Returns a snapshot of the last SETTINGS that this endpoint sent
309+
* <p>The map keys are SETTINGS ids as defined in {@link SettingsFrame}.
310+
* @return local (sent) SETTINGS
311+
*/
312+
public Map<Integer,Integer> getCurrentLocalSettings()
313+
{
314+
return localSettingsSnapshot.get();
315+
}
316+
317+
/**
318+
* Returns a snapshot of the last SETTINGS that the peer sent
319+
* <p>The map keys are SETTINGS ids as defined in {@link SettingsFrame}.
320+
* @return remote (received) SETTINGS
321+
*/
322+
public Map<Integer,Integer> getCurrentRemoteSettings()
323+
{
324+
return remoteSettingsSnapshot.get();
325+
}
326+
302327
@Override
303328
public void onData(DataFrame frame)
304329
{
@@ -445,6 +470,14 @@ public void onSettings(SettingsFrame frame, boolean reply)
445470
return;
446471

447472
Map<Integer, Integer> settings = frame.getSettings();
473+
remoteSettingsSnapshot.updateAndGet(origin -> {
474+
if (settings.isEmpty())
475+
return origin;
476+
Map<Integer, Integer> updated = new HashMap<>(origin);
477+
updated.putAll(settings);
478+
return Map.copyOf(updated);
479+
});
480+
448481
configure(settings, false);
449482
notifySettings(this, frame);
450483

@@ -811,6 +844,17 @@ public void succeeded(Stream pushed)
811844
@Override
812845
public void settings(SettingsFrame frame, Callback callback)
813846
{
847+
if (!frame.isReply())
848+
{
849+
Map<Integer, Integer> settings = frame.getSettings();
850+
localSettingsSnapshot.updateAndGet(origin -> {
851+
if (settings.isEmpty())
852+
return origin;
853+
Map<Integer, Integer> updated = new HashMap<>(origin);
854+
updated.putAll(settings);
855+
return Map.copyOf(updated);
856+
});
857+
}
814858
control(null, callback, frame);
815859
}
816860

@@ -1442,7 +1486,9 @@ protected static boolean isClientStream(int streamId)
14421486
@Override
14431487
public void dump(Appendable out, String indent) throws IOException
14441488
{
1445-
Dumpable.dumpObjects(out, indent, this, flowControl, flusher, new DumpableCollection("streams", streams.values()));
1489+
DumpableMap localSettings = new DumpableMap("local settings", getCurrentLocalSettings());
1490+
DumpableMap remoteSettings = new DumpableMap("remote settings", getCurrentRemoteSettings());
1491+
Dumpable.dumpObjects(out, indent, this, flowControl, flusher, new DumpableCollection("streams", streams.values()), localSettings, remoteSettings);
14461492
}
14471493

14481494
@Override
Lines changed: 218 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,218 @@
1+
//
2+
// ========================================================================
3+
// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
4+
//
5+
// This program and the accompanying materials are made available under the
6+
// terms of the Eclipse Public License v. 2.0 which is available at
7+
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
8+
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
9+
//
10+
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
11+
// ========================================================================
12+
//
13+
14+
package org.eclipse.jetty.http2.tests;
15+
16+
import java.io.IOException;
17+
import java.io.OutputStream;
18+
import java.net.Socket;
19+
import java.util.Collections;
20+
import java.util.Map;
21+
22+
import org.eclipse.jetty.http2.api.Session;
23+
import org.eclipse.jetty.http2.api.server.ServerSessionListener;
24+
import org.eclipse.jetty.http2.frames.PrefaceFrame;
25+
import org.eclipse.jetty.http2.frames.SettingsFrame;
26+
import org.eclipse.jetty.http2.parser.Parser;
27+
import org.eclipse.jetty.io.Connection;
28+
import org.eclipse.jetty.io.Content;
29+
import org.eclipse.jetty.io.EndPoint;
30+
import org.eclipse.jetty.io.RetainableByteBuffer;
31+
import org.eclipse.jetty.util.Callback;
32+
import org.eclipse.jetty.util.component.Dumpable;
33+
import org.junit.jupiter.api.BeforeEach;
34+
import org.junit.jupiter.api.Test;
35+
36+
import static org.junit.jupiter.api.Assertions.assertNotNull;
37+
import static org.junit.jupiter.api.Assertions.assertTrue;
38+
39+
public class HTTP2ConnectionDumpTest extends AbstractServerTest
40+
{
41+
@BeforeEach
42+
public void setUp() throws Exception
43+
{
44+
startServer(new ServerSessionListener()
45+
{
46+
@Override
47+
public void onAccept(Session session)
48+
{
49+
session.settings(new SettingsFrame(Map.of(
50+
SettingsFrame.MAX_CONCURRENT_STREAMS, 124,
51+
SettingsFrame.MAX_FRAME_SIZE, 32768
52+
), false), Callback.NOOP);
53+
}
54+
});
55+
}
56+
57+
@Test
58+
public void testDumpSettingsLocalAndRemote() throws Exception
59+
{
60+
try (Socket client = new Socket("localhost", connector.getLocalPort()))
61+
{
62+
client.setTcpNoDelay(true);
63+
client.setSoTimeout(5000);
64+
65+
// Client preface + SETTINGS(3=128)
66+
OutputStream output = client.getOutputStream();
67+
RetainableByteBuffer.Mutable accumulator = new RetainableByteBuffer.DynamicCapacity();
68+
generator.control(accumulator, new PrefaceFrame());
69+
generator.control(accumulator, new SettingsFrame(Map.of(SettingsFrame.MAX_CONCURRENT_STREAMS, 128), false));
70+
accumulator.writeTo(Content.Sink.from(output), false);
71+
output.flush();
72+
73+
Parser parser = new Parser(bufferPool, 8192);
74+
parser.init(new Parser.Listener() {});
75+
parseResponse(client, parser, 1000);
76+
}
77+
78+
String dump = getConnectionDump();
79+
assertNotNull(dump);
80+
81+
assertTrue(dump.contains("local settings"), dump);
82+
assertTrue(dump.contains("size=2"), dump);
83+
assertTrue(dump.contains("+> 3: 124"), dump);
84+
assertTrue(dump.contains("+> 5: 32768"), dump);
85+
86+
assertTrue(dump.contains("remote settings"), dump);
87+
assertTrue(dump.contains("size=1"), dump);
88+
assertTrue(dump.contains("+> 3: 128"), dump);
89+
}
90+
91+
@Test
92+
public void testDumpSettingsUpdate() throws Exception
93+
{
94+
try (Socket client = new Socket("localhost", connector.getLocalPort()))
95+
{
96+
client.setTcpNoDelay(true);
97+
client.setSoTimeout(5000);
98+
99+
OutputStream output = client.getOutputStream();
100+
Parser parser = new Parser(bufferPool, 8192);
101+
parser.init(new Parser.Listener() {});
102+
103+
RetainableByteBuffer.Mutable accumulator1 = new RetainableByteBuffer.DynamicCapacity();
104+
generator.control(accumulator1, new PrefaceFrame());
105+
generator.control(accumulator1, new SettingsFrame(Map.of(SettingsFrame.MAX_CONCURRENT_STREAMS, 128), false));
106+
accumulator1.writeTo(Content.Sink.from(output), false);
107+
output.flush();
108+
parseResponse(client, parser, 1000);
109+
110+
RetainableByteBuffer.Mutable accumulator2 = new RetainableByteBuffer.DynamicCapacity();
111+
generator.control(accumulator2, new SettingsFrame(Map.of(
112+
SettingsFrame.ENABLE_CONNECT_PROTOCOL, 1
113+
), false));
114+
accumulator2.writeTo(Content.Sink.from(output), false);
115+
output.flush();
116+
parseResponse(client, parser, 1000);
117+
}
118+
119+
String dump = getConnectionDump();
120+
assertNotNull(dump);
121+
122+
// New params are added and existing ones preserved.
123+
assertTrue(dump.contains("remote settings size=2"), dump);
124+
assertTrue(dump.contains("+> 3: 128"), dump);
125+
assertTrue(dump.contains("+> 8: 1"), dump);
126+
}
127+
128+
@Test
129+
public void testDumpSettingsIgnoreEmpty() throws Exception
130+
{
131+
try (Socket client = new Socket("localhost", connector.getLocalPort()))
132+
{
133+
client.setTcpNoDelay(true);
134+
client.setSoTimeout(5000);
135+
136+
OutputStream output = client.getOutputStream();
137+
Parser parser = new Parser(bufferPool, 8192);
138+
parser.init(new Parser.Listener() {});
139+
140+
RetainableByteBuffer.Mutable acc1 = new RetainableByteBuffer.DynamicCapacity();
141+
generator.control(acc1, new PrefaceFrame());
142+
generator.control(acc1, new SettingsFrame(Map.of(SettingsFrame.MAX_CONCURRENT_STREAMS, 128), false));
143+
acc1.writeTo(Content.Sink.from(output), false);
144+
output.flush();
145+
parseResponse(client, parser, 1000);
146+
147+
// Send empty SETTINGS (no-op).
148+
RetainableByteBuffer.Mutable acc2 = new RetainableByteBuffer.DynamicCapacity();
149+
generator.control(acc2, new SettingsFrame(Collections.emptyMap(), false));
150+
acc2.writeTo(Content.Sink.from(output), false);
151+
output.flush();
152+
parseResponse(client, parser, 1000);
153+
}
154+
155+
String dump = getConnectionDump();
156+
assertNotNull(dump);
157+
158+
// Unchanged local, remote settings.
159+
assertTrue(dump.contains("local settings size=2"), dump);
160+
assertTrue(dump.contains("+> 3: 124"), dump);
161+
assertTrue(dump.contains("+> 5: 32768"), dump);
162+
assertTrue(dump.contains("remote settings size=1"), dump);
163+
assertTrue(dump.contains("+> 3: 128"), dump);
164+
}
165+
166+
@Test
167+
public void testDumpSettingsIgnoreAck() throws Exception
168+
{
169+
try (Socket client = new Socket("localhost", connector.getLocalPort()))
170+
{
171+
client.setTcpNoDelay(true);
172+
client.setSoTimeout(5000);
173+
174+
OutputStream output = client.getOutputStream();
175+
RetainableByteBuffer.Mutable accumulator1 = new RetainableByteBuffer.DynamicCapacity();
176+
generator.control(accumulator1, new PrefaceFrame());
177+
generator.control(accumulator1, new SettingsFrame(Map.of(SettingsFrame.MAX_CONCURRENT_STREAMS, 128), false));
178+
accumulator1.writeTo(Content.Sink.from(output), false);
179+
output.flush();
180+
181+
Parser parser = new Parser(bufferPool, 8192);
182+
parser.init(new Parser.Listener() {});
183+
parseResponse(client, parser, 1000);
184+
185+
// Sent ACK, remote to local
186+
RetainableByteBuffer.Mutable accumulator2 = new RetainableByteBuffer.DynamicCapacity();
187+
generator.control(accumulator2, new SettingsFrame(Collections.emptyMap(), true));
188+
accumulator2.writeTo(Content.Sink.from(output), false);
189+
output.flush();
190+
parseResponse(client, parser, 1000);
191+
}
192+
193+
String dump = getConnectionDump();
194+
assertNotNull(dump);
195+
196+
// Unchanged local, remote settings.
197+
assertTrue(dump.contains("local settings size=2"), dump);
198+
assertTrue(dump.contains("+> 3: 124"), dump);
199+
assertTrue(dump.contains("+> 5: 32768"), dump);
200+
assertTrue(dump.contains("remote settings size=1"), dump);
201+
assertTrue(dump.contains("+> 3: 128"), dump);
202+
}
203+
204+
private String getConnectionDump() throws IOException
205+
{
206+
for (EndPoint ep : connector.getConnectedEndPoints())
207+
{
208+
Connection conn = ep.getConnection();
209+
if (conn instanceof Dumpable d)
210+
{
211+
StringBuilder sb = new StringBuilder();
212+
d.dump(sb, "");
213+
return sb.toString();
214+
}
215+
}
216+
return null;
217+
}
218+
}

0 commit comments

Comments
 (0)