Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
- Probe class availability without initializing the class during SDK init ([#5635](https://github.com/getsentry/sentry-java/pull/5635))
- Avoid constructing an exception per view when resolving view ids during view-hierarchy and gesture capture ([#5631](https://github.com/getsentry/sentry-java/pull/5631))
- Start the frame metrics thread lazily on first collection instead of during SDK init ([#5641](https://github.com/getsentry/sentry-java/pull/5641))
- Lazily allocate the `ReentrantLock` backing `AutoClosableReentrantLock` to avoid eager lock allocations for SDK objects that never contend during `SentryAndroid.init` ([#5643](https://github.com/getsentry/sentry-java/pull/5643))

## 8.46.0

Expand Down
2 changes: 1 addition & 1 deletion sentry/api/sentry.api
Original file line number Diff line number Diff line change
Expand Up @@ -7589,7 +7589,7 @@ public abstract class io/sentry/transport/TransportResult {
public static fun success ()Lio/sentry/transport/TransportResult;
}

public final class io/sentry/util/AutoClosableReentrantLock : java/util/concurrent/locks/ReentrantLock {
public final class io/sentry/util/AutoClosableReentrantLock {
public fun <init> ()V
public fun acquire ()Lio/sentry/ISentryLifecycleToken;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,57 @@
package io.sentry.util;

import io.sentry.ISentryLifecycleToken;
import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;
import java.util.concurrent.locks.ReentrantLock;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.TestOnly;

public final class AutoClosableReentrantLock extends ReentrantLock {
/**
* Hands out an {@link ISentryLifecycleToken} from {@link #acquire()} for use with
* try-with-resources (replacing {@code synchronized} blocks).
*
* <p>The underlying {@link ReentrantLock} is created lazily on the first {@link #acquire()}. Many
* SDK objects hold a lock but never contend on it (especially during {@code SentryAndroid.init}),
* so the eager allocation of a {@link ReentrantLock} (and its {@code AbstractQueuedSynchronizer})
* was pure GC and main-thread overhead. We keep a {@link ReentrantLock} rather than reverting to
* {@code synchronized} to stay friendly to virtual threads (Loom), see #3715.
*/
public final class AutoClosableReentrantLock {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: AutoClosableReentrantLock is no longer Serializable, which will cause a NotSerializableException when serializing classes that use it, like SynchronizedCollection.
Severity: MEDIUM

Suggested Fix

Make AutoClosableReentrantLock implement Serializable. Since its internal state (lock) is a ReentrantLock which is already serializable, this should be a straightforward change. Alternatively, mark the lock field in SynchronizedCollection and SynchronizedQueue as transient and provide custom writeObject/readObject methods to handle serialization correctly, if the lock state is not intended to be serialized.

Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent. Verify if this is a real issue. If it is, propose a fix; if not, explain why it's
not valid.

Location: sentry/src/main/java/io/sentry/util/AutoClosableReentrantLock.java#L20

Potential issue: The `AutoClosableReentrantLock` class no longer extends `ReentrantLock`
and does not implement `Serializable`. Consequently, classes like
`SynchronizedCollection` and `SynchronizedQueue`, which implement `Serializable` and
contain a non-transient `AutoClosableReentrantLock` field, will fail to serialize. Any
attempt by an SDK user to serialize an object containing one of these collections, such
as a `Scope` instance, will result in a `NotSerializableException` at runtime. This is a
binary compatibility break that was overlooked.

Did we get this right? ๐Ÿ‘ / ๐Ÿ‘Ž to inform future reviews.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this an issue on the JVM? Definitely not needed for Android.


private static final long serialVersionUID = -3283069816958445549L;
private static final @NotNull AtomicReferenceFieldUpdater<
AutoClosableReentrantLock, ReentrantLock>
LOCK_UPDATER =
AtomicReferenceFieldUpdater.newUpdater(
AutoClosableReentrantLock.class, ReentrantLock.class, "lock");

public ISentryLifecycleToken acquire() {
lock();
return new AutoClosableReentrantLockLifecycleToken(this);
private volatile @Nullable ReentrantLock lock;

public @NotNull ISentryLifecycleToken acquire() {
final @NotNull ReentrantLock theLock = getOrCreateLock();
theLock.lock();
return new AutoClosableReentrantLockLifecycleToken(theLock);
}

private @NotNull ReentrantLock getOrCreateLock() {
final @Nullable ReentrantLock existing = lock;
if (existing != null) {
return existing;
}
// The loser of the race discards its candidate and uses the winner's lock, so all callers
// contend on the same instance.
final @NotNull ReentrantLock candidate = new ReentrantLock();
if (LOCK_UPDATER.compareAndSet(this, null, candidate)) {
return candidate;
}
final @Nullable ReentrantLock winner = lock;
return winner != null ? winner : candidate;
}

@TestOnly
boolean isLocked() {
final @Nullable ReentrantLock current = lock;
return current != null && current.isLocked();
}

static final class AutoClosableReentrantLockLifecycleToken implements ISentryLifecycleToken {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
package io.sentry.util

import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicInteger
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertTrue

Expand All @@ -11,4 +15,49 @@ class AutoClosableReentrantLockTest {
lock.acquire().use { assertTrue(lock.isLocked) }
assertFalse(lock.isLocked)
}

@Test
fun `does not allocate the underlying lock until first acquire`() {
val lock = AutoClosableReentrantLock()
assertFalse(lock.isLocked)
}

@Test
fun `supports reentrant acquire from the same thread`() {
val lock = AutoClosableReentrantLock()
lock.acquire().use {
lock.acquire().use { assertTrue(lock.isLocked) }
assertTrue(lock.isLocked)
}
assertFalse(lock.isLocked)
}

@Test
fun `mutually excludes concurrent threads`() {
val lock = AutoClosableReentrantLock()
val inCriticalSection = AtomicInteger(0)
val maxObserved = AtomicInteger(0)
val start = CountDownLatch(1)
val threadCount = 8
val iterations = 1000
val threads =
(0 until threadCount).map {
Thread {
start.await()
repeat(iterations) {
lock.acquire().use {
val current = inCriticalSection.incrementAndGet()
maxObserved.accumulateAndGet(current, ::maxOf)
inCriticalSection.decrementAndGet()
}
}
}
}
threads.forEach(Thread::start)
start.countDown()
threads.forEach { it.join(TimeUnit.SECONDS.toMillis(10)) }

assertEquals(1, maxObserved.get())
assertFalse(lock.isLocked)
}
}
Loading