Skip to content

Commit 8b9700c

Browse files
mbezoyanjenkins
authored and
jenkins
committed
finagle-mysql: Add metrics for the cache of prepared statements
Problem ClientDispatcher in finagle-mysql uses a cache of prepared statements to reuse them. However there is no observability to this cache, it is hard to tell if the size is large enough or what the hit rate is. Solution Add 3 metrics showing the current state of the cache: - pstmt-cache/calls: the total number of calls - pstmt-cache/misses: the number of misses. The hit rate would be 1 - misses/calls then - pstmt-cache/evicted_<reason>: a collection of counters showing how many prepared statements were evicted and why Differential Revision: https://phabricator.twitter.biz/D479926
1 parent 07e1be2 commit 8b9700c

File tree

3 files changed

+88
-9
lines changed

3 files changed

+88
-9
lines changed

doc/src/sphinx/metrics/Mysql.rst

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,4 +15,13 @@
1515
A counter of the number of opened cursors.
1616

1717
**<label>/cursor/closed**
18-
A counter of the number of closed cursors.
18+
A counter of the number of closed cursors.
19+
20+
**<label>/pstmt-cache/calls**
21+
A counter of the number of requested prepared statements.
22+
23+
**<label>/pstmt-cache/misses**
24+
A counter of the number of times when a prepared statement was not the cache.
25+
26+
**<label>/pstmt-cache/evicted_size**
27+
A counter of the number of times prepared statements were evicted from the cache.

finagle-mysql/src/main/scala/com/twitter/finagle/mysql/ClientDispatcher.scala

