-
-
Notifications
You must be signed in to change notification settings - Fork 441
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Fix lazy select queries instrumentation (#3604)
* added SentryCrossProcessCursor wrapper * SQLiteSpanManager now wraps CrossProcessCursors to start a span only when the cursor is filled with data
- Loading branch information
1 parent
ae2294f
commit 7c34b37
Showing
5 changed files
with
214 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
51 changes: 51 additions & 0 deletions
51
sentry-android-sqlite/src/main/java/io/sentry/android/sqlite/SentryCrossProcessCursor.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
package io.sentry.android.sqlite | ||
|
||
import android.database.CrossProcessCursor | ||
import android.database.CursorWindow | ||
|
||
/* | ||
* SQLiteCursor executes the query lazily, when one of getCount() and onMove() is called. | ||
* Also, by docs, fillWindow() can be used to fill the cursor with data. | ||
* So we wrap these methods to create a span. | ||
* SQLiteCursor is never used directly in the code, but only the Cursor interface. | ||
* This means we can use CrossProcessCursor - that extends Cursor - as wrapper, since | ||
* CrossProcessCursor is an interface and we can use Kotlin delegation. | ||
*/ | ||
internal class SentryCrossProcessCursor( | ||
private val delegate: CrossProcessCursor, | ||
private val spanManager: SQLiteSpanManager, | ||
private val sql: String | ||
) : CrossProcessCursor by delegate { | ||
// We have to start the span only the first time, regardless of how many times its methods get called. | ||
private var isSpanStarted = false | ||
|
||
override fun getCount(): Int { | ||
if (isSpanStarted) { | ||
return delegate.count | ||
} | ||
isSpanStarted = true | ||
return spanManager.performSql(sql) { | ||
delegate.count | ||
} | ||
} | ||
|
||
override fun onMove(oldPosition: Int, newPosition: Int): Boolean { | ||
if (isSpanStarted) { | ||
return delegate.onMove(oldPosition, newPosition) | ||
} | ||
isSpanStarted = true | ||
return spanManager.performSql(sql) { | ||
delegate.onMove(oldPosition, newPosition) | ||
} | ||
} | ||
|
||
override fun fillWindow(position: Int, window: CursorWindow?) { | ||
if (isSpanStarted) { | ||
return delegate.fillWindow(position, window) | ||
} | ||
isSpanStarted = true | ||
return spanManager.performSql(sql) { | ||
delegate.fillWindow(position, window) | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
124 changes: 124 additions & 0 deletions
124
sentry-android-sqlite/src/test/java/io/sentry/android/sqlite/SentryCrossProcessCursorTest.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,124 @@ | ||
package io.sentry.android.sqlite | ||
|
||
import android.database.CrossProcessCursor | ||
import io.sentry.IHub | ||
import io.sentry.ISpan | ||
import io.sentry.SentryOptions | ||
import io.sentry.SentryTracer | ||
import io.sentry.SpanStatus | ||
import io.sentry.TransactionContext | ||
import org.mockito.kotlin.any | ||
import org.mockito.kotlin.eq | ||
import org.mockito.kotlin.mock | ||
import org.mockito.kotlin.verify | ||
import org.mockito.kotlin.whenever | ||
import kotlin.test.Test | ||
import kotlin.test.assertEquals | ||
import kotlin.test.assertNotNull | ||
import kotlin.test.assertTrue | ||
|
||
class SentryCrossProcessCursorTest { | ||
private class Fixture { | ||
private val hub = mock<IHub>() | ||
private val spanManager = SQLiteSpanManager(hub) | ||
val mockCursor = mock<CrossProcessCursor>() | ||
lateinit var options: SentryOptions | ||
lateinit var sentryTracer: SentryTracer | ||
|
||
fun getSut(sql: String, isSpanActive: Boolean = true): SentryCrossProcessCursor { | ||
options = SentryOptions().apply { | ||
dsn = "https://[email protected]/proj" | ||
} | ||
whenever(hub.options).thenReturn(options) | ||
sentryTracer = SentryTracer(TransactionContext("name", "op"), hub) | ||
|
||
if (isSpanActive) { | ||
whenever(hub.span).thenReturn(sentryTracer) | ||
} | ||
return SentryCrossProcessCursor(mockCursor, spanManager, sql) | ||
} | ||
} | ||
|
||
private val fixture = Fixture() | ||
|
||
@Test | ||
fun `all calls are propagated to the delegate`() { | ||
val sql = "sql" | ||
val cursor = fixture.getSut(sql) | ||
|
||
cursor.onMove(0, 1) | ||
verify(fixture.mockCursor).onMove(eq(0), eq(1)) | ||
|
||
cursor.count | ||
verify(fixture.mockCursor).count | ||
|
||
cursor.fillWindow(0, mock()) | ||
verify(fixture.mockCursor).fillWindow(eq(0), any()) | ||
|
||
// Let's verify other methods are delegated, even if not explicitly | ||
cursor.close() | ||
verify(fixture.mockCursor).close() | ||
|
||
cursor.getString(1) | ||
verify(fixture.mockCursor).getString(eq(1)) | ||
} | ||
|
||
@Test | ||
fun `getCount creates a span if a span is running`() { | ||
val sql = "execute" | ||
val sut = fixture.getSut(sql) | ||
assertEquals(0, fixture.sentryTracer.children.size) | ||
sut.count | ||
val span = fixture.sentryTracer.children.firstOrNull() | ||
assertSqlSpanCreated(sql, span) | ||
} | ||
|
||
@Test | ||
fun `getCount does not create a span if no span is running`() { | ||
val sut = fixture.getSut("execute", isSpanActive = false) | ||
sut.count | ||
assertEquals(0, fixture.sentryTracer.children.size) | ||
} | ||
|
||
@Test | ||
fun `onMove creates a span if a span is running`() { | ||
val sql = "execute" | ||
val sut = fixture.getSut(sql) | ||
assertEquals(0, fixture.sentryTracer.children.size) | ||
sut.onMove(0, 5) | ||
val span = fixture.sentryTracer.children.firstOrNull() | ||
assertSqlSpanCreated(sql, span) | ||
} | ||
|
||
@Test | ||
fun `onMove does not create a span if no span is running`() { | ||
val sut = fixture.getSut("execute", isSpanActive = false) | ||
sut.onMove(0, 5) | ||
assertEquals(0, fixture.sentryTracer.children.size) | ||
} | ||
|
||
@Test | ||
fun `fillWindow creates a span if a span is running`() { | ||
val sql = "execute" | ||
val sut = fixture.getSut(sql) | ||
assertEquals(0, fixture.sentryTracer.children.size) | ||
sut.fillWindow(0, mock()) | ||
val span = fixture.sentryTracer.children.firstOrNull() | ||
assertSqlSpanCreated(sql, span) | ||
} | ||
|
||
@Test | ||
fun `fillWindow does not create a span if no span is running`() { | ||
val sut = fixture.getSut("execute", isSpanActive = false) | ||
sut.fillWindow(0, mock()) | ||
assertEquals(0, fixture.sentryTracer.children.size) | ||
} | ||
|
||
private fun assertSqlSpanCreated(sql: String, span: ISpan?) { | ||
assertNotNull(span) | ||
assertEquals("db.sql.query", span.operation) | ||
assertEquals(sql, span.description) | ||
assertEquals(SpanStatus.OK, span.status) | ||
assertTrue(span.isFinished) | ||
} | ||
} |