Skip to content

Commit cf41689

Browse files
kevink-sqadrw
andauthored
feat: reattempt in introducing configurable http/2 control frame limiter with observability (#3422)
* feat: reattempt in introducing configurable http/2 control frame limiter with observability * run apiDump * remove make Endpoint nullable * Fix issue where multiple instance of the counter metric results in silently failed IllegalArgumentException --------- Co-authored-by: Andrew (Paradi) Alexander <[email protected]>
1 parent 492e062 commit cf41689

File tree

5 files changed

+76
-24
lines changed

5 files changed

+76
-24
lines changed

misk/api/misk.api

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2162,6 +2162,16 @@ public final class misk/web/jetty/MeasuredThreadPoolExecutor : misk/web/jetty/Me
21622162
public fun queueSize ()I
21632163
}
21642164

2165+
public final class misk/web/jetty/MeasuredWindowRateControl : org/eclipse/jetty/http2/parser/RateControl {
2166+
public synthetic fun <init> (ILmisk/metrics/v2/PeakGauge;Lio/prometheus/client/Counter;Lkotlin/jvm/internal/DefaultConstructorMarker;)V
2167+
public fun onEvent (Ljava/lang/Object;)Z
2168+
}
2169+
2170+
public final class misk/web/jetty/MeasuredWindowRateControl$Factory : org/eclipse/jetty/http2/parser/RateControl$Factory {
2171+
public fun <init> (Lmisk/metrics/v2/Metrics;Lmisk/web/WebConfig;)V
2172+
public fun newRateControl (Lorg/eclipse/jetty/io/EndPoint;)Lorg/eclipse/jetty/http2/parser/RateControl;
2173+
}
2174+
21652175
public final class misk/web/jetty/ThreadPoolQueueMetrics {
21662176
public final fun recordQueueLatency (Ljava/time/Duration;)V
21672177
}