Lines changed: 37 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import com.twitter.finagle.mysql.LostSyncException.const
88
import com.twitter.finagle.mysql.param.{MaxConcurrentPrepareStatements, UnsignedColumns}
99
import com.twitter.finagle.mysql.transport.{MysqlBuf, MysqlBufReader, Packet}
1010
import com.twitter.finagle.param.Stats
11+
import com.twitter.finagle.stats.{Counter, LazyStatsReceiver, StatsReceiver}
1112
import com.twitter.finagle.transport.Transport
1213
import com.twitter.finagle.{Service, ServiceProxy, Stack}
1314
import com.twitter.util._
@@ -24,13 +25,32 @@ case class ServerError(code: Short, sqlState: String, message: String) extends E
2425
* the chances of leaking prepared statements and can simplify the
2526
* implementation of prepared statements in the presence of a connection pool.
2627
*/
27-
private[mysql] class PrepareCache(svc: Service[Request, Result], cache: Caffeine[Object, Object])
28+
private[mysql] class PrepareCache(
29+
svc: Service[Request, Result],
30+
cache: Caffeine[Object, Object],
31+
statsReceiver: StatsReceiver)
2832
extends ServiceProxy[Request, Result](svc) {
2933

34+
private[this] val scopedStatsReceiver = new LazyStatsReceiver(
35+
statsReceiver.scope("pstmt-cache")
36+
)
37+
38+
private[this] val evictionCounters = {
39+
val counters = new Array[Counter](RemovalCause.values().length)
40+
for (value <- RemovalCause.values()) {
41+
counters(value.ordinal()) =
42+
scopedStatsReceiver.counter(s"evicted_${value.name().toLowerCase}")
43+
}
44+
counters
45+
}
46+
private[this] val callCounter = scopedStatsReceiver.counter("calls")
47+
private[this] val missCounter = scopedStatsReceiver.counter("misses")
48+
3049
private[this] val fn = {
3150
val listener = new RemovalListener[Request, Future[Result]] {
3251
// make sure prepared futures get removed eventually
3352
def onRemoval(request: Request, response: Future[Result], cause: RemovalCause): Unit = {
53+
evictionCounters(cause.ordinal()).incr()
3454
response.respond {
3555
case Return(r: PrepareOK) =>
3656
svc(CloseRequest(r.id)).unit
@@ -43,15 +63,23 @@ private[mysql] class PrepareCache(svc: Service[Request, Result], cache: Caffeine
4363
.removalListener(listener)
4464
.build[Request, Future[Result]]()
4565

46-
CaffeineCache.fromCache(Service.mk { req: Request => svc(req) }, underlying)
66+
CaffeineCache.fromCache(
67+
fn = { req: Request =>
68+
missCounter.incr()
69+
svc(req)
70+
},
71+
cache = underlying
72+
)
4773
}
4874

4975
/**
5076
* Populate cache with unique prepare requests identified by their
5177
* sql queries.
5278
*/
5379
override def apply(req: Request): Future[Result] = req match {
54-
case _: PrepareRequest => fn(req)
80+
case _: PrepareRequest =>
81+
callCounter.incr()
82+
fn(req)
5583
case _ => super.apply(req)
5684
}
5785
}
@@ -67,8 +95,9 @@ private[finagle] object ClientDispatcher {
6795
def apply(trans: Transport[Packet, Packet], params: Stack.Params): Service[Request, Result] = {
6896
val maxConcurrentPrepareStatements = params[MaxConcurrentPrepareStatements].num
6997
new PrepareCache(
70-
new ClientDispatcher(trans, params),
71-
Caffeine.newBuilder().maximumSize(maxConcurrentPrepareStatements)
98+
svc = new ClientDispatcher(trans, params),
99+
cache = Caffeine.newBuilder().maximumSize(maxConcurrentPrepareStatements),
100+
statsReceiver = params[Stats].statsReceiver
72101
)
73102
}
74103

@@ -171,8 +200,9 @@ private[finagle] final class ClientDispatcher(
171200
(seq2, _) <- readTx(req, ok.numOfCols)
172201
ps <- Future.collect(seq1.map { p => const(Field(p)) })
173202
cs <- Future.collect(seq2.map { p => const(Field(p)) })
174-
} yield ok.copy(params = ps, columns = cs)
175-
203+
} yield {
204+
ok.copy(params = ps, columns = cs)
205+
}
176206
result.ensure { signal.setDone() }
177207

178208
// decode OK Result

finagle-mysql/src/test/scala/com/twitter/finagle/mysql/unit/PrepareCacheTest.scala

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package com.twitter.finagle.mysql
22

33
import com.github.benmanes.caffeine.cache.Caffeine
44
import com.twitter.finagle.Service
5+
import com.twitter.finagle.stats.{InMemoryStatsReceiver, NullStatsReceiver}
56
import com.twitter.util.Future
67
import java.util.concurrent.Executor
78
import java.util.concurrent.LinkedBlockingQueue
@@ -24,7 +25,7 @@ class PrepareCacheTest extends FunSuite {
2425
.maximumSize(11)
2526
.executor(new Executor { def execute(r: Runnable) = r.run() })
2627

27-
val svc = new PrepareCache(dispatcher, cache)
28+
val svc = new PrepareCache(dispatcher, cache, NullStatsReceiver)
2829
val r0 = PrepareRequest("SELECT 0")
2930
svc(r0)
3031
svc(r0)
@@ -54,4 +55,43 @@ class PrepareCacheTest extends FunSuite {
5455
for (i <- 1 to 10) svc(PrepareRequest(s"SELECT $i"))
5556
assert(!q.isEmpty)
5657
}
58+
59+
test("emit stats") {
60+
61+
val q = new LinkedBlockingQueue[Request]()
62+
63+
val stmtId = 2
64+
val dispatcher = Service.mk[Request, Result] { req: Request =>
65+
q.offer(req)
66+
Future.value(PrepareOK(stmtId, 1, 1, 0))
67+
}
68+
69+
val cache = Caffeine
70+
.newBuilder()
71+
.maximumSize(1)
72+
.executor(new Executor { def execute(r: Runnable) = r.run() })
73+
74+
val ist = new InMemoryStatsReceiver()
75+
val svc = new PrepareCache(dispatcher, cache, ist)
76+
val r0 = PrepareRequest("SELECT 0")
77+
svc(r0)
78+
svc(r0)
79+
assert(q.poll() == r0)
80+
81+
assert(ist.counters(Seq("pstmt-cache", "calls")) == 2)
82+
assert(ist.counters(Seq("pstmt-cache", "misses")) == 1)
83+
assert(!ist.counters.contains(Seq("pstmt-cache", "evicted_size")))
84+
85+
val r1 = PrepareRequest("SELECT 1")
86+
svc(r1)
87+
88+
val expectedCounters = Seq(
89+
Seq("pstmt-cache", "calls") -> 3,
90+
Seq("pstmt-cache", "misses") -> 2,
91+
Seq("pstmt-cache", "evicted_size") -> 1
92+
).toMap
93+
94+
assert(expectedCounters == ist.counters)
95+
}
96+
5797
}

0 commit comments

Comments
 (0)