forked from DSpace/DSpace
-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathHibernateConcurrencyMonitor.java
More file actions
139 lines (126 loc) · 6.25 KB
/
Copy pathHibernateConcurrencyMonitor.java
File metadata and controls
139 lines (126 loc) · 6.25 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
/**
* The contents of this file are subject to the license and copyright
* detailed in the LICENSE and NOTICE files at the root of the source
* tree and available online at
*
* http://www.dspace.org/license/
*/
package org.dspace;
import java.io.File;
import java.io.PrintWriter;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
/**
* TEST DIAGNOSTIC (temporary) for the rare Hibernate {@code ConcurrentModificationException} thrown from
* {@code ResourceRegistryStandardImpl.releaseResources} during {@code @After} cleanup
* (see {@link AbstractIntegrationTestWithDatabase#destroy()}).
*
* <p>That CME provably requires a SECOND thread to mutate the test thread's per-session, non-thread-safe JDBC
* resource registry while the test thread commits/rolls back. A live thread-dump of a running IT JVM shows that
* NO legitimate background thread ever touches Hibernate (every persistent thread is Solr, HTTP-client, Jetty or
* a JVM thread). Therefore <b>any</b> non-test thread caught executing inside Hibernate JDBC / session code is,
* by definition, the culprit.</p>
*
* <p>This monitor is a JVM-wide background sampler. Every {@link #SAMPLE_INTERVAL_MS} ms it snapshots all thread
* stacks and records (de-duplicated) any non-test, non-monitor thread found inside
* {@code org.hibernate.resource.jdbc}, {@code org.hibernate.engine.jdbc} or {@code org.hibernate.internal.SessionImpl}.
* Records are flushed to {@code target/cme-dumps/} on a captured CME and at JVM shutdown. It is a pure observer:
* it never touches Hibernate, never throws into test code, and never changes behaviour. Delete once the culprit
* thread has been identified and fixed at its source.</p>
*/
public final class HibernateConcurrencyMonitor {
private static final long SAMPLE_INTERVAL_MS = 20;
/** Thread ids of legitimate test threads (the JUnit thread(s)) to ignore. */
private static final Set<Long> TEST_THREAD_IDS = ConcurrentHashMap.newKeySet();
/** De-duplicated culprit fingerprints: key -> formatted record. */
private static final Map<String, String> CULPRITS = new ConcurrentHashMap<>();
private static volatile boolean started;
private HibernateConcurrencyMonitor() {
}
/** Start the monitor exactly once per JVM (fork). Safe to call from every test's setUp. */
public static synchronized void startOnce() {
if (started) {
return;
}
started = true;
Thread t = new Thread(HibernateConcurrencyMonitor::loop, "hibernate-concurrency-monitor");
t.setDaemon(true);
t.start();
Runtime.getRuntime().addShutdownHook(new Thread(() -> flush("jvm-shutdown"), "hibernate-concurrency-flush"));
}
/** Mark the current thread as a legitimate test thread, so its (normal) Hibernate use is ignored. */
public static void markTestThread() {
TEST_THREAD_IDS.add(Thread.currentThread().getId());
}
private static void loop() {
final long monitorId = Thread.currentThread().getId();
while (true) {
try {
Map<Thread, StackTraceElement[]> all = Thread.getAllStackTraces();
for (Map.Entry<Thread, StackTraceElement[]> e : all.entrySet()) {
Thread th = e.getKey();
if (th.getId() == monitorId || TEST_THREAD_IDS.contains(th.getId())) {
continue;
}
if (touchesHibernateJdbc(e.getValue())) {
record(th, e.getValue());
}
}
Thread.sleep(SAMPLE_INTERVAL_MS);
} catch (InterruptedException ie) {
return;
} catch (Throwable ignore) {
// A diagnostic must never die from a transient error (e.g. a thread terminating mid-snapshot).
}
}
}
private static boolean touchesHibernateJdbc(StackTraceElement[] stack) {
for (StackTraceElement f : stack) {
String c = f.getClassName();
if (c.startsWith("org.hibernate.resource.jdbc")
|| c.startsWith("org.hibernate.engine.jdbc")
|| c.startsWith("org.hibernate.internal.SessionImpl")) {
return true;
}
}
return false;
}
private static void record(Thread th, StackTraceElement[] stack) {
StringBuilder sb = new StringBuilder();
int n = Math.min(stack.length, 25);
for (int i = 0; i < n; i++) {
sb.append("\tat ").append(stack[i]).append('\n');
}
String stackText = sb.toString();
String key = th.getName() + "|" + Integer.toHexString(stackText.hashCode());
CULPRITS.putIfAbsent(key, "\"" + th.getName() + "\" id=" + th.getId()
+ " daemon=" + th.isDaemon() + " state=" + th.getState()
+ " group=" + (th.getThreadGroup() == null ? "?" : th.getThreadGroup().getName()) + "\n" + stackText);
}
/** Write all captured culprit fingerprints to target/cme-dumps/ (no-op if none were caught). */
public static void flush(String reason) {
if (CULPRITS.isEmpty()) {
return;
}
try {
File dir = new File("target/cme-dumps");
dir.mkdirs();
File out = new File(dir, "hibernate-concurrency-" + System.currentTimeMillis() + "-" + reason + ".txt");
try (PrintWriter pw = new PrintWriter(out, "UTF-8")) {
pw.println("===== Non-test threads caught INSIDE Hibernate JDBC / session code =====");
pw.println("reason=" + reason + " distinctFingerprints=" + CULPRITS.size());
pw.println("Baseline: NO legitimate background thread touches Hibernate, so each entry below is a");
pw.println("suspect for the @After ConcurrentModificationException (concurrent access to the test");
pw.println("thread's non-thread-safe per-session JDBC ResourceRegistry).");
pw.println();
for (String rec : CULPRITS.values()) {
pw.println(rec);
pw.println("------------------------------------------------------------");
}
}
} catch (Exception ignore) {
// best-effort diagnostic
}
}
}