Skip to content

Commit bc815d5

Browse files
jcrossleyjenkins
authored and
jenkins
committed
finagle-core: Ability to configure StatsFilter with a HistogramCounterFactory to track request burstiness
Problem While we have a counter for the number of requests, we're limited by the frequency of metrics collection to see how these requests are spread out; that is, how "bursty" they are. Solution Introduce a HistogramCounter, created via a HistogramCounterFactory, that can be used to track request burstiness. The factory is configured on a client/server, and, if configured, is used in StatsFilter to track request burstiness. Differential Revision: https://phabricator.twitter.biz/D1180751
1 parent 05f5618 commit bc815d5

File tree

4 files changed

+192
-3
lines changed

4 files changed

+192
-3
lines changed

finagle-core/src/main/scala/com/twitter/finagle/param/Params.scala

+12
Original file line numberDiff line numberDiff line change
@@ -291,3 +291,15 @@ object ExceptionStatsHandler {
291291
lazy val default = ExceptionStatsHandler(StatsFilter.DefaultExceptions)
292292
}
293293
}
294+
295+
/**
296+
* A class eligible for configuring a
297+
* [[com.twitter.finagle.stats.HistogramCounterFactory]] throughout finagle servers
298+
* and clients.
299+
*/
300+
private[twitter] case class HistogramCounterFactory(
301+
histogramCounterFactoryOpt: Option[stats.HistogramCounterFactory])
302+
object HistogramCounterFactory {
303+
implicit val param: Stack.Param[HistogramCounterFactory] =
304+
Stack.Param(HistogramCounterFactory(None))
305+
}

finagle-core/src/main/scala/com/twitter/finagle/service/StatsFilter.scala

