From 89af9779c0c02811bf9a67c0681629257a37bc0c Mon Sep 17 00:00:00 2001 From: Kaushik Gopal Date: Tue, 30 Jun 2015 15:14:58 -0700 Subject: [PATCH 001/173] feat: add my RxUtils class with CompositeSubscription helpers --- .../com/morihacky/android/rxjava/RxUtils.java | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 app/src/main/java/com/morihacky/android/rxjava/RxUtils.java diff --git a/app/src/main/java/com/morihacky/android/rxjava/RxUtils.java b/app/src/main/java/com/morihacky/android/rxjava/RxUtils.java new file mode 100644 index 00000000..100b9aa0 --- /dev/null +++ b/app/src/main/java/com/morihacky/android/rxjava/RxUtils.java @@ -0,0 +1,21 @@ +package com.morihacky.android.rxjava; + +import rx.Subscription; +import rx.subscriptions.CompositeSubscription; + +public class RxUtils { + + public static void unsubscribeIfNotNull(Subscription subscription) { + if (subscription != null) { + subscription.unsubscribe(); + } + } + + public static CompositeSubscription getNewCompositeSubIfUnsubscribed(CompositeSubscription subscription) { + if (subscription == null || subscription.isUnsubscribed()) { + return new CompositeSubscription(); + } + + return subscription; + } +} From 481e17519d808b2ac5a6c0e459881c7117572024 Mon Sep 17 00:00:00 2001 From: Kaushik Gopal Date: Tue, 30 Jun 2015 15:15:51 -0700 Subject: [PATCH 002/173] fix: @mattlogan called me out for being lazy and not doing the right thing. unsubscribe responsibly so no mem leaking --- .../android/rxjava/RetrofitFragment.java | 81 +++++++++++-------- 1 file changed, 49 insertions(+), 32 deletions(-) diff --git a/app/src/main/java/com/morihacky/android/rxjava/RetrofitFragment.java b/app/src/main/java/com/morihacky/android/rxjava/RetrofitFragment.java index 04195e3b..10bc0e05 100644 --- a/app/src/main/java/com/morihacky/android/rxjava/RetrofitFragment.java +++ b/app/src/main/java/com/morihacky/android/rxjava/RetrofitFragment.java @@ -13,7 +13,6 @@ import butterknife.ButterKnife; import butterknife.InjectView; import butterknife.OnClick; -import com.morihacky.android.rxjava.R; import com.morihacky.android.rxjava.retrofit.Contributor; import com.morihacky.android.rxjava.retrofit.GithubApi; import com.morihacky.android.rxjava.retrofit.User; @@ -27,6 +26,7 @@ import rx.functions.Func1; import rx.functions.Func2; import rx.schedulers.Schedulers; +import rx.subscriptions.CompositeSubscription; import timber.log.Timber; import static com.google.common.base.Strings.isNullOrEmpty; @@ -41,6 +41,7 @@ public class RetrofitFragment private GithubApi _api; private ArrayAdapter _adapter; + private CompositeSubscription _subscriptions = new CompositeSubscription(); @Override public void onCreate(Bundle savedInstanceState) { @@ -48,6 +49,19 @@ public void onCreate(Bundle savedInstanceState) { _api = _createGithubApi(); } + @Override + public void onResume() { + super.onResume(); + _subscriptions = RxUtils.getNewCompositeSubIfUnsubscribed(_subscriptions); + } + + @Override + public void onPause() { + super.onPause(); + + RxUtils.unsubscribeIfNotNull(_subscriptions); + } + @Override public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @@ -70,42 +84,45 @@ public View onCreateView(LayoutInflater inflater, public void onListContributorsClicked() { _adapter.clear(); - _api.contributors(_username.getText().toString(), _repo.getText().toString()) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(new Observer>() { - @Override - public void onCompleted() { - Timber.d("Retrofit call 1 completed"); - } - - @Override - public void onError(Throwable e) { - Timber.e(e, "woops we got an error while getting the list of contributors"); - } - - @Override - public void onNext(List contributors) { - for (Contributor c : contributors) { - _adapter.add(format("%s has made %d contributions to %s", - c.login, - c.contributions, - _repo.getText().toString())); - - Timber.d("%s has made %d contributions to %s", - c.login, - c.contributions, - _repo.getText().toString()); - } - } - }); + _subscriptions.add(// + _api.contributors(_username.getText().toString(), _repo.getText().toString()) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(new Observer>() { + @Override + public void onCompleted() { + Timber.d("Retrofit call 1 completed"); + } + + @Override + public void onError(Throwable e) { + Timber.e(e, + "woops we got an error while getting the list of contributors"); + } + + @Override + public void onNext(List contributors) { + for (Contributor c : contributors) { + _adapter.add(format("%s has made %d contributions to %s", + c.login, + c.contributions, + _repo.getText().toString())); + + Timber.d("%s has made %d contributions to %s", + c.login, + c.contributions, + _repo.getText().toString()); + } + } + })); } @OnClick(R.id.btn_demo_retrofit_contributors_with_user_info) public void onListContributorsWithFullUserInfoClicked() { _adapter.clear(); - _api.contributors(_username.getText().toString(), _repo.getText().toString()) + _subscriptions.add(_api.contributors(_username.getText().toString(), + _repo.getText().toString()) .flatMap(new Func1, Observable>() { @Override public Observable call(List contributors) { @@ -167,7 +184,7 @@ public void onNext(Pair pair) { contributor.contributions, _repo.getText().toString()); } - }); + })); } // ----------------------------------------------------------------------------------- From 4b2180fe098cc477b701c5fdbf6eedfac8cef947 Mon Sep 17 00:00:00 2001 From: Kaushik Gopal Date: Thu, 2 Jul 2015 22:17:01 -0700 Subject: [PATCH 003/173] feat: add explanation for exponential backoff strategy with intended examples --- README.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/README.md b/README.md index d403de60..f7cc9d49 100644 --- a/README.md +++ b/README.md @@ -100,11 +100,30 @@ Cases demonstrated here: 3. run a task constantly every 1s (same as above but there's no delay before the first task fires off) 4. run a task constantly every 3s, but after running it 5 times, terminate automatically +### Exponential backoff + +[Exponential backoff](https://en.wikipedia.org/wiki/Exponential_backoff) is a strategy where based on feedback from a certain output we alter the rate of a process (usually reduce the number of retries or increase the wait time before retrying or re-executing a cetain process). + +It makes far more sense with examples, and RxJava makes it (relatively) simple to achieve such a strategy. + +#### retry (if error) with exponential backoff + +Say you have a network failure. A sensible strategy would be to NOT keep retrying your network call every 1 second. It would be smart instead (nay... elegant!) to retry with increasing delays. So you try at second 1 to execute the network call, no dice? try after 10 seconds... negatory? try after 20 seconds, no cookie? try after 1 minute. If this thing is still failing, you got to give up on the network yo! + +We simulate this behaviour using RxJava with the `retryWhen` + +#### "repeat" with exponential backoff + +Another variant of the exponential backoff strategy is to execute an operation repeatedly for a given number of times, but with delayed intervals. So you execute a certain operation 1 second from now, then you execute it again 10 seconds from now, then you execute the operation 20 seconds from now. After a grand total of 3 times you stop executing. + +Simulating this behavior is actually way more simpler than the retry mechanism. You can use a variant of the `delay` operator to achieve this. + ## Work in Progress: Examples that I would like to have here, but haven't found the time yet to flush out. + ### Pagination a. Simple pagination From d610fe782628904f7570b6884789831a9b86b767 Mon Sep 17 00:00:00 2001 From: Kaushik Gopal Date: Thu, 2 Jul 2015 23:40:08 -0700 Subject: [PATCH 004/173] feat: exponential backoff delay example --- app/build.gradle | 2 +- .../rxjava/ExponentialBackoffFragment.java | 154 ++++++++++++++++++ .../android/rxjava/MainFragment.java | 19 ++- .../layout/fragment_exponential_backoff.xml | 47 ++++++ app/src/main/res/layout/fragment_main.xml | 7 + app/src/main/res/values/strings.xml | 2 + 6 files changed, 222 insertions(+), 9 deletions(-) create mode 100644 app/src/main/java/com/morihacky/android/rxjava/ExponentialBackoffFragment.java create mode 100644 app/src/main/res/layout/fragment_exponential_backoff.xml diff --git a/app/build.gradle b/app/build.gradle index 7ff86bf2..3b59410e 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -8,7 +8,7 @@ dependencies { compile 'com.jakewharton:butterknife:5.1.1' compile 'com.jakewharton.timber:timber:2.4.2' compile 'io.reactivex:rxandroid:0.24.0' - //compile 'io.reactivex:rxjava-math:0.21.0' + compile 'io.reactivex:rxjava-math:0.21.0' compile 'com.squareup.retrofit:retrofit:1.6.1' compile 'com.squareup.okhttp:okhttp:2.0.0' diff --git a/app/src/main/java/com/morihacky/android/rxjava/ExponentialBackoffFragment.java b/app/src/main/java/com/morihacky/android/rxjava/ExponentialBackoffFragment.java new file mode 100644 index 00000000..c9edc5e0 --- /dev/null +++ b/app/src/main/java/com/morihacky/android/rxjava/ExponentialBackoffFragment.java @@ -0,0 +1,154 @@ +package com.morihacky.android.rxjava; + +import android.os.Bundle; +import android.os.Handler; +import android.support.annotation.Nullable; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ListView; +import butterknife.ButterKnife; +import butterknife.InjectView; +import butterknife.OnClick; +import com.morihacky.android.rxjava.wiring.LogAdapter; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.TimeUnit; +import rx.Observable; +import rx.Observer; +import rx.functions.Action0; +import rx.functions.Func1; +import rx.observables.MathObservable; +import rx.subscriptions.CompositeSubscription; +import timber.log.Timber; + +import static android.os.Looper.getMainLooper; + +public class ExponentialBackoffFragment + extends BaseFragment { + + @InjectView(R.id.list_threading_log) ListView _logList; + private LogAdapter _adapter; + private List _logs; + + private CompositeSubscription _subscriptions = new CompositeSubscription(); + + @Override + public void onResume() { + super.onResume(); + _subscriptions = RxUtils.getNewCompositeSubIfUnsubscribed(_subscriptions); + } + + @Override + public void onActivityCreated(@Nullable Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + _setupLogger(); + } + + @Override + public View onCreateView(LayoutInflater inflater, + @Nullable ViewGroup container, + @Nullable Bundle savedInstanceState) { + View layout = inflater.inflate(R.layout.fragment_exponential_backoff, container, false); + ButterKnife.inject(this, layout); + return layout; + } + + @Override + public void onPause() { + super.onPause(); + + RxUtils.unsubscribeIfNotNull(_subscriptions); + } + + // ----------------------------------------------------------------------------------- + + @OnClick(R.id.btn_eb_retry) + public void startRetryingWithExponentialBackoffStrategy() { + _logs = new ArrayList<>(); + _adapter.clear(); + } + + // ----------------------------------------------------------------------------------- + + @OnClick(R.id.btn_eb_delay) + public void startExecutingWithExponentialBackoffDelay() { + + _logs = new ArrayList<>(); + _adapter.clear(); + + _subscriptions.add(// + + Observable.range(1, 4)// + .delay(new Func1>() { + @Override + public Observable call(final Integer integer) { + // Rx-y way of doing the Fibonnaci :P + return MathObservable// + .sumInteger(Observable.range(1, integer)) + .flatMap(new Func1>() { + @Override + public Observable call(Integer targetSecondDelay) { + return Observable.just(integer) + .delay(targetSecondDelay, TimeUnit.SECONDS); + } + }); + } + })// + .doOnSubscribe(new Action0() { + @Override + public void call() { + _log(String.format("Execute 4 tasks with delay - time now: [xx:%2d]", + _getSecondHand())); + } + })// + .subscribe(new Observer() { + @Override + public void onCompleted() { + Timber.d("onCompleted"); + _log("Completed"); + } + + @Override + public void onError(Throwable e) { + Timber.d(e, "arrrr. Error"); + _log("Error"); + } + + @Override + public void onNext(Integer integer) { + Timber.d("emitting %d [xx:%2d]", integer, _getSecondHand()); + _log(String.format("emitting %d [xx:%2d]", integer, _getSecondHand())); + + } + })); + } + + // ----------------------------------------------------------------------------------- + + private int _getSecondHand() { + long millis = System.currentTimeMillis(); + return (int) (TimeUnit.MILLISECONDS.toSeconds(millis) - + TimeUnit.MINUTES.toSeconds(TimeUnit.MILLISECONDS.toMinutes(millis))); + } + + private void _setupLogger() { + _logs = new ArrayList<>(); + _adapter = new LogAdapter(getActivity(), new ArrayList()); + _logList.setAdapter(_adapter); + } + + private void _log(String logMsg) { + _logs.add(logMsg); + + // You can only do below stuff on main thread. + new Handler(getMainLooper()).post(new Runnable() { + + @Override + public void run() { + _adapter.clear(); + _adapter.addAll(_logs); + } + }); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/morihacky/android/rxjava/MainFragment.java b/app/src/main/java/com/morihacky/android/rxjava/MainFragment.java index ce9b926f..c80aa11b 100644 --- a/app/src/main/java/com/morihacky/android/rxjava/MainFragment.java +++ b/app/src/main/java/com/morihacky/android/rxjava/MainFragment.java @@ -46,20 +46,15 @@ public void demoThrottling() { getActivity().getSupportFragmentManager() .beginTransaction() .addToBackStack(this.toString()) - .replace(R.id.activity_main, - new DebounceSearchEmitterFragment(), - this.toString()) + .replace(R.id.activity_main, new DebounceSearchEmitterFragment(), this.toString()) .commit(); } @OnClick(R.id.btn_demo_retrofit) public void demoRetrofitCalls() { - getActivity().getSupportFragmentManager() - .beginTransaction() - .addToBackStack(this.toString()) + getActivity().getSupportFragmentManager().beginTransaction().addToBackStack(this.toString()) //.replace(R.id.activity_main, new RetrofitAsyncTaskDeathFragment(), this.toString()) - .replace(R.id.activity_main, new RetrofitFragment(), this.toString()) - .commit(); + .replace(R.id.activity_main, new RetrofitFragment(), this.toString()).commit(); } @OnClick(R.id.btn_demo_double_binding_textview) @@ -127,4 +122,12 @@ public void demoTimerIntervalDelays() { .commit(); } + @OnClick(R.id.btn_demo_exponential_backoff) + public void demoExponentialBackoff() { + getActivity().getSupportFragmentManager() + .beginTransaction() + .addToBackStack(this.toString()) + .replace(R.id.activity_main, new ExponentialBackoffFragment(), this.toString()) + .commit(); + } } diff --git a/app/src/main/res/layout/fragment_exponential_backoff.xml b/app/src/main/res/layout/fragment_exponential_backoff.xml new file mode 100644 index 00000000..10ac11d6 --- /dev/null +++ b/app/src/main/res/layout/fragment_exponential_backoff.xml @@ -0,0 +1,47 @@ + + + + + + + + + +