-
-
Notifications
You must be signed in to change notification settings - Fork 475
fix(okhttp): keep the wrapped EventListener per Call #6003
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
105211a
14f6b50
11786ec
3506dcc
247ee38
b508070
552a731
728129d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -45,7 +45,12 @@ public open class SentryOkHttpEventListener( | |
| private val scopes: IScopes = ScopesAdapter.getInstance(), | ||
| private val originalEventListenerCreator: ((call: Call) -> EventListener)? = null, | ||
| ) : EventListener() { | ||
| private var originalEventListener: EventListener? = null | ||
| private val originalEventListenerMap: ConcurrentHashMap<Call, EventListener> = ConcurrentHashMap() | ||
|
|
||
| // Set only by the constructors that wrap a single EventListener instance. Such a listener is | ||
| // shared by every Call anyway, exactly like OkHttp's own EventListener.asFactory(), so it | ||
| // exists independently of the callStart()..callEnd() window and can always be delegated to. | ||
| private var fixedOriginalEventListener: EventListener? = null | ||
|
|
||
| public companion object { | ||
| internal const val PROXY_SELECT_EVENT = "http.client.proxy_select_ms" | ||
|
|
@@ -65,7 +70,9 @@ public open class SentryOkHttpEventListener( | |
|
|
||
| public constructor( | ||
| originalEventListener: EventListener | ||
| ) : this(ScopesAdapter.getInstance(), originalEventListenerCreator = { originalEventListener }) | ||
| ) : this(ScopesAdapter.getInstance(), originalEventListenerCreator = { originalEventListener }) { | ||
| fixedOriginalEventListener = originalEventListener | ||
| } | ||
|
|
||
| public constructor( | ||
| originalEventListenerFactory: Factory | ||
|
|
@@ -77,35 +84,41 @@ public open class SentryOkHttpEventListener( | |
| public constructor( | ||
| scopes: IScopes = ScopesAdapter.getInstance(), | ||
| originalEventListener: EventListener, | ||
| ) : this(scopes, originalEventListenerCreator = { originalEventListener }) | ||
| ) : this(scopes, originalEventListenerCreator = { originalEventListener }) { | ||
| fixedOriginalEventListener = originalEventListener | ||
| } | ||
|
|
||
| public constructor( | ||
| scopes: IScopes = ScopesAdapter.getInstance(), | ||
| originalEventListenerFactory: Factory, | ||
| ) : this(scopes, originalEventListenerCreator = { originalEventListenerFactory.create(it) }) | ||
|
|
||
| override fun callStart(call: Call) { | ||
| originalEventListener = originalEventListenerCreator?.invoke(call) | ||
| // The EventListener.Factory contract binds a listener to a single call, so the wrapped | ||
| // listener is kept per call instead of in a field shared by all concurrent calls | ||
| val originalEventListener = getOrCreateEventListener(call) | ||
| originalEventListener?.callStart(call) | ||
| // If the wrapped EventListener is ours, we can just delegate the calls, | ||
| // without creating other events that would create duplicates | ||
| if (canCreateEventSpan()) { | ||
| if (canCreateEventSpan(originalEventListener)) { | ||
| eventMap[call] = SentryOkHttpEvent(scopes, call.request()) | ||
| } | ||
| } | ||
|
|
||
| override fun proxySelectStart(call: Call, url: HttpUrl) { | ||
| val originalEventListener = originalEventListenerMap[call] | ||
| originalEventListener?.proxySelectStart(call, url) | ||
| if (!canCreateEventSpan()) { | ||
| if (!canCreateEventSpan(originalEventListener)) { | ||
| return | ||
| } | ||
| val okHttpEvent: SentryOkHttpEvent = eventMap[call] ?: return | ||
| okHttpEvent.onEventStart(PROXY_SELECT_EVENT) | ||
| } | ||
|
|
||
| override fun proxySelectEnd(call: Call, url: HttpUrl, proxies: List<Proxy>) { | ||
| val originalEventListener = originalEventListenerMap[call] | ||
| originalEventListener?.proxySelectEnd(call, url, proxies) | ||
| if (!canCreateEventSpan()) { | ||
| if (!canCreateEventSpan(originalEventListener)) { | ||
| return | ||
| } | ||
| val okHttpEvent: SentryOkHttpEvent = eventMap[call] ?: return | ||
|
|
@@ -117,17 +130,19 @@ public open class SentryOkHttpEventListener( | |
| } | ||
|
|
||
| override fun dnsStart(call: Call, domainName: String) { | ||
| val originalEventListener = originalEventListenerMap[call] | ||
| originalEventListener?.dnsStart(call, domainName) | ||
| if (!canCreateEventSpan()) { | ||
| if (!canCreateEventSpan(originalEventListener)) { | ||
| return | ||
| } | ||
| val okHttpEvent: SentryOkHttpEvent = eventMap[call] ?: return | ||
| okHttpEvent.onEventStart(DNS_EVENT) | ||
| } | ||
|
|
||
| override fun dnsEnd(call: Call, domainName: String, inetAddressList: List<InetAddress>) { | ||
| val originalEventListener = originalEventListenerMap[call] | ||
| originalEventListener?.dnsEnd(call, domainName, inetAddressList) | ||
| if (!canCreateEventSpan()) { | ||
| if (!canCreateEventSpan(originalEventListener)) { | ||
| return | ||
| } | ||
| val okHttpEvent: SentryOkHttpEvent = eventMap[call] ?: return | ||
|
|
@@ -140,26 +155,29 @@ public open class SentryOkHttpEventListener( | |
| } | ||
|
|
||
| override fun connectStart(call: Call, inetSocketAddress: InetSocketAddress, proxy: Proxy) { | ||
| val originalEventListener = originalEventListenerMap[call] | ||
| originalEventListener?.connectStart(call, inetSocketAddress, proxy) | ||
| if (!canCreateEventSpan()) { | ||
| if (!canCreateEventSpan(originalEventListener)) { | ||
| return | ||
| } | ||
| val okHttpEvent: SentryOkHttpEvent = eventMap[call] ?: return | ||
| okHttpEvent.onEventStart(CONNECT_EVENT) | ||
| } | ||
|
|
||
| override fun secureConnectStart(call: Call) { | ||
| val originalEventListener = originalEventListenerMap[call] | ||
| originalEventListener?.secureConnectStart(call) | ||
| if (!canCreateEventSpan()) { | ||
| if (!canCreateEventSpan(originalEventListener)) { | ||
| return | ||
| } | ||
| val okHttpEvent: SentryOkHttpEvent = eventMap[call] ?: return | ||
| okHttpEvent.onEventStart(SECURE_CONNECT_EVENT) | ||
| } | ||
|
|
||
| override fun secureConnectEnd(call: Call, handshake: Handshake?) { | ||
| val originalEventListener = originalEventListenerMap[call] | ||
| originalEventListener?.secureConnectEnd(call, handshake) | ||
| if (!canCreateEventSpan()) { | ||
| if (!canCreateEventSpan(originalEventListener)) { | ||
| return | ||
| } | ||
| val okHttpEvent: SentryOkHttpEvent = eventMap[call] ?: return | ||
|
|
@@ -172,8 +190,9 @@ public open class SentryOkHttpEventListener( | |
| proxy: Proxy, | ||
| protocol: Protocol?, | ||
| ) { | ||
| val originalEventListener = originalEventListenerMap[call] | ||
| originalEventListener?.connectEnd(call, inetSocketAddress, proxy, protocol) | ||
| if (!canCreateEventSpan()) { | ||
| if (!canCreateEventSpan(originalEventListener)) { | ||
| return | ||
| } | ||
| val okHttpEvent: SentryOkHttpEvent = eventMap[call] ?: return | ||
|
|
@@ -188,8 +207,9 @@ public open class SentryOkHttpEventListener( | |
| protocol: Protocol?, | ||
| ioe: IOException, | ||
| ) { | ||
| val originalEventListener = originalEventListenerMap[call] | ||
| originalEventListener?.connectFailed(call, inetSocketAddress, proxy, protocol, ioe) | ||
| if (!canCreateEventSpan()) { | ||
| if (!canCreateEventSpan(originalEventListener)) { | ||
| return | ||
| } | ||
| val okHttpEvent: SentryOkHttpEvent = eventMap[call] ?: return | ||
|
|
@@ -202,53 +222,59 @@ public open class SentryOkHttpEventListener( | |
| } | ||
|
|
||
| override fun connectionAcquired(call: Call, connection: Connection) { | ||
| val originalEventListener = originalEventListenerMap[call] | ||
| originalEventListener?.connectionAcquired(call, connection) | ||
| if (!canCreateEventSpan()) { | ||
| if (!canCreateEventSpan(originalEventListener)) { | ||
| return | ||
| } | ||
| val okHttpEvent: SentryOkHttpEvent = eventMap[call] ?: return | ||
| okHttpEvent.onEventStart(CONNECTION_EVENT) | ||
| } | ||
|
|
||
| override fun connectionReleased(call: Call, connection: Connection) { | ||
| val originalEventListener = originalEventListenerMap[call] | ||
| originalEventListener?.connectionReleased(call, connection) | ||
| if (!canCreateEventSpan()) { | ||
| if (!canCreateEventSpan(originalEventListener)) { | ||
| return | ||
| } | ||
| val okHttpEvent: SentryOkHttpEvent = eventMap[call] ?: return | ||
| okHttpEvent.onEventFinish(CONNECTION_EVENT) | ||
| } | ||
|
|
||
| override fun requestHeadersStart(call: Call) { | ||
| val originalEventListener = originalEventListenerMap[call] | ||
| originalEventListener?.requestHeadersStart(call) | ||
| if (!canCreateEventSpan()) { | ||
| if (!canCreateEventSpan(originalEventListener)) { | ||
| return | ||
| } | ||
| val okHttpEvent: SentryOkHttpEvent = eventMap[call] ?: return | ||
| okHttpEvent.onEventStart(REQUEST_HEADERS_EVENT) | ||
| } | ||
|
|
||
| override fun requestHeadersEnd(call: Call, request: Request) { | ||
| val originalEventListener = originalEventListenerMap[call] | ||
| originalEventListener?.requestHeadersEnd(call, request) | ||
| if (!canCreateEventSpan()) { | ||
| if (!canCreateEventSpan(originalEventListener)) { | ||
| return | ||
| } | ||
| val okHttpEvent: SentryOkHttpEvent = eventMap[call] ?: return | ||
| okHttpEvent.onEventFinish(REQUEST_HEADERS_EVENT) | ||
| } | ||
|
|
||
| override fun requestBodyStart(call: Call) { | ||
| val originalEventListener = originalEventListenerMap[call] | ||
| originalEventListener?.requestBodyStart(call) | ||
| if (!canCreateEventSpan()) { | ||
| if (!canCreateEventSpan(originalEventListener)) { | ||
| return | ||
| } | ||
| val okHttpEvent: SentryOkHttpEvent = eventMap[call] ?: return | ||
| okHttpEvent.onEventStart(REQUEST_BODY_EVENT) | ||
| } | ||
|
|
||
| override fun requestBodyEnd(call: Call, byteCount: Long) { | ||
| val originalEventListener = originalEventListenerMap[call] | ||
| originalEventListener?.requestBodyEnd(call, byteCount) | ||
| if (!canCreateEventSpan()) { | ||
| if (!canCreateEventSpan(originalEventListener)) { | ||
| return | ||
| } | ||
| val okHttpEvent: SentryOkHttpEvent = eventMap[call] ?: return | ||
|
|
@@ -261,8 +287,9 @@ public open class SentryOkHttpEventListener( | |
| } | ||
|
|
||
| override fun requestFailed(call: Call, ioe: IOException) { | ||
| val originalEventListener = originalEventListenerMap[call] | ||
| originalEventListener?.requestFailed(call, ioe) | ||
| if (!canCreateEventSpan()) { | ||
| if (!canCreateEventSpan(originalEventListener)) { | ||
| return | ||
| } | ||
| val okHttpEvent: SentryOkHttpEvent = eventMap[call] ?: return | ||
|
|
@@ -282,17 +309,19 @@ public open class SentryOkHttpEventListener( | |
| } | ||
|
|
||
| override fun responseHeadersStart(call: Call) { | ||
| val originalEventListener = originalEventListenerMap[call] | ||
| originalEventListener?.responseHeadersStart(call) | ||
| if (!canCreateEventSpan()) { | ||
| if (!canCreateEventSpan(originalEventListener)) { | ||
| return | ||
| } | ||
| val okHttpEvent: SentryOkHttpEvent = eventMap[call] ?: return | ||
| okHttpEvent.onEventStart(RESPONSE_HEADERS_EVENT) | ||
| } | ||
|
|
||
| override fun responseHeadersEnd(call: Call, response: Response) { | ||
| val originalEventListener = originalEventListenerMap[call] | ||
| originalEventListener?.responseHeadersEnd(call, response) | ||
| if (!canCreateEventSpan()) { | ||
| if (!canCreateEventSpan(originalEventListener)) { | ||
| return | ||
| } | ||
| val okHttpEvent: SentryOkHttpEvent = eventMap[call] ?: return | ||
|
|
@@ -307,17 +336,19 @@ public open class SentryOkHttpEventListener( | |
| } | ||
|
|
||
| override fun responseBodyStart(call: Call) { | ||
| val originalEventListener = originalEventListenerMap[call] | ||
| originalEventListener?.responseBodyStart(call) | ||
| if (!canCreateEventSpan()) { | ||
| if (!canCreateEventSpan(originalEventListener)) { | ||
| return | ||
| } | ||
| val okHttpEvent: SentryOkHttpEvent = eventMap[call] ?: return | ||
| okHttpEvent.onEventStart(RESPONSE_BODY_EVENT) | ||
| } | ||
|
|
||
| override fun responseBodyEnd(call: Call, byteCount: Long) { | ||
| val originalEventListener = originalEventListenerMap[call] | ||
| originalEventListener?.responseBodyEnd(call, byteCount) | ||
| if (!canCreateEventSpan()) { | ||
| if (!canCreateEventSpan(originalEventListener)) { | ||
| return | ||
| } | ||
| val okHttpEvent: SentryOkHttpEvent = eventMap[call] ?: return | ||
|
|
@@ -330,8 +361,9 @@ public open class SentryOkHttpEventListener( | |
| } | ||
|
|
||
| override fun responseFailed(call: Call, ioe: IOException) { | ||
| val originalEventListener = originalEventListenerMap[call] | ||
| originalEventListener?.responseFailed(call, ioe) | ||
| if (!canCreateEventSpan()) { | ||
| if (!canCreateEventSpan(originalEventListener)) { | ||
| return | ||
| } | ||
| val okHttpEvent: SentryOkHttpEvent = eventMap[call] ?: return | ||
|
|
@@ -351,14 +383,15 @@ public open class SentryOkHttpEventListener( | |
| } | ||
|
|
||
| override fun callEnd(call: Call) { | ||
| originalEventListener?.callEnd(call) | ||
| originalEventListenerMap.remove(call)?.callEnd(call) | ||
| val okHttpEvent: SentryOkHttpEvent = eventMap.remove(call) ?: return | ||
| okHttpEvent.finish() | ||
| } | ||
|
|
||
| override fun callFailed(call: Call, ioe: IOException) { | ||
| val originalEventListener = originalEventListenerMap.remove(call) | ||
| originalEventListener?.callFailed(call, ioe) | ||
| if (!canCreateEventSpan()) { | ||
| if (!canCreateEventSpan(originalEventListener)) { | ||
| return | ||
| } | ||
| val okHttpEvent: SentryOkHttpEvent = eventMap.remove(call) ?: return | ||
|
|
@@ -370,26 +403,43 @@ public open class SentryOkHttpEventListener( | |
| } | ||
|
|
||
| override fun canceled(call: Call) { | ||
| // canceled() is not part of the call window: OkHttp may deliver it before callStart() and | ||
| // after callEnd()/callFailed(), because it holds the listener for the whole Call lifetime | ||
| // while we only keep it for the duration of the call. | ||
| val originalEventListener = | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We're close! (and thanks for the great updates) We still need to preserve the contract of EventListener.Factory that ensures only one EventListener instance is produced per Call lifecycle. Ie, the listener returned by the factory for a given call needs to be the listener that captures i) all of that call's lifecycle and ii) no other call's lifecycle. We've fixed (ii), but we're still violating (i) in the case of cancelation because we're creating an extra listener for early and late cancel() invocations. Possible solution Thoughts about using a weak per-call map for the wrapped listener instead? Something like a WeakHashMap<Call, EventListener> guarded by synchronized, with a getOrCreateOriginalEventListener(call) helper used by both callStart and canceled(). That'd^^ let us avoid removing entries on callEnd / callFailed, and completed calls would be gc'd as soon as the Call instance is unreachable.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed in 728129d. You were right that creating an extra listener was the wrong trade — and a Why not weak keys. What we do instead. OkHttp stores the listener on the call itself (
A stored "terminal" flag would have worked too, but it needs a per- Two supporting changes: Residual gap, stated plainly: a call that is canceled before it starts and then never executed keeps its map entry, because no terminal event ever arrives. That is far narrower than the late-cancel leak it replaces, and bounding it would need the weak keys that do not work here. |
||
| originalEventListenerMap[call] | ||
| ?: fixedOriginalEventListener | ||
| // The call already reached its terminal event, so its listener is gone. Call.cancel() is | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Bug: The Suggested FixThe logic in the Prompt for AI Agent |
||
| // documented as a no-op for a completed request, thus there is nothing to report, and | ||
| // creating a second listener here would break the Factory contract and leak the entry. | ||
| ?: if (call.isExecuted()) null else getOrCreateEventListener(call) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Cancel before start leaks listenersMedium Severity
Additional Locations (1)Reviewed by Cursor Bugbot for commit 728129d. Configure here. |
||
| originalEventListener?.canceled(call) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Start race drops canceled callbackMedium Severity
Reviewed by Cursor Bugbot for commit 728129d. Configure here. |
||
| } | ||
|
|
||
| override fun satisfactionFailure(call: Call, response: Response) { | ||
| originalEventListener?.satisfactionFailure(call, response) | ||
| originalEventListenerMap[call]?.satisfactionFailure(call, response) | ||
| } | ||
|
|
||
| override fun cacheHit(call: Call, response: Response) { | ||
| originalEventListener?.cacheHit(call, response) | ||
| originalEventListenerMap[call]?.cacheHit(call, response) | ||
| } | ||
|
|
||
| override fun cacheMiss(call: Call) { | ||
| originalEventListener?.cacheMiss(call) | ||
| originalEventListenerMap[call]?.cacheMiss(call) | ||
| } | ||
|
|
||
| override fun cacheConditionalHit(call: Call, cachedResponse: Response) { | ||
| originalEventListener?.cacheConditionalHit(call, cachedResponse) | ||
| originalEventListenerMap[call]?.cacheConditionalHit(call, cachedResponse) | ||
| } | ||
|
|
||
| // computeIfAbsent, so that a cancel racing callStart() cannot make the Factory produce two | ||
| // listeners for the same Call | ||
| private fun getOrCreateEventListener(call: Call): EventListener? { | ||
| val creator = originalEventListenerCreator ?: return null | ||
| return originalEventListenerMap.computeIfAbsent(call) { creator.invoke(it) } | ||
| } | ||
|
|
||
| private fun canCreateEventSpan(): Boolean { | ||
| private fun canCreateEventSpan(originalEventListener: EventListener?): Boolean { | ||
| // If the wrapped EventListener is ours, we shouldn't create spans, as the originalEventListener | ||
| // already did it | ||
| // In case SentryOkHttpEventListener from sentry-android-okhttp is used, the is check won't work | ||
|
|
||


Uh oh!
There was an error while loading. Please reload this page.