+26-3
Original file line numberDiff line numberDiff line change
@@ -45,14 +45,15 @@ object StatsFilter {
4545
* Creates a [[com.twitter.finagle.Stackable]] [[com.twitter.finagle.service.StatsFilter]].
4646
*/
4747
def module[Req, Rep]: Stackable[ServiceFactory[Req, Rep]] =
48-
new Stack.Module7[
48+
new Stack.Module8[
4949
param.Stats,
5050
param.ExceptionStatsHandler,
5151
param.ResponseClassifier,
5252
Param,
5353
Now,
5454
param.MetricBuilders,
5555
param.StandardStats,
56+
param.HistogramCounterFactory,
5657
ServiceFactory[Req, Rep]
5758
] {
5859
val role: Stack.Role = StatsFilter.role
@@ -65,6 +66,7 @@ object StatsFilter {
6566
now: Now,
6667
metrics: param.MetricBuilders,
6768
standardStats: param.StandardStats,
69+
histogramCounterFactory: param.HistogramCounterFactory,
6870
next: ServiceFactory[Req, Rep]
6971
): ServiceFactory[Req, Rep] = {
7072
val param.Stats(statsReceiver) = _stats
@@ -78,7 +80,8 @@ object StatsFilter {
7880
_param.unit,
7981
now.nowOrDefault(_param.unit),
8082
metrics.registry,
81-
standardStats.standardStats
83+
standardStats.standardStats,
84+
histogramCounterFactory.histogramCounterFactoryOpt
8285
).andThen(next)
8386
}
8487
}
@@ -163,6 +166,10 @@ object StatsFilter {
163166
*
164167
* @param metricsRegistry an optional [MetricBuilderRegistry] set by stack parameter
165168
* for injecting metrics and instrumenting top-line expressions
169+
*
170+
* @param histogramCounterFactoryOpt an optional [HistogramCounterFactory] that, if present, will
171+
* be used to record a stat for requests received over a 100ms
172+
* period. This can be used to see "burstiness" of requests.
166173
*/
167174
class StatsFilter[Req, Rep] private[service] (
168175
statsReceiver: StatsReceiver,
@@ -171,7 +178,8 @@ class StatsFilter[Req, Rep] private[service] (
171178
timeUnit: TimeUnit,
172179
now: () => Long,
173180
metricsRegistry: Option[CoreMetricsRegistry] = None,
174-
standardStats: StandardStats = Disabled)
181+
standardStats: StandardStats = Disabled,
182+
histogramCounterFactoryOpt: Option[HistogramCounterFactory] = None)
175183
extends SimpleFilter[Req, Rep] {
176184

177185
/**
@@ -278,6 +286,15 @@ class StatsFilter[Req, Rep] private[service] (
278286
statsReceiver.addGauge(Descriptions.pending, "pending") {
279287
outstandingRequestCount.sum()
280288
}
289+
private[this] val requestsHistogramCounterOpt = histogramCounterFactoryOpt match {
290+
case Some(histogramCounterFactory) =>
291+
Some(
292+
histogramCounterFactory(
293+
Seq("requests"),
294+
StatsFrequency.HundredMilliSecondly,
295+
statsReceiver))
296+
case None => None
297+
}
281298

282299
private[this] def isIgnorableResponse(rep: Try[Rep]): Boolean = rep match {
283300
case Throw(f: FailureFlags[_]) => f.isFlagged(FailureFlags.Ignorable)
@@ -302,6 +319,12 @@ class StatsFilter[Req, Rep] private[service] (
302319
stats.recordStats(request, response, duration)
303320
case None => // no-op
304321
}
322+
323+
requestsHistogramCounterOpt match {
324+
case Some(requestsHistogramCounter) =>
325+
requestsHistogramCounter.incr()
326+
case None => // no-op
327+
}
305328
}
306329
}
307330
}

finagle-core/src/test/scala/com/twitter/finagle/service/StatsFilterTest.scala

+36
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,42 @@ class StatsFilterTest extends AnyFunSuite {
116116
}
117117
}
118118

119+
test("requests histogram counter stat") {
120+
Time.withCurrentTimeFrozen { tc =>
121+
val timer = new MockTimer
122+
val sr = new InMemoryStatsReceiver()
123+
val histogramCounterFactory = new HistogramCounterFactory(timer, () => Time.now.inMillis)
124+
val filter =
125+
new StatsFilter[String, String](
126+
sr,
127+
ResponseClassifier.Default,
128+
StatsFilter.DefaultExceptions,
129+
TimeUnit.SECONDS,
130+
() => Time.now.inSeconds,
131+
histogramCounterFactoryOpt = Some(histogramCounterFactory)
132+
)
133+
var promise = new Promise[String]
134+
val svc = filter.andThen(new Service[String, String] {
135+
def apply(request: String): Promise[String] = promise
136+
})
137+
svc("1")
138+
svc("1")
139+
svc("1")
140+
promise.setValue("done")
141+
tc.advance(100.millis)
142+
timer.tick()
143+
144+
promise = new Promise[String]
145+
svc("1")
146+
svc("1")
147+
promise.setValue("done")
148+
tc.advance(100.millis)
149+
timer.tick()
150+
151+
assert(sr.stats(Seq("requests", "hundredMilliSecondly")) == Seq(3, 2))
152+
}
153+
}
154+
119155
test("report exceptions") {
120156
val (promise, receiver, statsService) = getService()
121157

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
package com.twitter.finagle.stats
2+
3+
import com.twitter.conversions.DurationOps._
4+
import com.twitter.util.MockTimer
5+
import com.twitter.util.Time
6+
import org.scalatest.funsuite.AnyFunSuite
7+
8+
class HistogramCounterTest extends AnyFunSuite {
9+
10+
test("Records stat at given frequency") {
11+
Time.withCurrentTimeFrozen { tc =>
12+
val timer = new MockTimer()
13+
val statsReceiver = new InMemoryStatsReceiver
14+
val histogramCounterFactory = new HistogramCounterFactory(timer, () => Time.now.inMillis)
15+
val histogramCounter =
16+
histogramCounterFactory(
17+
Seq("foo", "bar"),
18+
StatsFrequency.HundredMilliSecondly,
19+
statsReceiver
20+
)
21+
histogramCounter.incr(5)
22+
assert(statsReceiver.stats(Seq("foo", "bar", "hundredMilliSecondly")).isEmpty)
23+
histogramCounter.incr()
24+
25+
tc.advance(100.millis)
26+
timer.tick()
27+
28+
assert(statsReceiver.stats(Seq("foo", "bar", "hundredMilliSecondly")) == Seq(6))
29+
30+
tc.advance(100.millis)
31+
timer.tick()
32+
33+
assert(statsReceiver.stats(Seq("foo", "bar", "hundredMilliSecondly")) == Seq(6, 0))
34+
35+
histogramCounter.incr(2)
36+
37+
tc.advance(50.millis)
38+
timer.tick()
39+
40+
assert(statsReceiver.stats(Seq("foo", "bar", "hundredMilliSecondly")) == Seq(6, 0))
41+
histogramCounter.incr(3)
42+
43+
tc.advance(50.millis)
44+
timer.tick()
45+
46+
assert(statsReceiver.stats(Seq("foo", "bar", "hundredMilliSecondly")) == Seq(6, 0, 5))
47+
}
48+
}
49+
50+
test("Normalizes recorded stat to the elapsed time since last recording") {
51+
Time.withCurrentTimeFrozen { tc =>
52+
val timer = new MockTimer()
53+
val statsReceiver = new InMemoryStatsReceiver
54+
val histogramCounterFactory = new HistogramCounterFactory(timer, () => Time.now.inMillis)
55+
val histogramCounter =
56+
histogramCounterFactory(
57+
Seq("foo", "bar"),
58+
StatsFrequency.HundredMilliSecondly,
59+
statsReceiver
60+
)
61+
histogramCounter.incr(5)
62+
histogramCounter.incr(10)
63+
assert(statsReceiver.stats(Seq("foo", "bar", "hundredMilliSecondly")).isEmpty)
64+
65+
// Our task was slow to execute on the timer :(
66+
tc.advance(150.millis)
67+
timer.tick()
68+
69+
// We have 15 requests in 1.5 windows, so 10 requests in 1 window.
70+
assert(statsReceiver.stats(Seq("foo", "bar", "hundredMilliSecondly")) == Seq(10))
71+
72+
histogramCounter.incr(12)
73+
74+
tc.advance(300.millis)
75+
timer.tick()
76+
77+
// We have 12 requests in 3 windows, so 4 requests in 1 window.
78+
assert(statsReceiver.stats(Seq("foo", "bar", "hundredMilliSecondly")) == Seq(10, 4))
79+
}
80+
}
81+
82+
test("Returns the same histogramCounter object for equivalent names") {
83+
val timer = new MockTimer()
84+
val metrics = Metrics.createDetached()
85+
val statsReceiver = new MetricsStatsReceiver(metrics)
86+
87+
val histogramCounterFactory = new HistogramCounterFactory(timer, () => Time.now.inMillis)
88+
89+
val histogramCounter1 =
90+
histogramCounterFactory(Seq("foo", "bar"), StatsFrequency.HundredMilliSecondly, statsReceiver)
91+
val histogramCounter2 =
92+
histogramCounterFactory(Seq("foo", "bar"), StatsFrequency.HundredMilliSecondly, statsReceiver)
93+
val histogramCounter3 =
94+
histogramCounterFactory(Seq("foo/bar"), StatsFrequency.HundredMilliSecondly, statsReceiver)
95+
96+
val scopedStatsReceiver = statsReceiver.scope("foo")
97+
98+
val histogramCounter4 =
99+
histogramCounterFactory(Seq("bar"), StatsFrequency.HundredMilliSecondly, scopedStatsReceiver)
100+
101+
assert(histogramCounter1 eq histogramCounter2)
102+
assert(histogramCounter1 eq histogramCounter3)
103+
assert(histogramCounter1 eq histogramCounter4)
104+
}
105+
106+
test("Doesn't schedule recording of stats after close is called") {
107+
Time.withCurrentTimeFrozen { tc =>
108+
val timer = new MockTimer()
109+
assert(timer.tasks.size == 0)
110+
val histogramCounterFactory = new HistogramCounterFactory(timer, () => Time.now.inMillis)
111+
assert(timer.tasks.size == 1)
112+
histogramCounterFactory.close()
113+
tc.advance(100.millis)
114+
timer.tick()
115+
assert(timer.tasks.size == 0)
116+
}
117+
}
118+
}

0 commit comments

Comments
 (0)