diff --git a/exist-core/src/main/java/org/exist/collections/triggers/TriggerStatePerThread.java b/exist-core/src/main/java/org/exist/collections/triggers/TriggerStatePerThread.java
index b5c99909861..8fe507082e4 100644
--- a/exist-core/src/main/java/org/exist/collections/triggers/TriggerStatePerThread.java
+++ b/exist-core/src/main/java/org/exist/collections/triggers/TriggerStatePerThread.java
@@ -26,12 +26,14 @@
import org.exist.xmldb.XmldbURI;
import javax.annotation.Nullable;
+import java.lang.ref.WeakReference;
import java.util.ArrayDeque;
+import java.util.Collections;
import java.util.Deque;
import java.util.Iterator;
+import java.util.Map;
import java.util.Objects;
-import java.util.concurrent.ConcurrentHashMap;
-import java.util.concurrent.ConcurrentMap;
+import java.util.WeakHashMap;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
@@ -42,11 +44,10 @@
* @author Adam Retter
*/
public class TriggerStatePerThread {
-
- private static final ConcurrentMap> TRIGGER_STATES = new ConcurrentHashMap<>();
+ private static final Map TRIGGER_STATES = Collections.synchronizedMap(new WeakHashMap<>());
public static void setAndTest(final Txn txn, final Trigger trigger, final TriggerPhase triggerPhase, final TriggerEvent triggerEvent, final XmldbURI src, final @Nullable XmldbURI dst) throws CyclicTriggerException {
- final Deque states = getStates(txn);
+ final TriggerStates states = getStates(txn);
if (states.isEmpty()) {
if (triggerPhase != TriggerPhase.BEFORE) {
@@ -123,7 +124,7 @@ public static void clearIfFinished(final Txn txn, final TriggerPhase phase) {
if (phase == TriggerPhase.AFTER) {
int depth = 0;
- final Deque states = getStates(txn);
+ final TriggerStates states = getStates(txn);
for (final Iterator it = states.descendingIterator(); it.hasNext(); ) {
final TriggerState state = it.next();
switch (state.triggerPhase) {
@@ -144,25 +145,37 @@ public static void clearIfFinished(final Txn txn, final TriggerPhase phase) {
}
}
+ public static int keys() {
+ return TRIGGER_STATES.size();
+ }
+
+ public static void clearAll() {
+ TRIGGER_STATES.clear();
+ }
+
public static void clear(final Txn txn) {
- TRIGGER_STATES.remove(txn);
+ TRIGGER_STATES.remove(Thread.currentThread());
}
public static boolean isEmpty(final Txn txn) {
return getStates(txn).isEmpty();
}
- public static void forEach(BiConsumer> action) {
+ public static void dumpTriggerStates() {
+ TRIGGER_STATES.forEach((k, s) -> System.err.format("key: %s, size: %s", k, s.size()).println());
+ }
+
+ public static void forEach(BiConsumer action) {
TRIGGER_STATES.forEach(action);
}
- private static Deque getStates(final Txn txn) {
- return TRIGGER_STATES.computeIfAbsent(txn, TriggerStatePerThread::initStates);
+ private static TriggerStates getStates(final Txn txn) {
+ return TRIGGER_STATES.computeIfAbsent(Thread.currentThread(), key -> new TriggerStates());
}
- private static Deque initStates(final Txn txn) {
+ private static TriggerStates initStates(final Txn txn) {
txn.registerListener(new TransactionCleanUp(txn, TriggerStatePerThread::clear));
- return new ArrayDeque<>();
+ return new TriggerStates();
}
public record TransactionCleanUp(Txn txn, Consumer consumer) implements TxnListener {
@@ -177,6 +190,36 @@ public void abort() {
}
}
+ public static final class TriggerStates extends WeakReference> {
+ public TriggerStates() {
+ super(new ArrayDeque<>());
+ }
+
+ public Iterator descendingIterator() {
+ return get().descendingIterator();
+ }
+
+ public boolean isEmpty() {
+ return get().isEmpty();
+ }
+
+ public int size() {
+ return get().size();
+ }
+
+ public Iterator iterator() {
+ return get().iterator();
+ }
+
+ public TriggerState peekFirst() {
+ return get().peekFirst();
+ }
+
+ public void addFirst(TriggerState newState) {
+ get().addFirst(newState);
+ }
+ }
+
public record TriggerState(Trigger trigger, TriggerPhase triggerPhase, TriggerEvent triggerEvent, XmldbURI src,
@Nullable XmldbURI dst, boolean possiblyCyclic) {
diff --git a/exist-core/src/main/java/org/exist/collections/triggers/XQueryTrigger.java b/exist-core/src/main/java/org/exist/collections/triggers/XQueryTrigger.java
index 887d69e6eb1..c53d02323ba 100644
--- a/exist-core/src/main/java/org/exist/collections/triggers/XQueryTrigger.java
+++ b/exist-core/src/main/java/org/exist/collections/triggers/XQueryTrigger.java
@@ -119,7 +119,11 @@ public class XQueryTrigger extends SAXTrigger implements DocumentTrigger, Collec
private String bindingPrefix = null;
private XQuery service;
- public final static String PREPARE_EXCEPTION_MESSAGE = "Error during trigger prepare";
+ public static final String PREPARE_EXCEPTION_MESSAGE = "Error during trigger prepare";
+
+ public XQueryTrigger() {
+ XQueryTriggerMBeanImpl.init();
+ }
@Override
public void configure(final DBBroker broker, final Txn transaction, final Collection parent, final Map> parameters) throws TriggerException {
diff --git a/exist-core/src/main/java/org/exist/collections/triggers/XQueryTriggerMBean.java b/exist-core/src/main/java/org/exist/collections/triggers/XQueryTriggerMBean.java
new file mode 100644
index 00000000000..f38b8de6092
--- /dev/null
+++ b/exist-core/src/main/java/org/exist/collections/triggers/XQueryTriggerMBean.java
@@ -0,0 +1,11 @@
+package org.exist.collections.triggers;
+
+public interface XQueryTriggerMBean {
+ int getKeys();
+
+ void clear();
+
+ String dumpTriggerStates();
+
+ String listKeys();
+}
diff --git a/exist-core/src/main/java/org/exist/collections/triggers/XQueryTriggerMBeanImpl.java b/exist-core/src/main/java/org/exist/collections/triggers/XQueryTriggerMBeanImpl.java
new file mode 100644
index 00000000000..7b439cf059f
--- /dev/null
+++ b/exist-core/src/main/java/org/exist/collections/triggers/XQueryTriggerMBeanImpl.java
@@ -0,0 +1,54 @@
+package org.exist.collections.triggers;
+
+import javax.management.InstanceAlreadyExistsException;
+import javax.management.MBeanRegistrationException;
+import javax.management.MBeanServer;
+import javax.management.MalformedObjectNameException;
+import javax.management.NotCompliantMBeanException;
+import javax.management.ObjectName;
+import javax.management.StandardMBean;
+import java.lang.management.ManagementFactory;
+import java.util.StringJoiner;
+
+final class XQueryTriggerMBeanImpl extends StandardMBean implements XQueryTriggerMBean {
+
+ private XQueryTriggerMBeanImpl() throws NotCompliantMBeanException {
+ super(XQueryTriggerMBean.class);
+ }
+
+ static void init() {
+ try {
+ final ObjectName name = ObjectName.getInstance("org.exist.management.exist", "type", "TriggerStates");
+ final MBeanServer platformMBeanServer = ManagementFactory.getPlatformMBeanServer();
+ if (!platformMBeanServer.isRegistered(name)) {
+ platformMBeanServer.registerMBean(new XQueryTriggerMBeanImpl(), name);
+ }
+ } catch (final MalformedObjectNameException | InstanceAlreadyExistsException | MBeanRegistrationException | NotCompliantMBeanException ex) {
+ ex.printStackTrace();
+ }
+ }
+
+ @Override
+ public int getKeys() {
+ return TriggerStatePerThread.keys();
+ }
+
+ @Override
+ public void clear() {
+ TriggerStatePerThread.clearAll();
+ }
+
+ @Override
+ public String dumpTriggerStates() {
+ StringJoiner joiner = new StringJoiner("\n");
+ TriggerStatePerThread.forEach((k, v) -> joiner.add("%s: %s".formatted(k, v.size())));
+ return joiner.toString();
+ }
+
+ @Override
+ public String listKeys() {
+ StringJoiner joiner = new StringJoiner("\n");
+ TriggerStatePerThread.forEach((k, v) -> joiner.add("%s".formatted(k)));
+ return joiner.toString();
+ }
+}