Skip to content

Commit 879f196

Browse files
committed
Serial execution in background feature
Adds an optional serial attribute to @background: @background(serial="some_id") This idea comes from androidannotations#309: androidannotations#309 The principle of this implementation is to keep a separate queue for each serial identifier, and to give each task to the (unique) executor only after the previous task with the same serial identifier (if any) has completed execution. It guarantees that all tasks with the same serial identifier will be called in the order, one at a time (but not necessarily on the same thread). It is still compatible with the delay attribute. For example: @background(serial="some_id", delay=2000) executes the task in background after at least 2 seconds *and* after the previous task requested with serial="some_id" (if any) have completed execution.
1 parent c201130 commit 879f196

3 files changed

Lines changed: 153 additions & 12 deletions

File tree

‎AndroidAnnotations/androidannotations-api/src/main/java/org/androidannotations/annotations/Background.java‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,5 +28,7 @@
2828
@Retention(RetentionPolicy.CLASS)
2929
@Target(ElementType.METHOD)
3030
public @interface Background {
31-
long delay() default 0;
31+
int delay() default 0; /* in milliseconds */
32+
33+
String serial() default "";
3234
}

‎AndroidAnnotations/androidannotations/src/main/java/org/androidannotations/api/BackgroundExecutor.java‎

Lines changed: 147 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,12 @@
1515
*/
1616
package org.androidannotations.api;
1717

