Skip to content

Commit b40e89f

Browse files
Merge pull request #1388 from mattrjacobs/eliminate-circular-exception-creation
CompositeException stops mutating nested Exceptions
2 parents 05c636c + 8e27cdc commit b40e89f

File tree

3 files changed

+182
-108
lines changed

3 files changed

+182
-108
lines changed

rxjava-core/src/main/java/rx/exceptions/CompositeException.java

Lines changed: 107 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -15,38 +15,44 @@
1515
*/
1616
package rx.exceptions;
1717

18+
import java.io.PrintStream;
19+
import java.io.PrintWriter;
1820
import java.util.ArrayList;
1921
import java.util.Collection;
2022
import java.util.Collections;
21-
import java.util.HashSet;
23+
import java.util.LinkedHashSet;
2224
import java.util.List;
2325
import java.util.Set;
2426

2527
/**
2628
* Exception that is a composite of 1 or more other exceptions.
27-
* <p>
28-
* Use <code>getMessage()</code> to retrieve a concatenation of the composite exceptions.
29+
* A CompositeException does not modify the structure of any exception it wraps, but at print-time
30+
* iterates through the list of contained Throwables to print them all.
31+
*
32+
* Its invariant is to contains an immutable, ordered (by insertion order), unique list of non-composite exceptions.
33+
* This list may be queried by {@code #getExceptions()}
2934
*/
3035
public final class CompositeException extends RuntimeException {
3136

3237
private static final long serialVersionUID = 3026362227162912146L;
3338

3439
private final List<Throwable> exceptions;
3540
private final String message;
36-
private final Throwable cause;
3741

3842
public CompositeException(String messagePrefix, Collection<Throwable> errors) {
43+
Set<Throwable> deDupedExceptions = new LinkedHashSet<Throwable>();
3944
List<Throwable> _exceptions = new ArrayList<Throwable>();
40-
CompositeExceptionCausalChain _cause = new CompositeExceptionCausalChain();
41-
int count = 0;
42-
for (Throwable e : errors) {
43-
count++;
44-
attachCallingThreadStack(_cause, e);
45-
_exceptions.add(e);
45+
for (Throwable ex: errors) {
46+
if (ex instanceof CompositeException) {
47+
deDupedExceptions.addAll(((CompositeException) ex).getExceptions());
48+
} else {
49+
deDupedExceptions.add(ex);
50+
}
4651
}
52+
53+
_exceptions.addAll(deDupedExceptions);
4754
this.exceptions = Collections.unmodifiableList(_exceptions);
48-
this.message = count + " exceptions occurred. See them in causal chain below.";
49-
this.cause = _cause;
55+
this.message = exceptions.size() + " exceptions occurred. See them in causal chain below.";
5056
}
5157

5258
public CompositeException(Collection<Throwable> errors) {
@@ -70,57 +76,106 @@ public String getMessage() {
7076

7177
@Override
7278
public synchronized Throwable getCause() {
73-
return cause;
79+
return null;
7480
}
7581

76-
@SuppressWarnings("unused")
77-
// useful when debugging but don't want to make part of publicly supported API
78-
private static String getStackTraceAsString(StackTraceElement[] stack) {
79-
StringBuilder s = new StringBuilder();
80-
boolean firstLine = true;
81-
for (StackTraceElement e : stack) {
82-
if (e.toString().startsWith("java.lang.Thread.getStackTrace")) {
83-
// we'll ignore this one
84-
continue;
85-
}
86-
if (!firstLine) {
87-
s.append("\n\t");
88-
}
89-
s.append(e.toString());
90-
firstLine = false;
91-
}
92-
return s.toString();
82+
/**
83+
* All of the following printStackTrace functionality is derived from JDK Throwable printStackTrace.
84+
* In particular, the PrintStreamOrWriter abstraction is copied wholesale.
85+
*
86+
* Changes from the official JDK implementation:
87+
* * No infinite loop detection
88+
* * Smaller critical section holding printStream lock
89+
* * Explicit knowledge about exceptions List that this loops through
90+
*/
91+
@Override
92+
public void printStackTrace() {
93+
printStackTrace(System.err);
9394
}
9495

95-
/* package-private */ static void attachCallingThreadStack(Throwable e, Throwable cause) {
96-
Set<Throwable> seenCauses = new HashSet<Throwable>();
96+
@Override
97+
public void printStackTrace(PrintStream s) {
98+
printStackTrace(new WrappedPrintStream(s));
99+
}
97100

98-
while (e.getCause() != null) {
99-
e = e.getCause();
100-
if (seenCauses.contains(e.getCause())) {
101-
break;
102-
} else {
103-
seenCauses.add(e.getCause());
104-
}
101+
@Override
102+
public void printStackTrace(PrintWriter s) {
103+
printStackTrace(new WrappedPrintWriter(s));
104+
}
105+
106+
/**
107+
* Special handling for printing out a CompositeException
108+
* Loop through all inner exceptions and print them out
109+
* @param s stream to print to
110+
*/
111+
private void printStackTrace(PrintStreamOrWriter s) {
112+
StringBuilder bldr = new StringBuilder();
113+
bldr.append(this).append("\n");
114+
for (StackTraceElement myStackElement: getStackTrace()) {
115+
bldr.append("\tat ").append(myStackElement).append("\n");
116+
}
117+
int i = 1;
118+
for (Throwable ex: exceptions) {
119+
bldr.append(" ComposedException ").append(i).append(" :").append("\n");
120+
appendStackTrace(bldr, ex, "\t");
121+
i++;
105122
}
106-
// we now have 'e' as the last in the chain
107-
try {
108-
e.initCause(cause);
109-
} catch (Throwable t) {
110-
// ignore
111-
// the javadocs say that some Throwables (depending on how they're made) will never
112-
// let me call initCause without blowing up even if it returns null
123+
synchronized (s.lock()) {
124+
s.println(bldr.toString());
113125
}
114126
}
115127

116-
/* package-private */ final static class CompositeExceptionCausalChain extends RuntimeException {
117-
private static final long serialVersionUID = 3875212506787802066L;
118-
/* package-private */ static String MESSAGE = "Chain of Causes for CompositeException In Order Received =>";
128+
private void appendStackTrace(StringBuilder bldr, Throwable ex, String prefix) {
129+
bldr.append(prefix).append(ex).append("\n");
130+
for (StackTraceElement stackElement: ex.getStackTrace()) {
131+
bldr.append("\t\tat ").append(stackElement).append("\n");
132+
}
133+
if (ex.getCause() != null) {
134+
bldr.append("\tCaused by: ");
135+
appendStackTrace(bldr, ex.getCause(), "");
136+
}
137+
}
138+
139+
private abstract static class PrintStreamOrWriter {
140+
/** Returns the object to be locked when using this StreamOrWriter */
141+
abstract Object lock();
142+
143+
/** Prints the specified string as a line on this StreamOrWriter */
144+
abstract void println(Object o);
145+
}
146+
147+
/**
148+
* Same abstraction and implementation as in JDK to allow PrintStream and PrintWriter to share implementation
149+
*/
150+
private static class WrappedPrintStream extends PrintStreamOrWriter {
151+
private final PrintStream printStream;
152+
153+
WrappedPrintStream(PrintStream printStream) {
154+
this.printStream = printStream;
155+
}
156+
157+
Object lock() {
158+
return printStream;
159+
}
119160

120-
@Override
121-
public String getMessage() {
122-
return MESSAGE;
161+
void println(Object o) {
162+
printStream.println(o);
123163
}
124164
}
125165

166+
private static class WrappedPrintWriter extends PrintStreamOrWriter {
167+
private final PrintWriter printWriter;
168+
169+
WrappedPrintWriter(PrintWriter printWriter) {
170+
this.printWriter = printWriter;
171+
}
172+
173+
Object lock() {
174+
return printWriter;
175+
}
176+
177+
void println(Object o) {
178+
printWriter.println(o);
179+
}
180+
}
126181
}

rxjava-core/src/test/java/rx/exceptions/CompositeExceptionTest.java

Lines changed: 39 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@
1717

1818
import static org.junit.Assert.assertEquals;
1919

20+
import java.io.ByteArrayOutputStream;
21+
import java.io.PrintStream;
22+
import java.io.StringWriter;
2023
import java.util.ArrayList;
2124
import java.util.Arrays;
2225
import java.util.List;
@@ -30,7 +33,6 @@ public class CompositeExceptionTest {
3033
private final Throwable ex3 = new Throwable("Ex3", ex2);
3134

3235
public CompositeExceptionTest() {
33-
ex1.initCause(ex2);
3436
}
3537

3638
private CompositeException getNewCompositeExceptionWithEx123() {
@@ -50,46 +52,58 @@ public void testMultipleWithSameCause() {
5052
CompositeException ce = new CompositeException("3 failures with same root cause", Arrays.asList(e1, e2, e3));
5153

5254
assertEquals(3, ce.getExceptions().size());
53-
55+
assertNoCircularReferences(ce);
5456
}
5557

5658
@Test(timeout = 1000)
57-
public void testAttachCallingThreadStackParentThenChild() {
58-
CompositeException.attachCallingThreadStack(ex1, ex2);
59-
assertEquals("Ex2", ex1.getCause().getMessage());
59+
public void testCompositeExceptionFromParentThenChild() {
60+
CompositeException cex = new CompositeException(Arrays.asList(ex1, ex2));
61+
assertNoCircularReferences(cex);
62+
assertEquals(2, cex.getExceptions().size());
6063
}
6164

6265
@Test(timeout = 1000)
63-
public void testAttachCallingThreadStackChildThenParent() {
64-
CompositeException.attachCallingThreadStack(ex2, ex1);
65-
assertEquals("Ex1", ex2.getCause().getMessage());
66+
public void testCompositeExceptionFromChildThenParent() {
67+
CompositeException cex = new CompositeException(Arrays.asList(ex2, ex1));
68+
assertNoCircularReferences(cex);
69+
assertEquals(2, cex.getExceptions().size());
6670
}
6771

6872
@Test(timeout = 1000)
69-
public void testAttachCallingThreadStackAddComposite() {
70-
CompositeException.attachCallingThreadStack(ex1, getNewCompositeExceptionWithEx123());
71-
assertEquals("Ex2", ex1.getCause().getMessage());
73+
public void testCompositeExceptionFromChildAndComposite() {
74+
CompositeException cex = new CompositeException(Arrays.asList(ex1, getNewCompositeExceptionWithEx123()));
75+
assertNoCircularReferences(cex);
76+
assertEquals(3, cex.getExceptions().size());
7277
}
7378

7479
@Test(timeout = 1000)
75-
public void testAttachCallingThreadStackAddToComposite() {
76-
CompositeException compositeEx = getNewCompositeExceptionWithEx123();
77-
CompositeException.attachCallingThreadStack(compositeEx, ex1);
78-
assertEquals(CompositeException.CompositeExceptionCausalChain.MESSAGE, compositeEx.getCause().getMessage());
80+
public void testCompositeExceptionFromCompositeAndChild() {
81+
CompositeException cex = new CompositeException(Arrays.asList(getNewCompositeExceptionWithEx123(), ex1));
82+
assertNoCircularReferences(cex);
83+
assertEquals(3, cex.getExceptions().size());
7984
}
8085

8186
@Test(timeout = 1000)
82-
public void testAttachCallingThreadStackAddCompositeToItself() {
83-
CompositeException compositeEx = getNewCompositeExceptionWithEx123();
84-
CompositeException.attachCallingThreadStack(compositeEx, compositeEx);
85-
assertEquals(CompositeException.CompositeExceptionCausalChain.MESSAGE, compositeEx.getCause().getMessage());
87+
public void testCompositeExceptionFromTwoDuplicateComposites() {
88+
List<Throwable> exs = new ArrayList<Throwable>();
89+
exs.add(getNewCompositeExceptionWithEx123());
90+
exs.add(getNewCompositeExceptionWithEx123());
91+
CompositeException cex = new CompositeException(exs);
92+
assertNoCircularReferences(cex);
93+
assertEquals(3, cex.getExceptions().size());
8694
}
8795

88-
@Test(timeout = 1000)
89-
public void testAttachCallingThreadStackAddExceptionsToEachOther() {
90-
CompositeException.attachCallingThreadStack(ex1, ex2);
91-
CompositeException.attachCallingThreadStack(ex2, ex1);
92-
assertEquals("Ex2", ex1.getCause().getMessage());
93-
assertEquals("Ex1", ex2.getCause().getMessage());
96+
/**
97+
* This hijacks the Throwable.printStackTrace() output and puts it in a string, where we can look for
98+
* "CIRCULAR REFERENCE" (a String added by Throwable.printEnclosedStackTrace)
99+
*/
100+
private static void assertNoCircularReferences(Throwable ex) {
101+
ByteArrayOutputStream baos = new ByteArrayOutputStream();
102+
PrintStream printStream = new PrintStream(baos);
103+
StringWriter writer = new StringWriter();
104+
105+
ex.printStackTrace();
106+
//ex.printStackTrace(printStream);
107+
//assertFalse(baos.toString().contains("CIRCULAR REFERENCE"));
94108
}
95109
}

0 commit comments

Comments
 (0)