Skip to content

Commit 2ec9512

Browse files
committed
added PreserveFullStackTraceOperator for better traceability
1 parent 7752678 commit 2ec9512

File tree

5 files changed

+201
-23
lines changed

5 files changed

+201
-23
lines changed

build.gradle

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ def commonsLang3Version = '3.4'
2828
def groovyVersion = '2.4.5'
2929
def spockVersion = '1.0-groovy-2.4'
3030
def cglibVersion = '3.2.0'
31+
def awaitilityVersion = '1.7.0'
3132
def swingXVersion = '1.6.5-1'
3233

3334
dependencies {
@@ -44,6 +45,7 @@ dependencies {
4445
exclude group: 'org.codehaus.groovy'
4546
}
4647
testCompile("cglib:cglib:${cglibVersion}") // spock mocks
48+
testCompile "com.jayway.awaitility:awaitility:${awaitilityVersion}"
4749
}
4850

4951
sourceSets.main.java.srcDirs = ['src/main/java']

src/main/java/ch/petikoch/examples/mvvm_rxjava/example9/Example_9_View.java

Lines changed: 28 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@
2424
import java.awt.*;
2525
import java.lang.management.ManagementFactory;
2626

27+
import static ch.petikoch.examples.mvvm_rxjava.utils.PreserveFullStackTraceOperator.preserveFullStackTrace;
28+
2729
class Example_9_View extends StrictThreadingJFrame implements IView<Example_9_ViewModel> {
2830

2931
private final Example_9_View_StatusPanel statusPanel = new Example_9_View_StatusPanel();
@@ -33,32 +35,35 @@ class Example_9_View extends StrictThreadingJFrame implements IView<Example_9_Vi
3335
@Override
3436
public void bind(final Example_9_ViewModel viewModel) {
3537
statusPanel.bind(viewModel.vm2v_status);
36-
viewModel.vm2v_mainPanel.observeOn(SwingScheduler.getInstance()).subscribe(mainContentViewModel -> {
37-
if (mainContentViewModel instanceof Example_9_ViewModel_Step1) {
38-
Example_9_View_Step1Panel step1Panel = new Example_9_View_Step1Panel();
39-
step1Panel.bind((Example_9_ViewModel_Step1) mainContentViewModel);
38+
viewModel.vm2v_mainPanel
39+
.observeOn(SwingScheduler.getInstance())
40+
.lift(preserveFullStackTrace())
41+
.subscribe(mainContentViewModel -> {
42+
if (mainContentViewModel instanceof Example_9_ViewModel_Step1) {
43+
Example_9_View_Step1Panel step1Panel = new Example_9_View_Step1Panel();
44+
step1Panel.bind((Example_9_ViewModel_Step1) mainContentViewModel);
4045

41-
mainPanel.removeAll();
42-
mainPanel.add(step1Panel, BorderLayout.CENTER);
43-
mainPanel.revalidate();
44-
} else if (mainContentViewModel instanceof Example_9_ViewModel_Step2) {
45-
Example_9_View_Step2Panel step2Panel = new Example_9_View_Step2Panel();
46-
step2Panel.bind((Example_9_ViewModel_Step2) mainContentViewModel);
46+
mainPanel.removeAll();
47+
mainPanel.add(step1Panel, BorderLayout.CENTER);
48+
mainPanel.revalidate();
49+
} else if (mainContentViewModel instanceof Example_9_ViewModel_Step2) {
50+
Example_9_View_Step2Panel step2Panel = new Example_9_View_Step2Panel();
51+
step2Panel.bind((Example_9_ViewModel_Step2) mainContentViewModel);
4752

48-
mainPanel.removeAll();
49-
mainPanel.add(step2Panel, BorderLayout.CENTER);
50-
mainPanel.revalidate();
51-
} else if (mainContentViewModel instanceof Example_9_ViewModel_Step3) {
52-
Example__View_Step3Panel step3Panel = new Example__View_Step3Panel();
53-
step3Panel.bind((Example_9_ViewModel_Step3) mainContentViewModel);
53+
mainPanel.removeAll();
54+
mainPanel.add(step2Panel, BorderLayout.CENTER);
55+
mainPanel.revalidate();
56+
} else if (mainContentViewModel instanceof Example_9_ViewModel_Step3) {
57+
Example__View_Step3Panel step3Panel = new Example__View_Step3Panel();
58+
step3Panel.bind((Example_9_ViewModel_Step3) mainContentViewModel);
5459

55-
mainPanel.removeAll();
56-
mainPanel.add(step3Panel, BorderLayout.CENTER);
57-
mainPanel.revalidate();
58-
} else {
59-
throw new IllegalStateException("Unhandled: " + mainContentViewModel);
60-
}
61-
});
60+
mainPanel.removeAll();
61+
mainPanel.add(step3Panel, BorderLayout.CENTER);
62+
mainPanel.revalidate();
63+
} else {
64+
throw new IllegalStateException("Unhandled: " + mainContentViewModel);
65+
}
66+
});
6267
}
6368

6469
public Example_9_View() {

src/main/java/ch/petikoch/examples/mvvm_rxjava/rxjava_mvvm/RxViewModel2SwingViewBinder.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@
2222
import javax.swing.*;
2323
import javax.swing.text.JTextComponent;
2424

25+
import static ch.petikoch.examples.mvvm_rxjava.utils.PreserveFullStackTraceOperator.preserveFullStackTrace;
26+
2527
public class RxViewModel2SwingViewBinder {
2628

2729
public static BooleanBindOfAble bindViewModelBoolean(Observable<Boolean> source) {
@@ -47,6 +49,7 @@ private BooleanBindOfAble(final Observable<Boolean> source) {
4749
public void toSwingViewEnabledPropertyOf(JComponent target) {
4850
source.onBackpressureLatest()
4951
.observeOn(SwingScheduler.getInstance())
52+
.lift(preserveFullStackTrace())
5053
.subscribe(target::setEnabled);
5154
}
5255
}
@@ -62,12 +65,14 @@ private StringBindOfAble(final Observable<String> source) {
6265
public void toSwingViewText(JTextComponent target) {
6366
source.onBackpressureLatest()
6467
.observeOn(SwingScheduler.getInstance())
68+
.lift(preserveFullStackTrace())
6569
.subscribe(target::setText);
6670
}
6771

6872
public void toSwingViewLabel(JLabel target) {
6973
source.onBackpressureLatest()
7074
.observeOn(SwingScheduler.getInstance())
75+
.lift(preserveFullStackTrace())
7176
.subscribe(target::setText);
7277
}
7378
}
@@ -82,6 +87,7 @@ private BindOfAble(final Observable<T> source) {
8287

8388
public void toAction(Action1<T> action) {
8489
source.observeOn(SwingScheduler.getInstance())
90+
.lift(preserveFullStackTrace())
8591
.subscribe(action);
8692
}
8793
}
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
/**
2+
* Copyright 2015 Peti Koch
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package ch.petikoch.examples.mvvm_rxjava.utils;
17+
18+
import com.google.common.collect.Iterables;
19+
import com.google.common.collect.Lists;
20+
import rx.Observable;
21+
import rx.Scheduler;
22+
import rx.Subscriber;
23+
24+
import java.util.List;
25+
import java.util.stream.Collectors;
26+
27+
import static java.util.Arrays.asList;
28+
29+
/**
30+
* A RxJava {@link rx.Observable.Operator} which takes care to preserve stacktraces across asynchronous
31+
* {@link rx.Scheduler}-boundaries.
32+
* <p>
33+
* This {@link rx.Observable.Operator} is typically placed AFTER {@link Observable#observeOn(Scheduler)} or {@link
34+
* Observable#subscribeOn(Scheduler)}.
35+
* <p>
36+
* See <a href="https://github.com/ReactiveX/RxJava/issues/3521#issuecomment-163571622">RxJava Issue 3521</a> for more
37+
* background about the topic. This {@link rx.Observable.Operator} can be used as kind of a workaround for this unresolved
38+
* RxJava issue (concerning RxJava versions <= 1.1.0 and probably also later versions).
39+
* <p>
40+
* Advantages: Analysing issues (stacktraces) gets much simpler for asynchronous use cases, since you have a "full" stacktrace<br>
41+
* Disadvantage: Performance. Creating the internal RuntimeException-object to capture the stacktrace is "not cheap".
42+
* But since we are operating at an asynchronous boundary, this doesn't add much additional overhead and can probably be neglected
43+
*/
44+
public class PreserveFullStackTraceOperator<T> implements Observable.Operator<T, T> {
45+
46+
private final RuntimeException asyncOriginStackTraceProvider = new RuntimeException("async origin");
47+
private final long originThreadId = Thread.currentThread().getId(); // should be "enough" unique. See http://stackoverflow.com/a/591664/1662412
48+
49+
public static <T> PreserveFullStackTraceOperator<T> preserveFullStackTrace() {
50+
return new PreserveFullStackTraceOperator<>();
51+
}
52+
53+
@Override
54+
public Subscriber<? super T> call(Subscriber<? super T> child) {
55+
Subscriber<T> parent = new Subscriber<T>() {
56+
57+
@Override
58+
public void onCompleted() {
59+
child.onCompleted();
60+
}
61+
62+
@Override
63+
public void onError(Throwable throwable) {
64+
if (Thread.currentThread().getId() != originThreadId) {
65+
List<StackTraceElement> originalStackTraceElements = Lists.newArrayList(throwable.getStackTrace());
66+
List<StackTraceElement> additionalAsyncOriginStackTraceElements = asList(asyncOriginStackTraceProvider.getStackTrace()).stream()
67+
.filter(stackTraceElement -> !PreserveFullStackTraceOperator.class.getName().equals(stackTraceElement.getClassName()))
68+
.collect(Collectors.toList());
69+
Iterable<StackTraceElement> modifiedStackTraceElements = Iterables.concat(originalStackTraceElements, additionalAsyncOriginStackTraceElements);
70+
throwable.setStackTrace(Iterables.toArray(modifiedStackTraceElements, StackTraceElement.class));
71+
}
72+
child.onError(throwable);
73+
}
74+
75+
@Override
76+
public void onNext(T t) {
77+
child.onNext(t);
78+
}
79+
};
80+
81+
child.add(parent);
82+
83+
return parent;
84+
}
85+
86+
}
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
/*
2+
* Copyright 2015 Peti Koch
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package ch.petikoch.examples.mvvm_rxjava.utils
17+
18+
import com.jayway.awaitility.Awaitility
19+
import rx.Observable
20+
import rx.observers.TestSubscriber
21+
import rx.schedulers.Schedulers
22+
import spock.lang.Specification
23+
24+
import java.util.concurrent.Callable
25+
26+
import static ch.petikoch.examples.mvvm_rxjava.utils.PreserveFullStackTraceOperator.preserveFullStackTrace
27+
28+
class PreserveFullStackTraceOperatorTest extends Specification {
29+
30+
def 'operator adds asynchronous origin stack trace elements'() {
31+
given:
32+
def asyncPipeline = Observable.fromCallable { throw new IllegalStateException("Boom!") }
33+
.subscribeOn(Schedulers.io())
34+
def originalThrowable = getThrowableFromFailingObservable(asyncPipeline)
35+
assert originalThrowable instanceof IllegalStateException
36+
assert originalThrowable.getMessage() == 'Boom!'
37+
assert originalThrowable.getCause() == null
38+
39+
when:
40+
def enhancedThrowable = getThrowableFromFailingObservable(
41+
asyncPipeline.lift(preserveFullStackTrace())
42+
)
43+
44+
then:
45+
enhancedThrowable instanceof IllegalStateException
46+
enhancedThrowable.getMessage() == 'Boom!'
47+
enhancedThrowable.stackTrace.length > originalThrowable.stackTrace.length
48+
enhancedThrowable.stackTrace.any { it.className == this.class.name } == true
49+
enhancedThrowable.getCause() == null
50+
}
51+
52+
53+
def 'operator does not add additional stack trace elements if code is synchronous'() {
54+
given:
55+
def syncPipeline = Observable.fromCallable { throw new IllegalStateException("Boom!") }
56+
def originalThrowable = getThrowableFromFailingObservable(syncPipeline)
57+
assert originalThrowable instanceof IllegalStateException
58+
assert originalThrowable.getMessage() == 'Boom!'
59+
assert originalThrowable.getCause() == null
60+
61+
when:
62+
def enhancedThrowable = getThrowableFromFailingObservable(
63+
syncPipeline.lift(preserveFullStackTrace())
64+
)
65+
66+
then:
67+
enhancedThrowable instanceof IllegalStateException
68+
enhancedThrowable.getMessage() == 'Boom!'
69+
enhancedThrowable.stackTrace.length == originalThrowable.stackTrace.length
70+
enhancedThrowable.getCause() == null
71+
}
72+
73+
private Throwable getThrowableFromFailingObservable(Observable observable) {
74+
TestSubscriber testSubscriber = new TestSubscriber()
75+
observable.subscribe(testSubscriber)
76+
Awaitility.await().until({ testSubscriber.getOnErrorEvents().size() == 1 } as Callable<Boolean>)
77+
return testSubscriber.getOnErrorEvents().get(0)
78+
}
79+
}

0 commit comments

Comments
 (0)