misk/build.gradle.kts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ dependencies {
1515
api(libs.jacksonDatabind)
1616
api(libs.jakartaInject)
1717
api(libs.jakartaInject)
18+
api(libs.jettyHttp2Common)
19+
api(libs.jettyIo)
1820
api(libs.jettyServer)
1921
api(libs.jettyServletApi)
2022
api(libs.jettyUtil)
@@ -44,8 +46,6 @@ dependencies {
4446
implementation(libs.jettyAlpnServer)
4547
implementation(libs.jettyHttp)
4648
implementation(libs.jettyHttp2)
47-
implementation(libs.jettyHttp2Common)
48-
implementation(libs.jettyIo)
4949
implementation(libs.jettyServlet)
5050
implementation(libs.jettyServlets)
5151
implementation(libs.jettyUds)
@@ -87,6 +87,7 @@ dependencies {
8787
testImplementation(libs.kotlinxCoroutinesCore)
8888
testImplementation(libs.kotlinxCoroutinesTest)
8989
testImplementation(libs.logbackClassic)
90+
testImplementation(libs.mockitoCore)
9091
testImplementation(libs.okHttpMockWebServer)
9192
testImplementation(libs.okHttpSse)
9293
testImplementation(libs.openTracingMock)

misk/src/main/kotlin/misk/web/jetty/JettyService.kt

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ class JettyService @Inject internal constructor(
6868
private val connectionMetricsCollector: JettyConnectionMetricsCollector,
6969
private val statisticsHandler: StatisticsHandler,
7070
private val gzipHandler: GzipHandler,
71+
private val http2RateControlFactory: MeasuredWindowRateControl.Factory
7172
) : AbstractIdleService() {
7273
private val server = Server(threadPool)
7374
val healthServerUrl: HttpUrl? get() = server.healthUrl
@@ -128,6 +129,7 @@ class JettyService @Inject internal constructor(
128129
if (webConfig.http2) {
129130
val http2 = HTTP2CServerConnectionFactory(httpConfig)
130131
http2.customize(webConfig)
132+
http2.rateControlFactory = http2RateControlFactory
131133
httpConnectionFactories += http2
132134
}
133135

@@ -212,6 +214,7 @@ class JettyService @Inject internal constructor(
212214
if (webConfig.http2) {
213215
val http2 = HTTP2ServerConnectionFactory(httpsConfig)
214216
http2.customize(webConfig)
217+
http2.rateControlFactory = http2RateControlFactory
215218
httpsConnectionFactories += http2
216219
}
217220

@@ -255,7 +258,9 @@ class JettyService @Inject internal constructor(
255258
val udsConnFactories = mutableListOf<ConnectionFactory>()
256259
udsConnFactories.add(HttpConnectionFactory(httpConfig))
257260
if (socketConfig.h2c == true) {
258-
udsConnFactories.add(HTTP2CServerConnectionFactory(httpConfig))
261+
val http2 = HTTP2CServerConnectionFactory(httpConfig)
262+
http2.rateControlFactory = http2RateControlFactory
263+
udsConnFactories.add(http2)
259264
}
260265

261266
if (isJEP380Supported(socketConfig.path)) {

misk/src/main/kotlin/misk/web/jetty/MeasuredWindowRateControl.kt

Lines changed: 32 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,12 @@
1212
//
1313
package misk.web.jetty
1414

15+
import io.prometheus.client.Counter
16+
import jakarta.inject.Inject
17+
import jakarta.inject.Singleton
1518
import misk.metrics.v2.Metrics
19+
import misk.metrics.v2.PeakGauge
20+
import misk.web.WebConfig
1621
import org.eclipse.jetty.http2.parser.RateControl
1722
import org.eclipse.jetty.io.EndPoint
1823
import org.eclipse.jetty.util.NanoTime
@@ -24,19 +29,12 @@ import java.util.concurrent.atomic.AtomicInteger
2429
* Misk's RateControl implementation with observability for monitoring HTTP/2 frame rate limiting.
2530
* Almost the same implementation as [org.eclipse.jetty.http2.parser.WindowRateControl].
2631
*/
27-
internal class MeasuredWindowRateControl internal constructor(
28-
private val metrics: Metrics,
32+
class MeasuredWindowRateControl private constructor(
2933
private val maxEvents: Int,
34+
private val rateEventsPeakGauge: PeakGauge,
35+
private val rateLimitedEventCounter: Counter,
3036
) : RateControl {
3137

32-
private val rateEventsPeakGauge = metrics.peakGauge(
33-
"jetty_http2_rate_control_events_peak",
34-
"Peak gauge of observed events per second"
35-
)
36-
private val rateLimitedEventCounter = metrics.counter(
37-
"jetty_http2_rate_control_events_limited",
38-
"Count of rate limited events"
39-
)
4038

4139
private val events = ConcurrentLinkedQueue<Long>()
4240
private val size = AtomicInteger()
@@ -55,20 +53,37 @@ internal class MeasuredWindowRateControl internal constructor(
5553

5654
val count = size.incrementAndGet()
5755
rateEventsPeakGauge.record(count.toDouble())
58-
5956
if (maxEvents == -1) return true
6057

6158
val allowed = count <= maxEvents
6259
if (!allowed) rateLimitedEventCounter.inc()
6360
return allowed
6461
}
6562

66-
class Factory constructor(
67-
private val metrics: Metrics,
68-
private val maxEventRate: Int
63+
/**
64+
* Ensure the factory remains a singleton to prevent
65+
* multiple instantiations of the same metric objects
66+
*/
67+
@Singleton
68+
class Factory @Inject constructor(
69+
metrics: Metrics,
70+
private val webConfig: WebConfig,
6971
) : RateControl.Factory {
70-
override fun newRateControl(endPoint: EndPoint): RateControl {
71-
return MeasuredWindowRateControl(metrics, maxEventRate)
72+
73+
private val rateEventsPeakGauge = metrics.peakGauge(
74+
"jetty_http2_rate_control_events_peak",
75+
"Peak gauge of observed events per second"
76+
)
77+
private val rateLimitedEventCounter = metrics.counter(
78+
"jetty_http2_rate_control_events_limited",
79+
"Count of rate limited events"
80+
)
81+
82+
override fun newRateControl(endPoint: EndPoint?): RateControl {
83+
return MeasuredWindowRateControl(
84+
webConfig.jetty_http2_max_events_per_second,
85+
rateEventsPeakGauge,
86+
rateLimitedEventCounter)
7287
}
7388
}
74-
}
89+
}

misk/src/test/kotlin/misk/web/jetty/MeasuredWindowRateControlTest.kt

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,31 @@ import misk.metrics.v2.FakeMetrics
33
import misk.metrics.v2.FakeMetricsModule
44
import misk.testing.MiskTest
55
import misk.testing.MiskTestModule
6+
import misk.web.WebConfig
67
import misk.web.jetty.MeasuredWindowRateControl
78
import org.assertj.core.api.Assertions.assertThat
89
import org.junit.jupiter.api.Test
10+
import org.mockito.Mockito.mock
11+
import org.mockito.Mockito.`when`
912

1013
@MiskTest(startService = false)
1114
class MeasuredWindowRateControlTest {
1215

1316
@MiskTestModule val module = FakeMetricsModule()
1417
@Inject lateinit var metrics: FakeMetrics
1518

19+
fun maxEventRate(n : Int) : WebConfig {
20+
val webConfig = mock(WebConfig::class.java)
21+
`when`(webConfig.jetty_http2_max_events_per_second).thenReturn(n)
22+
return webConfig
23+
}
24+
1625
@Test
1726
fun `allows events when under maxEvents limit`() {
18-
val rateControl = MeasuredWindowRateControl(metrics, maxEvents = 5)
27+
val rateControl = MeasuredWindowRateControl.Factory(
28+
metrics,
29+
maxEventRate(5)
30+
).newRateControl(null)
1931

2032
repeat(5) {
2133
assertThat(rateControl.onEvent("test")).isTrue()
@@ -24,7 +36,10 @@ class MeasuredWindowRateControlTest {
2436

2537
@Test
2638
fun `blocks events when over maxEvents limit`() {
27-
val rateControl = MeasuredWindowRateControl(metrics, maxEvents = 2)
39+
val rateControl = MeasuredWindowRateControl.Factory(
40+
metrics,
41+
maxEventRate(2)
42+
).newRateControl(null)
2843

2944
assertThat(rateControl.onEvent("test1")).isTrue()
3045
assertThat(rateControl.onEvent("test2")).isTrue()
@@ -34,7 +49,10 @@ class MeasuredWindowRateControlTest {
3449

3550
@Test
3651
fun `allows unlimited events when maxEvents is -1`() {
37-
val rateControl = MeasuredWindowRateControl(metrics, maxEvents = -1)
52+
val rateControl = MeasuredWindowRateControl.Factory(
53+
metrics,
54+
maxEventRate(-1)
55+
).newRateControl(null)
3856

3957
repeat(100) {
4058
assertThat(rateControl.onEvent("test")).isTrue()
@@ -43,7 +61,10 @@ class MeasuredWindowRateControlTest {
4361

4462
@Test
4563
fun `window sliding allows events after time passes`() {
46-
val rateControl = MeasuredWindowRateControl(metrics, maxEvents = 2)
64+
val rateControl = MeasuredWindowRateControl.Factory(
65+
metrics,
66+
maxEventRate(2)
67+
).newRateControl(null)
4768

4869
assertThat(rateControl.onEvent("test1")).isTrue()
4970
assertThat(rateControl.onEvent("test2")).isTrue()

0 commit comments

Comments
 (0)