@@ -114,15 +114,19 @@ private class RxObservableCoroutine<T: Any>(
114
114
// to abort the corresponding send/offer invocation. From the standpoint of coroutines machinery,
115
115
// this failure is essentially equivalent to a failure of a child coroutine.
116
116
cancelCoroutine(e)
117
- doLockedSignalCompleted(e, false )
117
+ mutex.unlock( )
118
118
throw e
119
119
}
120
120
/*
121
- There is no sense to check for `isActive` before doing `unlock`, because cancellation/completion might
122
- happen after this check and before `unlock` (see signalCompleted that does not do anything
123
- if it fails to acquire the lock that we are still holding).
124
- We have to recheck `isCompleted` after `unlock` anyway.
121
+ * There is no sense to check for `isActive` before doing `unlock`, because cancellation/completion might
122
+ * happen after this check and before `unlock` (see signalCompleted that does not do anything
123
+ * if it fails to acquire the lock that we are still holding).
124
+ * We have to recheck `isCompleted` after `unlock` anyway.
125
125
*/
126
+ unlockAndCheckCompleted()
127
+ }
128
+
129
+ private fun unlockAndCheckCompleted () {
126
130
mutex.unlock()
127
131
// recheck isActive
128
132
if (! isActive && mutex.tryLock())
@@ -131,16 +135,32 @@ private class RxObservableCoroutine<T: Any>(
131
135
132
136
// assert: mutex.isLocked()
133
137
private fun doLockedSignalCompleted (cause : Throwable ? , handled : Boolean ) {
134
- // todo: handled is ignored here, might need something like in PublisherCoroutine to process
135
138
// cancellation failures
136
139
try {
137
140
if (_signal .value >= CLOSED ) {
138
141
_signal .value = SIGNALLED // we'll signal onError/onCompleted (that the final state -- no CAS needed)
139
142
try {
140
- if (cause != null && cause !is CancellationException )
141
- subscriber.onError(cause)
142
- else
143
+ if (cause != null && cause !is CancellationException ) {
144
+ /*
145
+ * Reactive frameworks have two types of exceptions: regular and fatal.
146
+ * Regular are passed to onError.
147
+ * Fatal can be passed to onError, but implementation **is free to swallow it** (e.g. see #1297).
148
+ * Such behaviour is inconsistent, leads to silent failures and we can't possibly know whether
149
+ * the cause will be handled by onError (and moreover, it depends on whether a fatal exception was
150
+ * thrown by subscriber or upstream).
151
+ * To make behaviour consistent and least surprising, we always handle fatal exceptions
152
+ * by coroutines machinery, anyway, they should not be present in regular program flow,
153
+ * thus our goal here is just to expose it as soon as possible.
154
+ */
155
+ if (cause.isFatal()) {
156
+ if (! handled) handleCoroutineException(context, cause)
157
+ } else {
158
+ subscriber.onError(cause)
159
+ }
160
+ }
161
+ else {
143
162
subscriber.onComplete()
163
+ }
144
164
} catch (e: Throwable ) {
145
165
// Unhandled exception (cannot handle in other way, since we are already complete)
146
166
handleCoroutineException(context, e)
@@ -164,4 +184,6 @@ private class RxObservableCoroutine<T: Any>(
164
184
override fun onCancelled (cause : Throwable , handled : Boolean ) {
165
185
signalCompleted(cause, handled)
166
186
}
167
- }
187
+ }
188
+
189
+ internal fun Throwable.isFatal () = this is VirtualMachineError || this is ThreadDeath || this is LinkageError
0 commit comments