Skip to content

Commit 508ab9d

Browse files
committed
[bugfix] Fix concurrency issue in XQueryContext.ContextUpdateListener
Closes #2185
1 parent 345ca6d commit 508ab9d

File tree

1 file changed

+31
-9
lines changed

1 file changed

+31
-9
lines changed

src/org/exist/xquery/XQueryContext.java

Lines changed: 31 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@
3030
import java.nio.charset.Charset;
3131
import java.nio.file.Path;
3232
import java.util.*;
33+
import java.util.concurrent.CopyOnWriteArrayList;
34+
import java.util.concurrent.atomic.AtomicReference;
3335
import java.util.function.Predicate;
3436
import java.util.stream.Collectors;
3537
import java.util.stream.Stream;
@@ -50,6 +52,7 @@
5052
import com.evolvedbinary.j8fu.tuple.Tuple2;
5153
import com.ibm.icu.text.Collator;
5254
import net.jcip.annotations.Immutable;
55+
import net.jcip.annotations.ThreadSafe;
5356
import org.apache.logging.log4j.LogManager;
5457
import org.apache.logging.log4j.Logger;
5558
import org.exist.Database;
@@ -3121,38 +3124,57 @@ public void setSource(final Source source) {
31213124
this.source = source;
31223125
}
31233126

3127+
/**
3128+
* NOTE: the {@link #unsubscribe()} method can be called
3129+
* from {@link org.exist.storage.NotificationService#unsubscribe(UpdateListener)}
3130+
* by another thread, so this class needs to be thread-safe.
3131+
*/
3132+
@ThreadSafe
31243133
private static class ContextUpdateListener implements UpdateListener {
3125-
private final List<UpdateListener> listeners = new ArrayList<>();
3134+
/*
3135+
* We use Concurrent safe data structures here, so that we don't have
3136+
* to block any calling threads.
3137+
*
3138+
* The AtomicReference enables us to quickly clear the listeners
3139+
* in #unsubscribe() and maintain happens-before integrity whilst
3140+
* unsubcribing them. The CopyOnWriteArrayList allows
3141+
* us to add listeners whilst iterating over a snapshot
3142+
* of existing iterators in other methods.
3143+
*/
3144+
private final AtomicReference<List<UpdateListener>> listeners = new AtomicReference<>(new CopyOnWriteArrayList<>());
31263145

31273146
private void addListener(final UpdateListener listener) {
3128-
listeners.add(listener);
3147+
listeners.get().add(listener);
31293148
}
31303149

31313150
@Override
31323151
public void documentUpdated(final DocumentImpl document, final int event) {
3133-
listeners.forEach(listener -> listener.documentUpdated(document, event));
3152+
listeners.get().forEach(listener -> listener.documentUpdated(document, event));
31343153
}
31353154

31363155
@Override
31373156
public void unsubscribe() {
3138-
listeners.forEach(UpdateListener::unsubscribe);
3139-
listeners.clear();
3157+
List<UpdateListener> prev = listeners.get();
3158+
while (!listeners.compareAndSet(prev, new CopyOnWriteArrayList<>())) {
3159+
prev = listeners.get();
3160+
}
3161+
3162+
prev.forEach(UpdateListener::unsubscribe);
31403163
}
31413164

31423165
@Override
31433166
public void nodeMoved(final NodeId oldNodeId, final NodeHandle newNode) {
3144-
listeners.forEach(listener -> listener.nodeMoved(oldNodeId, newNode));
3167+
listeners.get().forEach(listener -> listener.nodeMoved(oldNodeId, newNode));
31453168
}
31463169

31473170
@Override
31483171
public void debug() {
31493172
if (LOG.isDebugEnabled()) {
3150-
LOG.debug(String.format("XQueryContext: %s document update listeners", listeners.size()));
3173+
LOG.debug(String.format("XQueryContext: %s document update listeners", listeners.get().size()));
31513174
}
31523175

3153-
listeners.forEach(UpdateListener::debug);
3176+
listeners.get().forEach(UpdateListener::debug);
31543177
}
3155-
31563178
}
31573179

31583180
private final List<CleanupTask> cleanupTasks = new ArrayList<>();

0 commit comments

Comments
 (0)