Skip to content
This repository was archived by the owner on Jan 31, 2022. It is now read-only.

Commit d71947f

Browse files
Clément Le ProvostPLNech
authored andcommitted
Add an option to choose a different executor for completion handlers (#342)
By default, the main thread is used. This required quite a bit of refactoring, because `AsyncTask` doesn’t allow running the completion on anything else than the UI thread! I documented the reasons in the `Request` interface (private documentation). I took the opportunity to move the base asynchronous task to a top-level class. There is still a specialized class internal to `AbstractClient` to provide easy access to the client’s properties and methods. The offline mode also had to be refactored, because now that the completion executor can be changed to a potentially different thread, or even a pool of threads, we have to synchronize the fallback logic. Fixes #92.
1 parent 24b7cf7 commit d71947f

File tree

6 files changed

+324
-138
lines changed

6 files changed

+324
-138
lines changed

algoliasearch/src/main/java/com/algolia/search/saas/AbstractClient.java

Lines changed: 27 additions & 114 deletions
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,14 @@
2323

2424
package com.algolia.search.saas;
2525

26-
import android.os.AsyncTask;
2726
import android.os.Build;
2827
import android.os.Handler;
2928
import android.os.Looper;
3029
import android.support.annotation.NonNull;
3130
import android.support.annotation.Nullable;
3231

32+
import com.algolia.search.saas.helpers.HandlerExecutor;
33+
3334
import org.json.JSONException;
3435
import org.json.JSONObject;
3536
import org.json.JSONTokener;
@@ -49,6 +50,7 @@
4950
import java.util.HashMap;
5051
import java.util.List;
5152
import java.util.Map;
53+
import java.util.concurrent.Executor;
5254
import java.util.concurrent.ExecutorService;
5355
import java.util.concurrent.Executors;
5456
import java.util.zip.GZIPInputStream;
@@ -141,12 +143,12 @@ private static class HostStatus {
141143
*/
142144
private HashMap<String, String> headers = new HashMap<String, String>();
143145

144-
/** Handler used to execute operations on the main thread. */
145-
protected Handler mainHandler = new Handler(Looper.getMainLooper());
146-
147146
/** Thread pool used to run asynchronous requests. */
148147
protected ExecutorService searchExecutorService = Executors.newFixedThreadPool(4);
149148

149+
/** Executor used to run completion handlers. By default, runs on the main thread. */
150+
protected @NonNull Executor completionExecutor = new HandlerExecutor(new Handler(Looper.getMainLooper()));
151+
150152
protected Map<String, WeakReference<Object>> indices = new HashMap<>();
151153

152154
// ----------------------------------------------------------------------
@@ -372,6 +374,16 @@ private List<String> getWriteHostsThatAreUp() {
372374
return hostsThatAreUp(writeHosts);
373375
}
374376

377+
/**
378+
* Change the executor on which completion handlers are executed.
379+
* By default, completion handlers are executed on the main thread.
380+
*
381+
* @param completionExecutor The new completion executor to use.
382+
*/
383+
public void setCompletionExecutor(@NonNull Executor completionExecutor) {
384+
this.completionExecutor = completionExecutor;
385+
}
386+
375387
// ----------------------------------------------------------------------
376388
// Utilities
377389
// ----------------------------------------------------------------------
@@ -670,127 +682,28 @@ boolean isUpOrCouldBeRetried(String host) {
670682
// ----------------------------------------------------------------------
671683

672684
/**
673-
* Abstract {@link Request} implementation using an `AsyncTask`.
674-
* Derived classes just have to implement the {@link #run()} method.
685+
* Abstract convenience implementation of {@link FutureRequest} using the client's default executors.
675686
*/
676-
abstract protected class AsyncTaskRequest implements Request {
677-
/** The completion handler notified of the result. May be null if the caller omitted it. */
678-
private CompletionHandler completionHandler;
679-
680-
/** The executor used to execute the request. */
681-
private ExecutorService executorService;
682-
683-
private boolean finished = false;
684-
685-
/**
686-
* The underlying asynchronous task.
687-
*/
688-
private AsyncTask<Void, Void, APIResult> task = new AsyncTask<Void, Void, APIResult>() {
689-
@Override
690-
protected APIResult doInBackground(Void... params) {
691-
try {
692-
return new APIResult(run());
693-
} catch (AlgoliaException e) {
694-
return new APIResult(e);
695-
}
696-
}
697-
698-
@Override
699-
protected void onPostExecute(APIResult result) {
700-
finished = true;
701-
if (completionHandler != null) {
702-
completionHandler.requestCompleted(result.content, result.error);
703-
}
704-
}
705-
706-
@Override
707-
protected void onCancelled(APIResult apiResult) {
708-
finished = true;
709-
}
710-
};
711-
687+
abstract protected class AsyncTaskRequest extends FutureRequest {
712688
/**
713-
* Construct a new request with the specified completion handler, executing on the client's default executor.
689+
* Construct a new request with the specified completion handler, executing on the client's search executor,
690+
* and calling the completion handler on the client's completion executor.
714691
*
715-
* @param completionHandler The completion handler to be notified of results. May be null if the caller omitted it.
692+
* @param completionHandler The completion handler to be notified of results. May be null if the caller omitted it.
716693
*/
717694
protected AsyncTaskRequest(@Nullable CompletionHandler completionHandler) {
718695
this(completionHandler, searchExecutorService);
719696
}
720697

721698
/**
722-
* Construct a new request with the specified completion handler, executing on the specified executor.
723-
*
724-
* @param completionHandler The completion handler to be notified of results. May be null if the caller omitted it.
725-
* @param executorService Executor service on which to execute the request.
726-
*/
727-
protected AsyncTaskRequest(@Nullable CompletionHandler completionHandler, @NonNull ExecutorService executorService) {
728-
this.completionHandler = completionHandler;
729-
this.executorService = executorService;
730-
}
731-
732-
/**
733-
* Run this request synchronously. To be implemented by derived classes.
734-
* <p>
735-
* <strong>Do not call this method directly.</strong> Will be run in a background thread when calling
736-
* {@link #start()}.
737-
* </p>
738-
*
739-
* @return The request's result.
740-
* @throws AlgoliaException If an error was encountered.
741-
*/
742-
@NonNull
743-
abstract protected JSONObject run() throws AlgoliaException;
744-
745-
/**
746-
* Run this request asynchronously.
747-
*
748-
* @return This instance.
749-
*/
750-
public AsyncTaskRequest start() {
751-
// WARNING: Starting with Honeycomb (3.0), `AsyncTask` execution is serial, so we must force parallel
752-
// execution. See <http://developer.android.com/reference/android/os/AsyncTask.html>.
753-
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
754-
task.executeOnExecutor(executorService);
755-
} else {
756-
task.execute();
757-
}
758-
return this;
759-
}
760-
761-
/**
762-
* Cancel this request.
763-
* The listener will not be called after a request has been cancelled.
764-
* <p>
765-
* WARNING: Cancelling a request may or may not cancel the underlying network call, depending how late the
766-
* cancellation happens. In other words, a cancelled request may have already been executed by the server. In any
767-
* case, cancelling never carries "undo" semantics.
768-
* </p>
769-
*/
770-
@Override
771-
public void cancel() {
772-
// NOTE: We interrupt the task's thread to better cope with timeouts.
773-
task.cancel(true /* mayInterruptIfRunning */);
774-
}
775-
776-
/**
777-
* Test if this request is still running.
778-
*
779-
* @return true if completed or cancelled, false if still running.
780-
*/
781-
@Override
782-
public boolean isFinished() {
783-
return finished;
784-
}
785-
786-
/**
787-
* Test if this request has been cancelled.
699+
* Construct a new request with the specified completion handler, executing on the specified executor, and
700+
* calling the completion handler on the client's completion executor.
788701
*
789-
* @return true if cancelled, false otherwise.
702+
* @param completionHandler The completion handler to be notified of results. May be null if the caller omitted it.
703+
* @param requestExecutor Executor on which to execute the request.
790704
*/
791-
@Override
792-
public boolean isCancelled() {
793-
return task.isCancelled();
705+
protected AsyncTaskRequest(@Nullable CompletionHandler completionHandler, @NonNull Executor requestExecutor) {
706+
super(completionHandler, requestExecutor, completionExecutor);
794707
}
795708
}
796709
}
Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
/*
2+
* Copyright (c) 2012-2017 Algolia
3+
* http://www.algolia.com/
4+
*
5+
* Permission is hereby granted, free of charge, to any person obtaining a copy
6+
* of this software and associated documentation files (the "Software"), to deal
7+
* in the Software without restriction, including without limitation the rights
8+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
* copies of the Software, and to permit persons to whom the Software is
10+
* furnished to do so, subject to the following conditions:
11+
*
12+
* The above copyright notice and this permission notice shall be included in
13+
* all copies or substantial portions of the Software.
14+
*
15+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21+
* THE SOFTWARE.
22+
*/
23+
24+
package com.algolia.search.saas;
25+
26+
import android.support.annotation.NonNull;
27+
import android.support.annotation.Nullable;
28+
import android.util.Log;
29+
30+
import org.json.JSONObject;
31+
32+
import java.util.concurrent.Callable;
33+
import java.util.concurrent.CancellationException;
34+
import java.util.concurrent.ExecutionException;
35+
import java.util.concurrent.Executor;
36+
import java.util.concurrent.FutureTask;
37+
38+
/**
39+
* Abstract {@link Request} implementation, using a {@link java.util.concurrent.Future Future} internally.
40+
* Derived classes just have to implement the {@link #run()} method.
41+
*/
42+
abstract class FutureRequest implements Request {
43+
/** The completion handler notified of the result. May be null if the caller omitted it. */
44+
private final @Nullable CompletionHandler completionHandler;
45+
46+
/** The executor used to execute the request. */
47+
private final @NonNull Executor requestExecutor;
48+
49+
/** The executor used to execute the completion handler. */
50+
private final @NonNull Executor completionExecutor;
51+
52+
/** The callable running the request. */
53+
private Callable<APIResult> callable = new Callable<APIResult>() {
54+
@Override
55+
public APIResult call() throws Exception {
56+
try {
57+
return new APIResult(run());
58+
} catch (AlgoliaException e) {
59+
return new APIResult(e);
60+
}
61+
}
62+
};
63+
64+
/**
65+
* The future used to manage this request.
66+
* Compared to the raw `Callable`, the future gives us cancellation (built-in) and completion (the overridden
67+
* `done()` method).
68+
*/
69+
private FutureTask<APIResult> task = new FutureTask<APIResult>(callable) {
70+
@Override
71+
protected void done() {
72+
if (completionHandler == null) {
73+
return;
74+
}
75+
try {
76+
final APIResult result = get();
77+
completionExecutor.execute(new Runnable() {
78+
@Override
79+
public void run() {
80+
// NOTE: Cancellation might have intervened after the request execution, but before the
81+
// completion handler has been called.
82+
if (isCancelled()) {
83+
return;
84+
}
85+
completionHandler.requestCompleted(result.content, result.error);
86+
}
87+
});
88+
}
89+
// If the task was cancelled, the call to `get()` will throw a `CancellationException`.
90+
catch (CancellationException e) {
91+
// Ignore.
92+
}
93+
catch (InterruptedException | ExecutionException e) {
94+
// Should never happen => log the error, but do not crash.
95+
Log.e(this.getClass().getName(), "When processing in background", e);
96+
}
97+
}
98+
};
99+
100+
/**
101+
* Construct a new request with the specified completion handler, executing on the specified executor, and
102+
* calling the completion handler on another executor.
103+
*
104+
* @param completionHandler The completion handler to be notified of results. May be null if the caller omitted it.
105+
* @param requestExecutor Executor on which to execute the request.
106+
* @param completionExecutor Executor on which to call the completion handler.
107+
*/
108+
FutureRequest(@Nullable CompletionHandler completionHandler, @NonNull Executor requestExecutor, @NonNull Executor completionExecutor) {
109+
this.completionHandler = completionHandler;
110+
this.requestExecutor = requestExecutor;
111+
this.completionExecutor = completionExecutor;
112+
}
113+
114+
/**
115+
* Run this request synchronously. To be implemented by derived classes.
116+
* <p>
117+
* <strong>Do not call this method directly.</strong> Will be run in a background thread when calling
118+
* {@link #start()}.
119+
* </p>
120+
*
121+
* @return The request's result.
122+
* @throws AlgoliaException If an error was encountered.
123+
*/
124+
@NonNull
125+
abstract protected JSONObject run() throws AlgoliaException;
126+
127+
/**
128+
* Run this request asynchronously.
129+
*
130+
* @return This instance.
131+
*/
132+
public FutureRequest start() {
133+
requestExecutor.execute(task);
134+
return this;
135+
}
136+
137+
/**
138+
* Cancel this request.
139+
* The listener will not be called after a request has been cancelled.
140+
* <p>
141+
* WARNING: Cancelling a request may or may not cancel the underlying network call, depending how late the
142+
* cancellation happens. In other words, a cancelled request may have already been executed by the server. In any
143+
* case, cancelling never carries "undo" semantics.
144+
* </p>
145+
*/
146+
@Override
147+
public void cancel() {
148+
// NOTE: We interrupt the task's thread to better cope with timeouts.
149+
task.cancel(true /* mayInterruptIfRunning */);
150+
}
151+
152+
/**
153+
* Test if this request is still running.
154+
*
155+
* @return true if completed or cancelled, false if still running.
156+
*/
157+
@Override
158+
public boolean isFinished() {
159+
return task.isDone();
160+
}
161+
162+
/**
163+
* Test if this request has been cancelled.
164+
*
165+
* @return true if cancelled, false otherwise.
166+
*/
167+
@Override
168+
public boolean isCancelled() {
169+
return task.isCancelled();
170+
}
171+
}

algoliasearch/src/main/java/com/algolia/search/saas/Request.java

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,27 @@
3131
*/
3232
public interface Request
3333
{
34+
// ==============================================================================================================
35+
// DISCUSSION: Why don't we use `AsyncTask` directly?
36+
// --------------------------------------------------
37+
// `AsyncTask` does not fit our purpose for many reasons:
38+
//
39+
// - `AsyncTask` has serial execution, whereas we need parallel execution. We don't want network requests to be
40+
// processed serially, especially when searching "as you type"!
41+
//
42+
// - Having a custom interface leads to better encapsulation. We can refactor the underlying implementation without
43+
// any breaking change.
44+
//
45+
// - The public API of `AsyncTask` is very verbose, and does not fit our purpose:
46+
// - `cancel(.)` has an extraneous parameter `mayInterruptIfRunning`;
47+
// - no `isFinished()` method.
48+
//
49+
// - `AsyncTask` does not allow calling the completion handler on another thread than the UI thread.
50+
//
51+
// - In theory, `AsyncTask` must be created on the UI thread, making it potentially dangerous to call API methods
52+
// from another thread.
53+
// ==============================================================================================================
54+
3455
/**
3556
* Cancel this request.
3657
* The listener will not be called after a request has been cancelled.

0 commit comments

Comments
 (0)