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(); + } +}