Skip to content

Subscription.isActive() blocks while change stream cursor long-polls #5201

Description

@SnehaKumawat

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:

  1. Register a change stream with maxAwaitTime(Duration.ofSeconds(30))
  2. From a separate thread (e.g., a @scheduled monitor), call subscription.isActive()
  3. 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

Metadata

Metadata

Assignees

Labels

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions