I feel like I'm not understanding something about RxJava2's error-handling. (btw, I have read RxJava2 observable take throws UndeliverableException and I understand what happens, but not exactly why or if there's a better way to deal with it than what I discuss below. I have also seen RuntimeException thrown and not caught in RxJava2 Single after it has been disposed and it doesn't seem exactly relevant.)
Let's say I have the following, very much simplified example:
val sub = Observable.fromCallable(callableThatCanReturnNull)
.subscribe({ println(it) }, { System.err.println(it) })
And the following sequence of events happens, in order:
sub.dispose()
That darn Callable returns null.
Looking at the RxJava2 source, I see this:
#Override
public void subscribeActual(Observer<? super T> s) {
DeferredScalarDisposable<T> d = new DeferredScalarDisposable<T>(s);
s.onSubscribe(d);
if (d.isDisposed()) {
return;
}
T value;
try {
value = ObjectHelper.requireNonNull(callable.call(), "Callable returned null");
} catch (Throwable e) {
Exceptions.throwIfFatal(e);
if (!d.isDisposed()) {
s.onError(e);
} else {
RxJavaPlugins.onError(e);
}
return;
}
d.complete(value);
}
Apparently sometime between the first d.isDisposed() check and the second, d is disposed, and so we hit RxJavaPlugins.onError(e) with e being a NullPointerException (from ObjectHelper.requireNonNull()). If we have not called RxJavaPlugins.setErrorHandler() with something, then the default behavior is to throw a fatal exception that will crash the application. This seems Bad (tm). My current approach is the following:
GlobalErrorFilter errorFilter = new GlobalErrorFilter(BuildConfig.DEBUG /* alwaysCrash */);
RxJavaPlugins.setErrorHandler(t -> {
if (errorFilter.filter(t)) {
// Crash in some cases
throw new RuntimeException(t);
} else {
// Just log it in others
Logger.e("RxError", t, "Ignoring uncaught Rx exception: %s", t.getLocalizedMessage());
}
});
And my GlobalErrorFilter takes into consideration this fromCallable case and also UndeliverableException. In fact, it looks like this:
class GlobalErrorFilter(private val alwaysCrash: Boolean = false) {
/**
* Returns `true` if app should crash; `false` otherwise. Prefers to return `true`.
*/
fun filter(t: Throwable) = when {
alwaysCrash -> true
t is UndeliverableException -> false
t.localizedMessage.contains("Callable returned null") -> false
else -> true
}
}
This feels really hackish. What I want is to tell RxJava that if I have disposed of an observable, I do not care whether (1) it emits new items (2) it completes (3) it onErrors. I just don't care. Is there a way to do that without this global error handler?
Related
I need to read the content of a collection in real-time. Here is what I have tried:
override fun getItems() = callbackFlow {
val listener = db.collection("items").addSnapshotListener { snapshot, e ->
val response = if (snapshot != null) {
val items = snapshot.toObjects(Item::class.java)
Response.Success(items)
} else {
Response.Error(e)
}
trySend(response).isSuccess //???
}
awaitClose {
listener.remove()
}
}
And it works fine. The problem is that I don't understand the purpose of .isSuccess. Is it mandatory to be added?
trySend() returns a ChannelResult object which contains the result of the operation. If ChannelResult.isSuccess returns true then the response had been successfully sent, otherwise the operation has been failed for some reason (maybe because of the buffer overflow) or because of a coroutine had been finished. You may handle it if you want, but usually it's omitted. Or you may log this result.
so i tried to use onErrorReturn to return the result that i wanted but it will complete the stream afterwards, how do i catch the error return as Next and still continue the stream?
with code below, it wont reach retryWhen when there is error and if i flip it around it wont re-subscribe with retryWhen if there is an error
fun process(): Observable<State> {
return publishSubject
.flatMap { intent ->
actAsRepo(intent) // Might return error
.map { State(data = it, error = null) }
}
.onErrorReturn { State(data = "", error = it) } // catch the error
.retryWhen { errorObs ->
errorObs.flatMap {
Observable.just(State.defaultState()) // continue subscribing
}
}
}
private fun actAsRepo(string: String): Observable<String> {
if (string.contains('A')) {
throw IllegalArgumentException("Contains A")
} else {
return Observable.just("Wrapped from repo: $string")
}
}
subscriber will be
viewModel.process().subscribe(this::render)
onError is a terminal operator. If an onError happens, it will be passed along from operator to operator. You could use an onError-operator which catches the onError and provides a fallback.
In your example the onError happens in the inner-stream of the flatMap. The onError will be propagated downstream to the onErrorReturn opreator. If you look at the implementation, you will see that the onErrorReturn lambda will be invoked, the result will be pushed downstream with onNext following a onComplete
#Override
public void onError(Throwable t) {
T v;
try {
v = valueSupplier.apply(t);
} catch (Throwable e) {
Exceptions.throwIfFatal(e);
downstream.onError(new CompositeException(t, e));
return;
}
if (v == null) {
NullPointerException e = new NullPointerException("The supplied value is null");
e.initCause(t);
downstream.onError(e);
return;
}
downstream.onNext(v); // <--------
downstream.onComplete(); // <--------
}
What is the result of your solution?
Your stream completes because of: #retryWhen JavaDoc
If the upstream to the operator is asynchronous, signalling onNext followed by onComplete immediately may result in the sequence to be completed immediately. Similarly, if this inner {#code ObservableSource} signals {#code onError} or {#code onComplete} while the upstream is active, the sequence is terminated with the same signal immediately.
What you ought to do:
Place the onErrorReturn behind the map opreator in the flatMap. With this ordering your stream will not complete, when the inner-flatMap stream onErrors.
Why is this?
The flatMap operator completes, when the outer (source: publishSubject) and the inner stream (subscription) both complete. In this case the outer stream (publishSubject) emits onNext and the inner-stream will complete after sending { State(data = "", error = it) } via onNext. Therefore the stream will remain open.
interface ApiCall {
fun call(s: String): Observable<String>
}
class ApiCallImpl : ApiCall {
override fun call(s: String): Observable<String> {
// important: warp call into observable, that the exception is caught and emitted as onError downstream
return Observable.fromCallable {
if (s.contains('A')) {
throw IllegalArgumentException("Contains A")
} else {
s
}
}
}
}
data class State(val data: String, val err: Throwable? = null)
apiCallImpl.call will return an lazy observable, which will throw an error on subscription, not at observable assembly time.
// no need for retryWhen here, except you want to catch onComplete from the publishSubject, but once the publishSubject completes no re-subscription will help you, because the publish-subject is terminated and onNext invocations will not be accepted anymore (see implementation).
fun process(): Observable<State> {
return publishSubject
.flatMap { intent ->
apiCallImpl.call(intent) // Might return error
.map { State(data = it, err = null) }
.onErrorReturn { State("", err = it) }
}
}
Test
lateinit var publishSubject: PublishSubject<String>
lateinit var apiCallImpl: ApiCallImpl
#Before
fun init() {
publishSubject = PublishSubject.create()
apiCallImpl = ApiCallImpl()
}
#Test
fun myTest() {
val test = process().test()
publishSubject.onNext("test")
publishSubject.onNext("A")
publishSubject.onNext("test2")
test.assertNotComplete()
.assertNoErrors()
.assertValueCount(3)
.assertValueAt(0) {
assertThat(it).isEqualTo(State("test", null))
true
}
.assertValueAt(1) {
assertThat(it.data).isEmpty()
assertThat(it.err).isExactlyInstanceOf(IllegalArgumentException::class.java)
true
}
.assertValueAt(2) {
assertThat(it).isEqualTo(State("test2", null))
true
}
}
Alternative
This alternative behaves a little bit different, than the first solution. The flatMap-Operator takes a boolean (delayError), which will result in swallowing onError messages, until the sources completes. When the source completes, the errors will be emitted.
You may use delayError true, when the exception is of no use and must not be logged at the time of appearance
process
fun process(): Observable<State> {
return publishSubject
.flatMap({ intent ->
apiCallImpl.call(intent)
.map { State(data = it, err = null) }
}, true)
}
Test
Only two values are emitted. The error will not be transformed to a fallback value.
#Test
fun myTest() {
val test = process().test()
publishSubject.onNext("test")
publishSubject.onNext("A")
publishSubject.onNext("test2")
test.assertNotComplete()
.assertNoErrors()
.assertValueAt(0) {
assertThat(it).isEqualTo(State("test", null))
true
}
.assertValueAt(1) {
assertThat(it).isEqualTo(State("test2", null))
true
}
.assertValueCount(2)
}
NOTE: I think you want to use switchMap in this case, instead of flatMap.
Basically I have to make a network request using OkHttp in parallel to various addresses. I only care about the result of the first one that succeeds. Can I do this with Flow on Kotlin?
I've been looking around but I'm struggling with getting the requests to run in parallel, the always run in sequence.
The code basically takes a list of addresses and should return the only address that worked or null if none worked.
Thanks.
Edit: I should mention I plan on using this on Android. I can probably do it with RX but wanted to learn Flow. Also trying to limit the libraries I add to the app.
Edit: I have marked an answer as correct however that isn't how I did but it took me very close to how I did it but since I'm new to Flow I have no idea if how I did it is correct though I'm pretty sure it works after my testing.
I have a function that throws NoSuchElementException when not found. It calls searchForIPAsync which is a suspend function that does all the OkHttp work and returns true|false.
#Throws(NoSuchElementException::class)
private suspend fun findWorkingIP(ipsToTest: MutableList<String>): String? = ipsToTest
.asFlow()
.flatMapMerge(ipsToTest.size)
{ impl ->
flow<String?> {
val res = connectionHelper.searchForIPAsync(getURLToTest(impl))
if (res) {
emit(impl)
} else {
}
}
}.first()
Then I call this and catch the exception in case nothing is found:
try {
val ipFound = findWorkingIP(ipsToTest)
Log.w(TAG, "find: Got something " + ipFound);
return ipFound
} catch (ex: NoSuchElementException) {
Log.w(TAG, "find: not found");
}
Although the Flow-based solution in another answer is a close match to what you need, unfortunately as of Kotlin 1.3.2 the Flow implementation has a bug that breaks it. The bug already has a proposed fix so this should be resolved with the next patch release of Kotlin. In the meantime, here's a similar solution that uses async and Channel instead:
suspend fun getShortUrl(urls: List<String>): String = coroutineScope {
val chan = Channel<String?>()
urls.forEach { url ->
launch {
try {
fetchUrl(url)
} catch (e: Exception) {
null
}.also { chan.send(it) }
}
}
try {
(1..urls.size).forEach { _ ->
chan.receive()?.also { return#coroutineScope it }
}
throw Exception("All services failed")
} finally {
coroutineContext[Job]!!.cancelChildren()
}
}
I have this function, which works wonderfully.
inline fun <reified T:Any>String.parse() : T {
return GsonBuilder().setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES).create().fromJson<T>(this, T::class.java)
}
fun request (callback: (MyClass)->Unit) {
val url = URL("someurl").readText()
val myObject : MyClass = str.parse()
callback(myObject)
}
net.request {
it.myFunction()
println (it.myString)
}
myObject automatically filled with an object of type MyClass, and returned correctly to the callback.
Now I want to catch the error, and have the callback can return it as well.
fun request (callback: (MyClass?, Exception?)->Unit) {
try {
val url = URL("someurl").readText()
val myObject : MyClass = str.parse()
callback(myObject, null)
}
catch (e: Exception) {
callback(null, e)
}
}
net.request { response, error ->
if (response != null ) { // do something }
else { // report something }
}
But this is ugly, because no matter what, I will have to force the callback to always have two parameters, but only one is present at a time. So I'm searching for optional callback methods. I want to be able to call the method like this:
net.request {
onSuccess { response -> // do something }
onError { error -> // report something }
}
Or probably:
net.request
.onSuccess { response -> // do something }
.onError { error -> // report something }
If I don't want to handle the error, I simply make a call like this:
net.request {
onSuccess { // do something with 'it' }
}
What I can found over the internet is overwriting the existing callback methods like this. This is not what I want. I want to write that callback from scratch. Looking at the source code sometimes doesn't help either because the code is in Java, and I don't understand Java. Not yet.
And I understand that major library in Kotlin like retrofit or JavaRx probably already implement something like this, but I just want to know the bare minimum code needed to do this, as this is how I learn. I just can't find the correct tutorial for this.
You can try this library out. It helps model success/failure operations concisely. Your callback can then take in your MyClass object wrapped this way Result<MyClass, Exception> or just Result.
To pass a value to your callback, you then do Callback(Result.of(MyClass)) for a successful operation or a Callback(Result.of(Exception())) in case of a failure.
You can then consume the callback by using any of the functions below
//if successful
result.success {
}
//if failed
result.failure {
}
//fold is there, if you want to handle both success and failure
result.fold({ value ->
//do something with value
}, { error ->
//do something with error
})
Hope this helps :)
Edit: As #user2340612 pointed out, Result has been added to the Kotlin stdlib. See here for more details
Trying to test new Android Room librarty with RxJava adapter. And I want to handle result if my query returns 0 objects from DB:
So here is DAO method:
#Query("SELECT * FROM auth_info")
fun getAuthInfo(): Flowable<AuthResponse>
And how I handle it:
database.authDao()
.getAuthInfo()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.switchIfEmpty { Log.d(TAG, "IS EMPTY") }
.firstOrError()
.subscribe(
{ authResponse -> Log.d(TAG, authResponse.token) },
{ error -> Log.d(TAG, error.message) })
My DB is empty, so I expect .switchIfEmty() to work, but none of handling methods is firing. Neither .subscribe() nor .switchIfEmpty()
Db Flowables are observable (so they keep dispatching if database changes) so it never completes. You can try returning List<AuthResponse>. We've considered back porting an optional but decided not to do it, at least for now. Instead, we'll probably add support for Optional in different known libraries.
In version 1.0.0-alpha5, room added support of Maybe and Single to DAOs, so now you can write something like
#Query("SELECT * FROM auth_info")
fun getAuthInfo(): Maybe<AuthResponse>
You can read more about it here
switchIfEmpty takes as parameter a Publisher<AuthResponse>. Through SAM-conversion your given anonymous function is turned into this class. However it does not follow the behavior expected from a Publisher so it will not work as expected.
Replace it with a correct implementation like Flowable.empty().doOnSubscribe { Log.d(TAG, "IS EMPTY") } and it should work.
You could use some wrapper for result. For example:
public Single<QueryResult<Transaction>> getTransaction(long id) {
return createSingle(() -> database.getTransactionDao().getTransaction(id))
.map(QueryResult::new);
}
public class QueryResult<D> {
public D data;
public QueryResult() {}
public QueryResult(D data) {
this.data = data;
}
public boolean isEmpty(){
return data != null;
}
}
protected <T> Single<T> createSingle(final Callable<T> func) {
return Single.create(emitter -> {
try {
T result = func.call();
emitter.onSuccess(result);
} catch (Exception ex) {
Log.e("TAG", "Error of operation with db");
}
});
}
And use it like 'Single' in this case you will get result in any case. Use:
dbStorage.getTransaction(selectedCoin.getId())
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(r -> {
if(!r.isEmpty()){
// we have some data from DB
} else {
}
})