Skip to content

Commit 3163d99

Browse files
authored
Merge pull request #2025 from newrelic/slick-3.5.0-bug
Slick 3.5.0 bug fix
2 parents 3be0439 + a439eb9 commit 3163d99

File tree

8 files changed

+291
-2
lines changed

8 files changed

+291
-2
lines changed

instrumentation/slick-2.12_3.2.0/build.gradle

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,10 @@ verifyInstrumentation {
2525
fails 'com.typesafe.slick:slick_2.11:[3.2.0,)'
2626

2727
// scala 12
28-
passesOnly 'com.typesafe.slick:slick_2.12:[3.2.0,)'
28+
passesOnly 'com.typesafe.slick:slick_2.12:[3.2.0,3.5.0)'
2929

3030
// scala 13
31-
passesOnly 'com.typesafe.slick:slick_2.13:[3.3.2,)'
31+
passesOnly 'com.typesafe.slick:slick_2.13:[3.3.2,3.5.0)'
3232

3333
excludeRegex ".*(RC|M)[0-9].*"
3434
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
/*
2+
*
3+
* * Copyright 2024 New Relic Corporation. All rights reserved.
4+
* * SPDX-License-Identifier: Apache-2.0
5+
*
6+
*/
7+
8+
9+
package slick.util;
10+
11+
import com.newrelic.api.agent.weaver.SkipIfPresent;
12+
13+
@SkipIfPresent(originalName = "slick.util.AsyncExecutor$DefaultAsyncExecutor")
14+
public class SkipDefaultAsyncExecutor {
15+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# Slick 3.5.0 Instrumentation
2+
3+
This instrumentation hooks into the `run` Function parameter of `AsyncExecutor.prioritizedRunnable`.
4+
We create a segment, named `ORM/Slick/SlickQuery`, whenever the `run` Function is eventually called.
5+
6+
Previous versions of the Slick instrumentation wrapped various executors, execution contexts, and runnables
7+
related to Slick's `AsyncExecutor`. This created casting issues in Slick 3.5.0+, because our wrappers effectively upcasted
8+
these types, which have concrete implementations in the Slick source code. Our wrappers ignored critical overridden methods,
9+
falling through to the default implementations and disrupting Slick's underlying concurrency mechanism.
10+
11+
To avoid these casting issues, we are no longer wrapping anything, except the `run` Function.
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
apply plugin: 'scala'
2+
scala.zincVersion = "1.7.1"
3+
4+
isScalaProjectEnabled(project, "scala-2.13")
5+
6+
dependencies {
7+
implementation(project(":newrelic-api"))
8+
implementation(project(":agent-bridge"))
9+
implementation(project(":newrelic-weaver-api"))
10+
implementation(project(":newrelic-weaver-scala-api"))
11+
implementation("org.scala-lang:scala-library:2.13.0")
12+
implementation("com.typesafe.slick:slick_2.13:3.5.1")
13+
14+
testImplementation("com.h2database:h2:1.4.190")
15+
testImplementation(project(":instrumentation:jdbc-h2")) { transitive = false }
16+
testImplementation(project(":instrumentation:jdbc-generic")) { transitive = false }
17+
}
18+
19+
jar {
20+
manifest { attributes 'Implementation-Title': 'com.newrelic.instrumentation.slick-2.12_3.5.0' }
21+
}
22+
23+
verifyInstrumentation {
24+
// scala 11 should be instrumented by another module
25+
fails 'com.typesafe.slick:slick_2.11:[3.2.0,)'
26+
27+
// scala 12
28+
passesOnly 'com.typesafe.slick:slick_2.12:[3.5.0,)'
29+
30+
// scala 13
31+
passesOnly 'com.typesafe.slick:slick_2.13:[3.5.0,)'
32+
33+
excludeRegex ".*(RC|M)[0-9].*"
34+
}
35+
36+
site {
37+
title 'Slick'
38+
type 'Datastore'
39+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/*
2+
*
3+
* * Copyright 2024 New Relic Corporation. All rights reserved.
4+
* * SPDX-License-Identifier: Apache-2.0
5+
*
6+
*/
7+
8+
9+
package slick.util
10+
11+
import com.newrelic.api.agent.{NewRelic, Token, Trace}
12+
import slick.util.AsyncExecutor.PrioritizedRunnable
13+
import slick.util.AsyncExecutor.PrioritizedRunnable.SetConnectionReleased
14+
15+
object AsyncExecutorUtil{
16+
17+
def wrapRunMethod(run: SetConnectionReleased => Unit, token: Token): SetConnectionReleased => Unit = setConnectionReleased => {
18+
doRun(run, setConnectionReleased, token)
19+
}
20+
21+
@Trace(async = true)
22+
def doRun(run: SetConnectionReleased => Unit, setConnectionReleased: SetConnectionReleased, token: Token): Unit = {
23+
if (token != null) {
24+
token.linkAndExpire();
25+
}
26+
NewRelic.getAgent.getTracedMethod.setMetricName("ORM", "Slick", "slickQuery")
27+
run(setConnectionReleased)
28+
}
29+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
/*
2+
*
3+
* * Copyright 2024 New Relic Corporation. All rights reserved.
4+
* * SPDX-License-Identifier: Apache-2.0
5+
*
6+
*/
7+
8+
9+
package slick.util;
10+
11+
import com.newrelic.agent.bridge.AgentBridge;
12+
import com.newrelic.api.agent.Token;
13+
import com.newrelic.api.agent.Transaction;
14+
import com.newrelic.api.agent.weaver.MatchType;
15+
import com.newrelic.api.agent.weaver.Weave;
16+
import com.newrelic.api.agent.weaver.Weaver;
17+
18+
import scala.Function0;
19+
import scala.Function1;
20+
import slick.util.AsyncExecutor.PrioritizedRunnable;
21+
22+
@Weave(type = MatchType.Interface, originalName = "slick.util.AsyncExecutor")
23+
public class AsyncExecutor_Instrumentation {
24+
public PrioritizedRunnable prioritizedRunnable(Function0 priority, Function1 run) {
25+
Transaction txn = AgentBridge.getAgent().getTransaction(false);
26+
Token token = null;
27+
if ( txn != null) {
28+
token = txn.getToken();
29+
run = AsyncExecutorUtil.wrapRunMethod(run, token);
30+
}
31+
return Weaver.callOriginal();
32+
}
33+
}
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
/*
2+
*
3+
* * Copyright 2024 New Relic Corporation. All rights reserved.
4+
* * SPDX-License-Identifier: Apache-2.0
5+
*
6+
*/
7+
8+
package com.nr.agent.instrumentation.slickdb
9+
10+
import com.newrelic.agent.introspec.InstrumentationTestConfig
11+
import com.newrelic.agent.introspec.InstrumentationTestRunner
12+
import com.newrelic.agent.introspec.Introspector;
13+
import com.newrelic.api.agent.Trace;
14+
15+
import scala.language.postfixOps
16+
17+
import org.junit._
18+
import org.junit.runner.RunWith;
19+
20+
import scala.concurrent._
21+
import scala.concurrent.duration._
22+
import scala.concurrent.ExecutionContext.Implicits.global;
23+
import slick.jdbc.H2Profile.api._
24+
25+
// Copied from slick-3.0.0 module
26+
@RunWith(classOf[InstrumentationTestRunner])
27+
@InstrumentationTestConfig(includePrefixes = Array("slick", "org.h2"))
28+
class SlickTest_350 {
29+
import SlickTest_350.slickdb
30+
import SlickTest_350.users
31+
32+
@Test
33+
def testCrud() {
34+
slickInsert();
35+
slickUpdate();
36+
slickDelete();
37+
Await.result(slickResult(), 20 seconds)
38+
val introspector :Introspector = InstrumentationTestRunner.getIntrospector()
39+
awaitFinishedTx(introspector, 4);
40+
val txnNames = introspector.getTransactionNames()
41+
txnNames.forEach(name => {
42+
val metrics = introspector.getMetricsForTransaction(name)
43+
Assert.assertTrue(metrics.containsKey("ORM/Slick/slickQuery"))
44+
})
45+
}
46+
47+
/*
48+
This test runs 50 concurrent queries, exceeding the default number of db connections provided by slick (20).
49+
A bug was discovered in the previous instrumentation for slick versions 3.5.0+, where for slick queries occurring
50+
outside a transaction, the executor would eventually pause and stop taking up any new work (despite having available
51+
threads). This module was created in response to that bug (see the README) and this test captures the bug's behavior.
52+
It will fail if run with previous instrumentation and slick <3.5.0.
53+
*/
54+
@Test
55+
def testNoTxn(): Unit = {
56+
try {
57+
Await.result(runConcurrentQueries, 10.seconds)
58+
} catch {
59+
case _: Throwable => Assert.fail("Futures timed out running concurrent queries.")
60+
}
61+
62+
}
63+
64+
65+
@Trace(dispatcher = true)
66+
def slickResult() :Future[String] = {
67+
slickdb.run(users.result).map(units => {
68+
var res :String = ""
69+
units.foreach {
70+
case (id, first_name, last_name) =>
71+
res += " * " + id + ": " + first_name + " " + last_name + "\n"
72+
}
73+
"Got results: \n"+res
74+
})
75+
}
76+
77+
@Trace(dispatcher = true)
78+
def slickInsert() :Future[String] = {
79+
slickdb.run(users.map(u => (u.id, u.first_name, u.last_name)) += (4, "John", "JacobJingle")).map(rowsInserted => {
80+
"Table now has "+rowsInserted+" users"
81+
})
82+
}
83+
84+
@Trace(dispatcher = true)
85+
def slickUpdate() :Future[String] = {
86+
slickdb.run(users.filter(_.id === 1).map(u => (u.first_name)).update(("Fred"))).map(result => {
87+
"result: "+result
88+
})
89+
}
90+
91+
@Trace(dispatcher = true)
92+
def slickDelete() :Future[String] = {
93+
// people.filter(p => p.name === "M. Odersky").delete
94+
slickdb.run(users.filter(_.id === 2).delete).map(result => {
95+
"result: "+result
96+
})
97+
}
98+
99+
def testQuery(id: Int) = {
100+
users.filter(_.id === id)
101+
}
102+
103+
def runConcurrentQueries = Future.traverse(1 to 50) { x =>
104+
val whichId = (x % 3) + 1
105+
slickdb.run(testQuery(whichId).result).map { v => println(s"Query Result $x: " + v) }
106+
}
107+
108+
// introspector does not handle async tx finishing very well so we're sleeping as a workaround
109+
private def awaitFinishedTx(introspector :Introspector, expectedTxCount: Int = 1) {
110+
while(introspector.getFinishedTransactionCount() <= expectedTxCount-1) {
111+
Thread.sleep(100)
112+
}
113+
Thread.sleep(100)
114+
}
115+
116+
}
117+
118+
class Users(tag: Tag) extends Table[(Int, String, String)] (tag, "user") {
119+
def id = column[Int]("id", O.PrimaryKey)
120+
def first_name = column[String]("first_name")
121+
def last_name = column[String]("last_name")
122+
// Every table needs a * projection with the same type as the table's type parameter
123+
def * = (id, first_name, last_name)
124+
}
125+
126+
object SlickTest_350 {
127+
val DB_DRIVER: String = "org.h2.Driver";
128+
val DB_CONNECTION: String = "jdbc:h2:mem:test;DB_CLOSE_DELAY=-1;DATABASE_TO_UPPER=false";
129+
130+
val slickdb = Database.forURL(DB_CONNECTION, DB_DRIVER)
131+
val users = TableQuery[Users]
132+
133+
@BeforeClass
134+
def setup() {
135+
// set up h2
136+
Assert.assertNotNull("Unable to create h2 db.", slickdb)
137+
Assert.assertNotNull("Unable to create user table.", users)
138+
Await.result(initData(), 10.seconds) //make sure we don't enter the test suite until the init task finishes
139+
}
140+
141+
@AfterClass
142+
def teardown() {
143+
// tear down h2
144+
if (null != slickdb) {
145+
slickdb.close();
146+
}
147+
}
148+
149+
def initData() = {
150+
val setup = DBIO.seq(
151+
// Create and populate the tables
152+
users.schema.create,
153+
users ++= Seq(
154+
(1, "Fakus", "Namus"),
155+
(2, "Some", "Guy"),
156+
(3, "Whatser", "Name")
157+
))
158+
slickdb.run(setup)
159+
}
160+
161+
}

settings.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -339,6 +339,7 @@ include 'instrumentation:servlet-user-5.0'
339339
include 'instrumentation:slick-3.0.0'
340340
include 'instrumentation:slick-2.11_3.2.0'
341341
include 'instrumentation:slick-2.12_3.2.0'
342+
include 'instrumentation:slick-2.12_3.5.0'
342343
include 'instrumentation:solr-4.0.0'
343344
include 'instrumentation:solr-5.0.0'
344345
include 'instrumentation:solr-5.1.0'

0 commit comments

Comments
 (0)