18+
import java.util.ArrayList;
19+
import java.util.HashMap;
20+
import java.util.HashSet;
21+
import java.util.List;
22+
import java.util.Map;
23+
import java.util.Set;
1824
import java.util.concurrent.Executor;
1925
import java.util.concurrent.Executors;
2026
import java.util.concurrent.ScheduledExecutorService;
@@ -24,39 +30,121 @@ public class BackgroundExecutor {
2430

2531
private static Executor executor = Executors.newScheduledThreadPool(2 * Runtime.getRuntime().availableProcessors());
2632

33+
/*
34+
* serialRunning is used as a lock in synchronized blocks for both
35+
* serialRunning and serialQueues access
36+
*/
37+
38+
/* Set of queueIds having a currently running task */
39+
private static final Set<String> serialRunning = new HashSet<String>();
40+
41+
/* Tasks queues for each serial */
42+
private static final Map<String, List<Task>> serialQueues = new HashMap<String, List<Task>>();
43+
2744
/**
28-
* Execute a task after the given delay.
45+
* Execute a task after (at least) the given delay <strong>and</strong>
46+
* after all tasks added with the same non-null <code>serial</code> (if any)
47+
* have completed execution.
2948
*
3049
* @param runnable
3150
* the task to execute
3251
* @param delay
3352
* the time from now to delay execution, in milliseconds
53+
* @param serial
54+
* the serial queue to use (<code>null</code> or <code>""</code>
55+
* for no serial execution)
3456
* @throws IllegalArgumentException
3557
* if <code>delay</code> is strictly positive and the current
3658
* executor does not support scheduling (if
3759
* {@link #setExecutor(Executor)} has been called with such an
3860
* executor)
3961
*/
40-
public static void execute(Runnable runnable, long delay) {
41-
if (delay > 0) {
42-
if (!(executor instanceof ScheduledExecutorService)) {
43-
throw new IllegalArgumentException("The executor set does not support scheduling");
62+
public static void execute(Runnable runnable, int delay, String serial) {
63+
/* "" means null (a default annotation String value cannot be null) */
64+
if (serial == null || serial.isEmpty()) {
65+
if (delay > 0) {
66+
/* no serial, but a delay: schedule the task */
67+
if (!(executor instanceof ScheduledExecutorService)) {
68+
throw new IllegalArgumentException("The executor set does not support scheduling");
69+
}
70+
((ScheduledExecutorService) executor).schedule(runnable, delay, TimeUnit.MILLISECONDS);
71+
} else {
72+
/* no serial, no delay: execute now */
73+
executor.execute(runnable);
4474
}
45-
((ScheduledExecutorService) executor).schedule(runnable, delay, TimeUnit.MILLISECONDS);
4675
} else {
47-
/* execute now */
48-
executor.execute(runnable);
76+
/* serial is defined, the delay is managed by Task */
77+
Task task = new Task(runnable, delay, serial);
78+
79+
synchronized (serialRunning) {
80+
if (serialRunning.contains(serial)) {
81+
/* a task for this serial is already running, queue this one */
82+
List<Task> queue = serialQueues.get(serial);
83+
if (queue == null) {
84+
/* the queue does not exist yet */
85+
queue = new ArrayList<Task>();
86+
serialQueues.put(serial, queue);
87+
}
88+
/* queue the task for later execution */
89+
queue.add(task);
90+
} else {
91+
/* mark this serial as having a running task */
92+
serialRunning.add(serial);
93+
/* execute the task (a wrapper for runnable) now */
94+
execute(task, delay); /* do not pass serial here */
95+
}
96+
}
4997
}
5098
}
5199

52100
/**
53101
* Execute a task.
54102
*
103+
* Equivalent to {@link #execute(Runnable, int, String) execute(runnable, 0,
104+
* null)}.
105+
*
55106
* @param runnable
56107
* the task to execute
57108
*/
58109
public static void execute(Runnable runnable) {
59-
execute(runnable, 0);
110+
execute(runnable, 0, null);
111+
}
112+
113+
/**
114+
* Execute a task after the given delay.
115+
*
116+
* Equivalent to {@link #execute(Runnable, int, String) execute(runnable,
117+
* delay, null)}.
118+
*
119+
* @param runnable
120+
* the task to execute
121+
* @param delay
122+
* the time from now to delay execution, in milliseconds
123+
* @throws IllegalArgumentException
124+
* if <code>delay</code> is strictly positive and the current
125+
* executor does not support scheduling (if
126+
* {@link #setExecutor(Executor)} has been called with such an
127+
* executor)
128+
*/
129+
public static void execute(Runnable runnable, int delay) {
130+
execute(runnable, delay, null);
131+
}
132+
133+
/**
134+
* Execute a task after all tasks added with the same non-null
135+
* <code>serial</code> (if any) have completed execution.
136+
*
137+
* Equivalent to {@link #execute(Runnable, int, String) execute(runnable, 0,
138+
* serial)}.
139+
*
140+
* @param runnable
141+
* the task to execute
142+
* @param serial
143+
* the serial queue to use (<code>null</code> or <code>""</code>
144+
* for no serial execution)
145+
*/
146+
public static void execute(Runnable runnable, String serial) {
147+
execute(runnable, 0, serial);
60148
}
61149

62150
/**
@@ -72,4 +160,54 @@ public static void setExecutor(Executor executor) {
72160
BackgroundExecutor.executor = executor;
73161
}
74162

163+
private static class Task implements Runnable {
164+
165+
Runnable runnable;
166+
long targetTime; /* in milliseconds since epoch */
167+
String serial;
168+
169+
Task(Runnable runnable, int delay, String serial) {
170+
this.runnable = runnable;
171+
if (delay > 0) {
172+
targetTime = System.currentTimeMillis() + delay;
173+
}
174+
this.serial = serial;
175+
}
176+
177+
@Override
178+
public void run() {
179+
try {
180+
runnable.run();
181+
} finally {
182+
/* handle next tasks */
183+
postExecute();
184+
}
185+
}
186+
187+
private void postExecute() {
188+
synchronized (serialRunning) {
189+
List<Task> queue = serialQueues.get(serial);
190+
if (queue == null) {
191+
/* no task is queue for this serial, mark it as not running */
192+
serialRunning.remove(serial);
193+
} else {
194+
/* queue is not empty, retrieve the oldest queued task */
195+
Task nextTask = queue.remove(0);
196+
197+
if (queue.isEmpty()) {
198+
/* no more tasks in the queue */
199+
serialQueues.remove(serial);
200+
}
201+
202+
/* compute the remaining delay */
203+
int delay = Math.max(0, (int) (nextTask.targetTime - System.currentTimeMillis()));
204+
205+
/* execute the next task */
206+
execute(nextTask, delay); /* do not pass serial here */
207+
}
208+
}
209+
}
210+
211+
}
212+
75213
}

‎AndroidAnnotations/androidannotations/src/main/java/org/androidannotations/processing/BackgroundProcessor.java‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,12 +55,13 @@ public void process(Element element, JCodeModel codeModel, EBeanHolder holder) t
5555
{
5656
// Execute Runnable
5757
Background annotation = element.getAnnotation(Background.class);
58-
long delay = annotation.delay();
58+
int delay = annotation.delay();
59+
String serial = annotation.serial();
5960

6061
JClass backgroundExecutorClass = holder.refClass(BackgroundExecutor.class);
6162
JInvocation executeCall;
6263

63-
executeCall = backgroundExecutorClass.staticInvoke("execute").arg(_new(anonymousRunnableClass)).arg(lit(delay));
64+
executeCall = backgroundExecutorClass.staticInvoke("execute").arg(_new(anonymousRunnableClass)).arg(lit(delay)).arg(lit(serial));
6465

6566
delegatingMethod.body().add(executeCall);
6667

0 commit comments

Comments
 (0)