Subscription.isActive() is documented as a non-blocking check, but in practice it blocks for up to maxAwaitTime seconds. This is because CursorReadingTask uses a single ReentrantLock shared between getState() and getNext(). When the cursor thread is inside getNext() → cursor.tryNext() (long-polling MongoDB), any call to getState() from another thread blocks until the poll completes.
This makes it impossible to reliably monitor subscription health from a scheduled monitor thread without risking thread starvation or adding workarounds like executor-based timeout wrappers.
Reproduction:
- Register a change stream with maxAwaitTime(Duration.ofSeconds(30))
- From a separate thread (e.g., a @scheduled monitor), call subscription.isActive()
- The call blocks for up to 30 seconds while getNext() holds the lock
Suggested fix:
Make the state field in CursorReadingTask volatile instead of guarding reads with the lock. The field is a simple enum reference — reads are atomic in Java and don't need mutual exclusion with getNext(). The lock should still be used for state transitions (writes) but not for reads.
// Current (blocks):
public State getState() {
return lock.execute(() -> state);
}
// Suggested (non-blocking):
private volatile State state = State.CREATED;
public State getState() {
return state;
}
Spring Data MongoDB version: 4.5.10
Spring Boot version: 3.5.12
Subscription.isActive() is documented as a non-blocking check, but in practice it blocks for up to maxAwaitTime seconds. This is because CursorReadingTask uses a single ReentrantLock shared between getState() and getNext(). When the cursor thread is inside getNext() → cursor.tryNext() (long-polling MongoDB), any call to getState() from another thread blocks until the poll completes.
This makes it impossible to reliably monitor subscription health from a scheduled monitor thread without risking thread starvation or adding workarounds like executor-based timeout wrappers.
Reproduction:
Suggested fix:
Make the state field in CursorReadingTask volatile instead of guarding reads with the lock. The field is a simple enum reference — reads are atomic in Java and don't need mutual exclusion with getNext(). The lock should still be used for state transitions (writes) but not for reads.
// Current (blocks):
public State getState() {
return lock.execute(() -> state);
}
// Suggested (non-blocking):
private volatile State state = State.CREATED;
public State getState() {
return state;
}
Spring Data MongoDB version: 4.5.10
Spring Boot version: 3.5.12