diff --git a/README.md b/README.md index 034376c..4d35246 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,6 @@ +### Development in this repository is stopped. Future development continues on https://github.com/yigit/android-priority-jobqueue +========================== + ![logo](http://downloads.path.com/logo.png) Android Priority Job Queue (Job Manager) @@ -14,6 +17,7 @@ It is written primarily with [flexibility][10] & [functionality][11] in mind. Th - [What's happening under the hood?](#under-the-hood) - [Advantages](#advantages) - [Getting Started](#getting-started) + - [Version History](#version-history) - [Building](#building) - [Running Tests](#running-tests) - [wiki][9] @@ -128,7 +132,7 @@ At Path, we use [greenrobot's EventBus](https://github.com/greenrobot/EventBus); ### Getting Started We distribute artifacts through maven central repository. -Gradle: `compile 'com.path:android-priority-jobqueue:0.9.9'` +Gradle: `compile 'com.path:android-priority-jobqueue:1.1.2'` Maven: @@ -136,7 +140,7 @@ Maven: com.path android-priority-jobqueue - 0.9.9 + 1.1.2 ``` @@ -148,6 +152,21 @@ We highly recommend checking how you can configure job manager and individual jo * [Review sample app][6] * [Review sample configuration][7] +### Version History + - 1.1.2 (Feb 18, 2014) + - Report exceptions to logger if addInBackground fails. (#31) + - 1.1.1 (Feb 8, 2014) + - Fixed an important bug (#35) where jobs in the same group may run in parallel if many of them become available at the same time while multiple consumer threads are waiting for a new job. + - 1.1 (Jan 30, 2014) + - Job Status query API (#18) + - Fixed a stackoverflow bug when network status changes after a long time. (#21) + - 1.0 (Jan 14, 2014): + - Added [parameterized][12] constructor for Job for more readable code. + - Deprecated `BaseJob` in favor of a more complete `Job` class. + - 0.9.9 (Dec 16, 2013): + - First public release. + + ### [Wiki][9] ### Dependencies @@ -218,3 +237,4 @@ THE SOFTWARE. [9]: https://github.com/path/android-priority-jobqueue/wiki [10]: https://github.com/path/android-priority-jobqueue/wiki/Job-Manager-Configuration [11]: https://github.com/path/android-priority-jobqueue/wiki/Job-Configuration +[12]: https://github.com/path/android-priority-jobqueue/blob/master/jobqueue/src/com/path/android/jobqueue/Params.java diff --git a/examples/twitter/TwitterClient/build.gradle b/examples/twitter/TwitterClient/build.gradle index defa741..cdc0a8d 100644 --- a/examples/twitter/TwitterClient/build.gradle +++ b/examples/twitter/TwitterClient/build.gradle @@ -1,10 +1,15 @@ +task wrapper(type: Wrapper) { + gradleVersion = '1.10' +} + + buildscript { repositories { mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:0.7.+' + classpath 'com.android.tools.build:gradle:0.8.+' } } @@ -45,7 +50,7 @@ tasks.create(name: "runTwitter", type: RunApk){ android { compileSdkVersion 17 - buildToolsVersion "18.1.0" + buildToolsVersion "19.0.1" defaultConfig { minSdkVersion 15 targetSdkVersion 17 @@ -53,8 +58,8 @@ android { dependencies { compile 'de.greenrobot:eventbus:2.1.0-beta-1' - compile 'org.twitter4j:twitter4j-core:3.0.3' - compile 'com.path:android-priority-jobqueue:1.0-SNAPSHOT' + compile 'org.twitter4j:twitter4j-core:3.0.5' + compile 'com.path:android-priority-jobqueue:1.1-SNAPSHOT' compile files('external-libs/greenDAO.jar') } diff --git a/examples/twitter/TwitterClient/gradlew b/examples/twitter/TwitterClient/gradlew new file mode 100755 index 0000000..91a7e26 --- /dev/null +++ b/examples/twitter/TwitterClient/gradlew @@ -0,0 +1,164 @@ +#!/usr/bin/env bash + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn ( ) { + echo "$*" +} + +die ( ) { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; +esac + +# For Cygwin, ensure paths are in UNIX format before anything is touched. +if $cygwin ; then + [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` +fi + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >&- +APP_HOME="`pwd -P`" +cd "$SAVED" >&- + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules +function splitJvmOpts() { + JVM_OPTS=("$@") +} +eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS +JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" + +exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" diff --git a/examples/twitter/TwitterClient/gradlew.bat b/examples/twitter/TwitterClient/gradlew.bat new file mode 100644 index 0000000..aec9973 --- /dev/null +++ b/examples/twitter/TwitterClient/gradlew.bat @@ -0,0 +1,90 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windowz variants + +if not "%OS%" == "Windows_NT" goto win9xME_args +if "%@eval[2+2]" == "4" goto 4NT_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* +goto execute + +:4NT_args +@rem Get arguments from the 4NT Shell from JP Software +set CMD_LINE_ARGS=%$ + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/examples/twitter/TwitterClient/src/com/path/android/jobqueue/examples/twitter/Config.java b/examples/twitter/TwitterClient/src/com/path/android/jobqueue/examples/twitter/Config.java index 0d60b42..7c2e6a7 100644 --- a/examples/twitter/TwitterClient/src/com/path/android/jobqueue/examples/twitter/Config.java +++ b/examples/twitter/TwitterClient/src/com/path/android/jobqueue/examples/twitter/Config.java @@ -1,11 +1,11 @@ package com.path.android.jobqueue.examples.twitter; public class Config { - public static final String CONSUMER_KEY = "W1aPoCdw3QjdKpRm4NdTQ"; - public static final String CONSUMER_SECRET = "fCvy2QV2uSmaaPHRkYQqFA4dZHrusjvQR5lNmEeHI"; + public static final String CONSUMER_KEY = "APEGHy66BMYzvgEktDfc1Q"; + public static final String CONSUMER_SECRET = "wf8XXYwivxHQtiIqPSD3lpq6po9JGRyYBIX0lT0"; - public static final String ACCESS_TOKEN = "1443060589-h6JU83NsHMYx5M47Is2RzlVZmvHPbxQND9xT6KQ"; - public static final String ACCESS_TOKEN_SECRET = "QLut9Mgwge5WptlVnCz9wxmbJrqBFNazkEYrGDZKYE"; + public static final String ACCESS_TOKEN = "1443060589-ZW0lPmcN0NOwy2AchLjZjixHLhPXClYOPQ0IhWG"; + public static final String ACCESS_TOKEN_SECRET = "Tq8aVvT1PA6PXtKHgI5v1EL5UQj3JcGlFzXla2zethjYO"; public static final String REQUEST_TOKEN_URL = "https://api.twitter.com/oauth/request_token"; public static final String AUTHORIZE_URL = "https://api.twitter.com/oauth/authorize"; diff --git a/examples/twitter/TwitterClient/src/com/path/android/jobqueue/examples/twitter/controllers/TwitterController.java b/examples/twitter/TwitterClient/src/com/path/android/jobqueue/examples/twitter/controllers/TwitterController.java index 18e9200..abd4635 100644 --- a/examples/twitter/TwitterClient/src/com/path/android/jobqueue/examples/twitter/controllers/TwitterController.java +++ b/examples/twitter/TwitterClient/src/com/path/android/jobqueue/examples/twitter/controllers/TwitterController.java @@ -3,6 +3,7 @@ import com.path.android.jobqueue.examples.twitter.Config; import twitter4j.*; import twitter4j.auth.AccessToken; +import twitter4j.conf.ConfigurationBuilder; import java.util.List; @@ -21,7 +22,13 @@ public synchronized static TwitterController getInstance() { } public TwitterController() { - twitter = TwitterFactory.getSingleton(); + twitter = new TwitterFactory(new ConfigurationBuilder() + .setOAuthAccessToken(Config.ACCESS_TOKEN) + .setOAuthAccessTokenSecret(Config.ACCESS_TOKEN_SECRET) + .setDebugEnabled(true) + .setOAuthConsumerKey(Config.CONSUMER_KEY) + .setOAuthConsumerKey(Config.CONSUMER_SECRET) + .build()).getSingleton(); AccessToken accessToken = new AccessToken(Config.ACCESS_TOKEN, Config.ACCESS_TOKEN_SECRET); twitter.setOAuthConsumer(Config.CONSUMER_KEY, Config.CONSUMER_SECRET); twitter.setOAuthAccessToken(accessToken); diff --git a/jobqueue/AndroidManifest.xml b/jobqueue/AndroidManifest.xml index 4912d66..7e37c4b 100644 --- a/jobqueue/AndroidManifest.xml +++ b/jobqueue/AndroidManifest.xml @@ -1,8 +1,8 @@ + android:versionCode="2" + android:versionName="1.1.2"> diff --git a/jobqueue/build.gradle b/jobqueue/build.gradle index 856bf65..e6d5fbf 100644 --- a/jobqueue/build.gradle +++ b/jobqueue/build.gradle @@ -1,4 +1,8 @@ import java.util.regex.Pattern +task wrapper(type: Wrapper) { + gradleVersion = '1.10' +} + apply plugin: 'maven' apply plugin: 'signing' @@ -8,7 +12,7 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:0.7.+' + classpath 'com.android.tools.build:gradle:0.8.+' } } @@ -22,7 +26,7 @@ apply plugin: 'android-library' android { compileSdkVersion 19 - buildToolsVersion "18.1.0" + buildToolsVersion "19.0.1" sourceSets { main { manifest.srcFile 'AndroidManifest.xml' diff --git a/jobqueue/build.xml b/jobqueue/build.xml index a11dd7e..279be48 100644 --- a/jobqueue/build.xml +++ b/jobqueue/build.xml @@ -193,7 +193,7 @@ - + diff --git a/jobqueue/gradle/wrapper/gradle-wrapper.jar b/jobqueue/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..5838598 Binary files /dev/null and b/jobqueue/gradle/wrapper/gradle-wrapper.jar differ diff --git a/jobqueue/gradle/wrapper/gradle-wrapper.properties b/jobqueue/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..59739c3 --- /dev/null +++ b/jobqueue/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Thu Jan 30 14:19:18 PST 2014 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=http\://services.gradle.org/distributions/gradle-1.10-bin.zip diff --git a/jobqueue/gradlew b/jobqueue/gradlew new file mode 100755 index 0000000..91a7e26 --- /dev/null +++ b/jobqueue/gradlew @@ -0,0 +1,164 @@ +#!/usr/bin/env bash + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn ( ) { + echo "$*" +} + +die ( ) { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; +esac + +# For Cygwin, ensure paths are in UNIX format before anything is touched. +if $cygwin ; then + [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` +fi + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >&- +APP_HOME="`pwd -P`" +cd "$SAVED" >&- + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules +function splitJvmOpts() { + JVM_OPTS=("$@") +} +eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS +JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" + +exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" diff --git a/jobqueue/gradlew.bat b/jobqueue/gradlew.bat new file mode 100644 index 0000000..aec9973 --- /dev/null +++ b/jobqueue/gradlew.bat @@ -0,0 +1,90 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windowz variants + +if not "%OS%" == "Windows_NT" goto win9xME_args +if "%@eval[2+2]" == "4" goto 4NT_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* +goto execute + +:4NT_args +@rem Get arguments from the 4NT Shell from JP Software +set CMD_LINE_ARGS=%$ + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/jobqueue/src/com/path/android/jobqueue/AsyncAddCallback.java b/jobqueue/src/com/path/android/jobqueue/AsyncAddCallback.java new file mode 100644 index 0000000..2b464e6 --- /dev/null +++ b/jobqueue/src/com/path/android/jobqueue/AsyncAddCallback.java @@ -0,0 +1,12 @@ +package com.path.android.jobqueue; + +import android.app.Activity; + +/** + * If you are adding the job via the async adder, you can provide a callback method to receive the ID. + * Please keep in mind that job manager will keep a strong reference to this callback. So if the callback is an + * anonymous class inside an {@link Activity} context, it may leak the activity until the job is added. + */ +public interface AsyncAddCallback { + public void onAdded(long jobId); +} diff --git a/jobqueue/src/com/path/android/jobqueue/JobManager.java b/jobqueue/src/com/path/android/jobqueue/JobManager.java index 8c18ea7..1ac6968 100644 --- a/jobqueue/src/com/path/android/jobqueue/JobManager.java +++ b/jobqueue/src/com/path/android/jobqueue/JobManager.java @@ -11,6 +11,7 @@ import com.path.android.jobqueue.nonPersistentQueue.NonPersistentPriorityQueue; import com.path.android.jobqueue.persistentQueue.sqlite.SqliteJobQueue; +import java.util.Collection; import java.util.concurrent.*; /** @@ -40,6 +41,7 @@ public class JobManager implements NetworkEventProvider.Listener { private final ConcurrentHashMap persistentOnAddedLocks; private final ConcurrentHashMap nonPersistentOnAddedLocks; private final ScheduledExecutorService timedExecutor; + private final Object getNextJobLock = new Object(); /** * Default constructor that will create a JobManager with 1 {@link SqliteJobQueue} and 1 {@link NonPersistentPriorityQueue} @@ -156,6 +158,10 @@ public void addJobInBackground(Job job) { addJobInBackground(job.getPriority(), job.getDelayInMs(), job); } + public void addJobInBackground(Job job, /*nullable*/ AsyncAddCallback callback) { + addJobInBackground(job.getPriority(), job.getDelayInMs(), job, callback); + } + //need to sync on related job queue before calling this private void addOnAddedLock(ConcurrentHashMap lockMap, long id) { lockMap.put(id, new CountDownLatch(1)); @@ -256,30 +262,36 @@ private JobHolder getNextJob() { boolean haveNetwork = hasNetwork(); JobHolder jobHolder; boolean persistent = false; - synchronized (nonPersistentJobQueue) { - jobHolder = nonPersistentJobQueue.nextJobAndIncRunCount(haveNetwork, runningJobGroups.getSafe()); - } - if (jobHolder == null) { - //go to disk, there aren't any non-persistent jobs - synchronized (persistentJobQueue) { - jobHolder = persistentJobQueue.nextJobAndIncRunCount(haveNetwork, runningJobGroups.getSafe()); - persistent = true; + synchronized (getNextJobLock) { + final Collection runningJobIds = runningJobGroups.getSafe(); + synchronized (nonPersistentJobQueue) { + jobHolder = nonPersistentJobQueue.nextJobAndIncRunCount(haveNetwork, runningJobIds); } - } - if(jobHolder != null) { - //wait for onAdded locks - if(persistent) { - waitForOnAddedLock(persistentOnAddedLocks, jobHolder.getId()); - } else { - waitForOnAddedLock(nonPersistentOnAddedLocks, jobHolder.getId()); + if (jobHolder == null) { + //go to disk, there aren't any non-persistent jobs + synchronized (persistentJobQueue) { + jobHolder = persistentJobQueue.nextJobAndIncRunCount(haveNetwork, runningJobIds); + persistent = true; + } + } + if(jobHolder == null) { + return null; + } + if(persistent && dependencyInjector != null) { + dependencyInjector.inject(jobHolder.getBaseJob()); + } + if(jobHolder.getGroupId() != null) { + runningJobGroups.add(jobHolder.getGroupId()); } } - if(persistent && jobHolder != null && dependencyInjector != null) { - dependencyInjector.inject(jobHolder.getBaseJob()); - } - if(jobHolder != null && jobHolder.getGroupId() != null) { - runningJobGroups.add(jobHolder.getGroupId()); + + //wait for onAdded locks. wait for locks after job is selected so that we minimize the lock + if(persistent) { + waitForOnAddedLock(persistentOnAddedLocks, jobHolder.getId()); + } else { + waitForOnAddedLock(nonPersistentOnAddedLocks, jobHolder.getId()); } + return jobHolder; } @@ -299,6 +311,48 @@ private void reAddJob(JobHolder jobHolder) { } } + /** + * Returns the current status of a {@link Job}. + *

+ * You should not call this method on the UI thread because it may make a db request. + *

+ *

+ * This is not a very fast call so try not to make it unless necessary. Consider using events if you need to be + * informed about a job's lifecycle. + *

+ * @param id the ID, returned by the addJob method + * @param isPersistent Jobs are added to different queues depending on if they are persistent or not. This is necessary + * because each queue has independent id sets. + * @return + */ + public JobStatus getJobStatus(long id, boolean isPersistent) { + if(jobConsumerExecutor.isRunning(id, isPersistent)) { + return JobStatus.RUNNING; + } + JobHolder holder; + if(isPersistent) { + synchronized (persistentJobQueue) { + holder = persistentJobQueue.findJobById(id); + } + } else { + synchronized (nonPersistentJobQueue) { + holder = nonPersistentJobQueue.findJobById(id); + } + } + if(holder == null) { + return JobStatus.UNKNOWN; + } + boolean network = hasNetwork(); + if(holder.requiresNetwork() && !network) { + return JobStatus.WAITING_NOT_READY; + } + if(holder.getDelayUntilNs() > System.nanoTime()) { + return JobStatus.WAITING_NOT_READY; + } + + return JobStatus.WAITING_READY; + } + private void removeJob(JobHolder jobHolder) { if (jobHolder.getBaseJob().isPersistent()) { synchronized (persistentJobQueue) { @@ -497,12 +551,24 @@ public void run() { */ @Deprecated public void addJobInBackground(final int priority, final long delay, final BaseJob baseJob) { + addJobInBackground(priority, delay, baseJob, null); + } + + protected void addJobInBackground(final int priority, final long delay, final BaseJob baseJob, + /*nullable*/final AsyncAddCallback callback) { final long callTime = System.nanoTime(); timedExecutor.execute(new Runnable() { @Override public void run() { - final long runDelay = (System.nanoTime() - callTime) / NS_PER_MS; - addJob(priority, Math.max(0, delay - runDelay), baseJob); + try { + final long runDelay = (System.nanoTime() - callTime) / NS_PER_MS; + long id = addJob(priority, Math.max(0, delay - runDelay), baseJob); + if(callback != null) { + callback.onAdded(id); + } + } catch (Throwable t) { + JqLog.e(t, "addJobInBackground received an exception. job class: %s", baseJob.getClass().getSimpleName() ); + } } }); } diff --git a/jobqueue/src/com/path/android/jobqueue/JobQueue.java b/jobqueue/src/com/path/android/jobqueue/JobQueue.java index 0755505..a149a00 100644 --- a/jobqueue/src/com/path/android/jobqueue/JobQueue.java +++ b/jobqueue/src/com/path/android/jobqueue/JobQueue.java @@ -72,4 +72,11 @@ public interface JobQueue { */ void clear(); + /** + * returns the job with the given id if it exists in the queue + * @param id id of the job, returned by insert method + * @return JobHolder with the given id or null if it does not exists + */ + JobHolder findJobById(long id); + } diff --git a/jobqueue/src/com/path/android/jobqueue/JobStatus.java b/jobqueue/src/com/path/android/jobqueue/JobStatus.java new file mode 100644 index 0000000..b687cc3 --- /dev/null +++ b/jobqueue/src/com/path/android/jobqueue/JobStatus.java @@ -0,0 +1,36 @@ +package com.path.android.jobqueue; + +/** + * Identifies the current status of a job if it is in the queue + */ +public enum JobStatus { + /** + * Job is in the queue but cannot run yet. + * As of v 1.1, this might be: + *
    + *
  • Job requires network but there is no available network connection
  • + *
  • Job is delayed. We are waiting for the time to pass
  • + *
+ */ + WAITING_NOT_READY, + /** + * Job is in the queue, ready to be run. Waiting for an available consumer. + */ + WAITING_READY, + /** + * Job is being executed by one of the runners. + */ + RUNNING, + /** + * Job is not known by job queue. + *

This might be: + *

    + *
  • Invalid ID
  • + *
  • Job has been completed
  • + *
  • Job has failed
  • + *
  • Job has just been added, about to be delivered into a queue
  • + *
+ *

+ */ + UNKNOWN +} diff --git a/jobqueue/src/com/path/android/jobqueue/Params.java b/jobqueue/src/com/path/android/jobqueue/Params.java index c3e323a..1695d94 100644 --- a/jobqueue/src/com/path/android/jobqueue/Params.java +++ b/jobqueue/src/com/path/android/jobqueue/Params.java @@ -97,23 +97,23 @@ public Params setDelayMs(long delayMs) { return this; } - boolean doesRequireNetwork() { + public boolean doesRequireNetwork() { return requiresNetwork; } - String getGroupId() { + public String getGroupId() { return groupId; } - boolean isPersistent() { + public boolean isPersistent() { return persistent; } - int getPriority() { + public int getPriority() { return priority; } - long getDelayMs() { + public long getDelayMs() { return delayMs; } } diff --git a/jobqueue/src/com/path/android/jobqueue/cachedQueue/CachedJobQueue.java b/jobqueue/src/com/path/android/jobqueue/cachedQueue/CachedJobQueue.java index be3279a..c373c99 100644 --- a/jobqueue/src/com/path/android/jobqueue/cachedQueue/CachedJobQueue.java +++ b/jobqueue/src/com/path/android/jobqueue/cachedQueue/CachedJobQueue.java @@ -93,6 +93,11 @@ public void clear() { delegate.clear(); } + @Override + public JobHolder findJobById(long id) { + return delegate.findJobById(id); + } + private static class Cache { Integer count; DelayUntil delayUntil; diff --git a/jobqueue/src/com/path/android/jobqueue/executor/JobConsumerExecutor.java b/jobqueue/src/com/path/android/jobqueue/executor/JobConsumerExecutor.java index 2334f62..139d92c 100644 --- a/jobqueue/src/com/path/android/jobqueue/executor/JobConsumerExecutor.java +++ b/jobqueue/src/com/path/android/jobqueue/executor/JobConsumerExecutor.java @@ -6,6 +6,7 @@ import com.path.android.jobqueue.config.Configuration; import com.path.android.jobqueue.log.JqLog; +import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; @@ -21,7 +22,8 @@ public class JobConsumerExecutor { private final Contract contract; private final int keepAliveSeconds; private final AtomicInteger activeConsumerCount = new AtomicInteger(0); - private final AtomicInteger runningJobCount = new AtomicInteger(0); + // key : id + (isPersistent) + private final ConcurrentHashMap runningJobHolders; public JobConsumerExecutor(Configuration config, Contract contract) { @@ -31,6 +33,7 @@ public JobConsumerExecutor(Configuration config, Contract contract) { this.keepAliveSeconds = config.getConsumerKeepAlive(); this.contract = contract; threadGroup = new ThreadGroup("JobConsumers"); + runningJobHolders = new ConcurrentHashMap(); } /** @@ -92,17 +95,43 @@ private boolean isAboveLoadFactor(boolean inConsumerThread) { int consumerCnt = activeConsumerCount.intValue() - (inConsumerThread ? 1 : 0); boolean res = consumerCnt < minConsumerSize || - consumerCnt * loadFactor < contract.countRemainingReadyJobs() + runningJobCount.get(); + consumerCnt * loadFactor < contract.countRemainingReadyJobs() + runningJobHolders.size(); if(JqLog.isDebugEnabled()) { JqLog.d("%s: load factor check. %s = (%d < %d)|| (%d * %d < %d + %d). consumer thread: %s", Thread.currentThread().getName(), res, consumerCnt, minConsumerSize, - consumerCnt, loadFactor, contract.countRemainingReadyJobs(), runningJobCount.get(), inConsumerThread); + consumerCnt, loadFactor, contract.countRemainingReadyJobs(), runningJobHolders.size(), inConsumerThread); } return res; } } + private void onBeforeRun(JobHolder jobHolder) { + runningJobHolders.put(createRunningJobHolderKey(jobHolder), jobHolder); + } + + private void onAfterRun(JobHolder jobHolder) { + runningJobHolders.remove(createRunningJobHolderKey(jobHolder)); + } + + private String createRunningJobHolderKey(JobHolder jobHolder) { + return createRunningJobHolderKey(jobHolder.getId(), jobHolder.getBaseJob().isPersistent()); + } + + private String createRunningJobHolderKey(long id, boolean isPersistent) { + return id + "_" + (isPersistent ? "t" : "f"); + } + + /** + * returns true if job is currently handled by one of the executor threads + * @param id id of the job + * @param persistent boolean flag to distinguish id conflicts + * @return true if job is currently handled here + */ + public boolean isRunning(long id, boolean persistent) { + return runningJobHolders.containsKey(createRunningJobHolderKey(id, persistent)); + } + /** * contract between the {@link JobManager} and {@link JobConsumerExecutor} */ @@ -168,13 +197,13 @@ public void run() { do { nextJob = contract.isRunning() ? contract.getNextJob(executor.keepAliveSeconds, TimeUnit.SECONDS) : null; if (nextJob != null) { - executor.runningJobCount.incrementAndGet(); + executor.onBeforeRun(nextJob); if (nextJob.safeRun(nextJob.getRunCount())) { contract.removeJob(nextJob); } else { contract.insertOrReplace(nextJob); } - executor.runningJobCount.decrementAndGet(); + executor.onAfterRun(nextJob); } } while (nextJob != null); } finally { diff --git a/jobqueue/src/com/path/android/jobqueue/nonPersistentQueue/JobSet.java b/jobqueue/src/com/path/android/jobqueue/nonPersistentQueue/JobSet.java index 2613245..b084a0e 100644 --- a/jobqueue/src/com/path/android/jobqueue/nonPersistentQueue/JobSet.java +++ b/jobqueue/src/com/path/android/jobqueue/nonPersistentQueue/JobSet.java @@ -11,6 +11,7 @@ public interface JobSet { public JobHolder peek(Collection excludeGroupIds); public JobHolder poll(Collection excludeGroupIds); + public JobHolder findById(long id); public boolean offer(JobHolder holder); public boolean remove(JobHolder holder); public void clear(); diff --git a/jobqueue/src/com/path/android/jobqueue/nonPersistentQueue/MergedQueue.java b/jobqueue/src/com/path/android/jobqueue/nonPersistentQueue/MergedQueue.java index f845afd..f32fd7c 100644 --- a/jobqueue/src/com/path/android/jobqueue/nonPersistentQueue/MergedQueue.java +++ b/jobqueue/src/com/path/android/jobqueue/nonPersistentQueue/MergedQueue.java @@ -110,31 +110,33 @@ public JobHolder poll(Collection excludeGroupIds) { */ @Override public JobHolder peek(Collection excludeGroupIds) { - JobHolder delayed = queue0.peek(excludeGroupIds); - //if queue for this job has changed, re-add it and try peek from scratch - if(delayed != null && decideQueue(delayed) != SetId.S0) { - queue1.offer(delayed); - queue0.remove(delayed); - return peek(excludeGroupIds); - } - JobHolder nonDelayed = queue1.peek(excludeGroupIds); - //if queue for this job has changed, re-add it and try peek from scratch - if(nonDelayed != null && decideQueue(nonDelayed) != SetId.S1) { - queue0.offer(nonDelayed); - queue1.remove(nonDelayed); - return peek(excludeGroupIds); - } - if(delayed == null) { + while (true) { + JobHolder delayed = queue0.peek(excludeGroupIds); + //if queue for this job has changed, re-add it and try peek from scratch + if(delayed != null && decideQueue(delayed) != SetId.S0) { + queue1.offer(delayed); + queue0.remove(delayed); + continue;//retry + } + JobHolder nonDelayed = queue1.peek(excludeGroupIds); + //if queue for this job has changed, re-add it and try peek from scratch + if(nonDelayed != null && decideQueue(nonDelayed) != SetId.S1) { + queue0.offer(nonDelayed); + queue1.remove(nonDelayed); + continue;//retry + } + if(delayed == null) { + return nonDelayed; + } + if(nonDelayed == null) { + return delayed; + } + int cmp = retrieveComparator.compare(delayed, nonDelayed); + if(cmp == -1) { + return delayed; + } return nonDelayed; } - if(nonDelayed == null) { - return delayed; - } - int cmp = retrieveComparator.compare(delayed, nonDelayed); - if(cmp == -1) { - return delayed; - } - return nonDelayed; } @@ -199,6 +201,19 @@ public CountWithGroupIdsResult countReadyJobs(SetId setId, Collection ex } } + + + /** + * Returns the JobHolder that has the given id + * @param id id job the job + * @return + */ + @Override + public JobHolder findById(long id) { + JobHolder q0 = queue0.findById(id); + return q0 == null ? queue1.findById(id) : q0; + } + /** * simple enum to identify queues */ diff --git a/jobqueue/src/com/path/android/jobqueue/nonPersistentQueue/NonPersistentJobSet.java b/jobqueue/src/com/path/android/jobqueue/nonPersistentQueue/NonPersistentJobSet.java index c178453..aec9272 100644 --- a/jobqueue/src/com/path/android/jobqueue/nonPersistentQueue/NonPersistentJobSet.java +++ b/jobqueue/src/com/path/android/jobqueue/nonPersistentQueue/NonPersistentJobSet.java @@ -3,7 +3,13 @@ import com.path.android.jobqueue.JobHolder; import com.path.android.jobqueue.log.JqLog; -import java.util.*; +import java.util.Collection; +import java.util.Comparator; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.TreeSet; /** * This is the default implementation of JobSet. @@ -11,13 +17,15 @@ * version */ public class NonPersistentJobSet implements JobSet { - TreeSet set; + private final TreeSet set; //groupId -> # of jobs in that group - Map existingGroups; + private final Map existingGroups; + private final Map idCache; public NonPersistentJobSet(Comparator comparator) { this.set = new TreeSet(comparator); this.existingGroups = new HashMap(); + this.idCache = new HashMap(); } private JobHolder safeFirst() { @@ -62,6 +70,11 @@ public JobHolder poll(Collection excludeGroupIds) { return peek; } + @Override + public JobHolder findById(long id) { + return idCache.get(id); + } + @Override public boolean offer(JobHolder holder) { if(holder.getId() == null) { @@ -73,9 +86,13 @@ public boolean offer(JobHolder holder) { remove(holder); result = set.add(holder); } - if(result && holder.getGroupId() != null) { - incGroupCount(holder.getGroupId()); + if(result) { + idCache.put(holder.getId(), holder); + if(holder.getGroupId() != null) { + incGroupCount(holder.getGroupId()); + } } + return result; } @@ -103,8 +120,11 @@ private void decGroupCount(String groupId) { @Override public boolean remove(JobHolder holder) { boolean removed = set.remove(holder); - if(removed && holder.getGroupId() != null) { - decGroupCount(holder.getGroupId()); + if(removed) { + idCache.remove(holder.getId()); + if(holder.getGroupId() != null) { + decGroupCount(holder.getGroupId()); + } } return removed; } @@ -115,6 +135,7 @@ public boolean remove(JobHolder holder) { public void clear() { set.clear(); existingGroups.clear(); + idCache.clear(); } @Override diff --git a/jobqueue/src/com/path/android/jobqueue/nonPersistentQueue/NonPersistentPriorityQueue.java b/jobqueue/src/com/path/android/jobqueue/nonPersistentQueue/NonPersistentPriorityQueue.java index 618d4da..627d09b 100644 --- a/jobqueue/src/com/path/android/jobqueue/nonPersistentQueue/NonPersistentPriorityQueue.java +++ b/jobqueue/src/com/path/android/jobqueue/nonPersistentQueue/NonPersistentPriorityQueue.java @@ -91,11 +91,22 @@ public Long getNextJobDelayUntilNs(boolean hasNetwork) { return next == null ? null : next.getDelayUntilNs(); } + /** + * {@inheritDoc} + */ @Override public void clear() { jobs.clear(); } + /** + * {@inheritDoc} + */ + @Override + public JobHolder findJobById(long id) { + return jobs.findById(id); + } + public final Comparator jobComparator = new Comparator() { @Override public int compare(JobHolder holder1, JobHolder holder2) { diff --git a/jobqueue/src/com/path/android/jobqueue/persistentQueue/sqlite/SqlHelper.java b/jobqueue/src/com/path/android/jobqueue/persistentQueue/sqlite/SqlHelper.java index ad5d689..50e3ad7 100644 --- a/jobqueue/src/com/path/android/jobqueue/persistentQueue/sqlite/SqlHelper.java +++ b/jobqueue/src/com/path/android/jobqueue/persistentQueue/sqlite/SqlHelper.java @@ -8,6 +8,9 @@ * Helper class for {@link SqliteJobQueue} to generate sql queries and statements. */ public class SqlHelper { + + /**package**/ String FIND_BY_ID_QUERY; + private SQLiteStatement insertStatement; private SQLiteStatement insertOrReplaceStatement; private SQLiteStatement deleteStatement; @@ -16,6 +19,7 @@ public class SqlHelper { private SQLiteStatement nextJobDelayedUntilWithNetworkStatement; private SQLiteStatement nextJobDelayedUntilWithoutNetworkStatement; + final SQLiteDatabase db; final String tableName; final String primaryKeyColumnName; @@ -28,6 +32,7 @@ public SqlHelper(SQLiteDatabase db, String tableName, String primaryKeyColumnNam this.columnCount = columnCount; this.primaryKeyColumnName = primaryKeyColumnName; this.sessionId = sessionId; + FIND_BY_ID_QUERY = "SELECT * FROM " + tableName + " WHERE " + DbOpenHelper.ID_COLUMN.columnName + " = ?"; } public static String create(String tableName, Property primaryKey, Property... properties) { diff --git a/jobqueue/src/com/path/android/jobqueue/persistentQueue/sqlite/SqliteJobQueue.java b/jobqueue/src/com/path/android/jobqueue/persistentQueue/sqlite/SqliteJobQueue.java index 5f290e5..eefc667 100644 --- a/jobqueue/src/com/path/android/jobqueue/persistentQueue/sqlite/SqliteJobQueue.java +++ b/jobqueue/src/com/path/android/jobqueue/persistentQueue/sqlite/SqliteJobQueue.java @@ -159,6 +159,25 @@ public int countReadyJobs(boolean hasNetwork, Collection excludeGroups) } } + /** + * {@inheritDoc} + */ + @Override + public JobHolder findJobById(long id) { + Cursor cursor = db.rawQuery(sqlHelper.FIND_BY_ID_QUERY, new String[]{Long.toString(id)}); + try { + if(!cursor.moveToFirst()) { + return null; + } + return createJobHolderFromCursor(cursor); + } catch (InvalidBaseJobException e) { + JqLog.e(e, "invalid job on findJobById"); + return null; + } finally { + cursor.close(); + } + } + /** * {@inheritDoc} */ @@ -246,6 +265,9 @@ public Long getNextJobDelayUntilNs(boolean hasNetwork) { } } + /** + * {@inheritDoc} + */ @Override public void clear() { sqlHelper.truncate(); diff --git a/jobqueue/test/com/path/android/jobqueue/test/jobmanager/AddInBackgroundTest.java b/jobqueue/test/com/path/android/jobqueue/test/jobmanager/AddInBackgroundTest.java index 20be3a3..454d3c5 100644 --- a/jobqueue/test/com/path/android/jobqueue/test/jobmanager/AddInBackgroundTest.java +++ b/jobqueue/test/com/path/android/jobqueue/test/jobmanager/AddInBackgroundTest.java @@ -1,8 +1,14 @@ package com.path.android.jobqueue.test.jobmanager; +import com.path.android.jobqueue.AsyncAddCallback; +import com.path.android.jobqueue.BaseJob; import com.path.android.jobqueue.Job; +import com.path.android.jobqueue.JobHolder; +import com.path.android.jobqueue.JobManager; +import com.path.android.jobqueue.JobQueue; import com.path.android.jobqueue.Params; import com.path.android.jobqueue.test.jobs.DummyJob; +import org.fest.reflect.core.*; import org.hamcrest.*; import org.junit.Test; import org.junit.runner.RunWith; @@ -14,15 +20,19 @@ @RunWith(RobolectricTestRunner.class) public class AddInBackgroundTest extends JobManagerTestBase { @Test - public void testAddInBackground() { - addInBackground(false); - addInBackground(true); - + public void testAddInBackground() throws InterruptedException { + for(boolean delay : new boolean[]{true, false}) { + for(boolean useCallback : new boolean[]{true, false}) { + addInBackground(delay, useCallback); + } + } } - public void addInBackground(boolean delayed) { + + public void addInBackground(boolean delayed, boolean useCallback) throws InterruptedException { long currentThreadId = Thread.currentThread().getId(); final AtomicLong onAddedThreadId = new AtomicLong(); final CountDownLatch addedLatch = new CountDownLatch(2); + Job dummyJob = new DummyJob(new Params(1).setDelayMs(delayed ? 1000 : 0)) { @Override public void onAdded() { @@ -31,9 +41,33 @@ public void onAdded() { addedLatch.countDown(); } }; - createJobManager().addJobInBackground(dummyJob); - - addedLatch.countDown(); + JobManager jobManager = createJobManager(); + jobManager.stop(); + final AtomicLong jobId = new AtomicLong(0); + if(useCallback) { + jobManager.addJobInBackground(dummyJob, new AsyncAddCallback() { + @Override + public void onAdded(long id) { + jobId.set(id); + addedLatch.countDown(); + } + }); + } else { + addedLatch.countDown(); + jobManager.addJobInBackground(dummyJob); + } + addedLatch.await(); MatcherAssert.assertThat("thread ids should be different. delayed:" + delayed, currentThreadId, CoreMatchers.not(onAddedThreadId.get())); + if(useCallback) { + JobQueue queue = getNonPersistentQueue(jobManager); + JobHolder holder = queue.findJobById(jobId.longValue()); + MatcherAssert.assertThat("there should be a job in the holder. id:" + jobId.longValue() +", delayed:" + delayed + ", use cb:" + useCallback + , holder, CoreMatchers.notNullValue()); + MatcherAssert.assertThat("id callback should have the proper id:", holder.getBaseJob(), CoreMatchers.is((BaseJob) dummyJob)); + } + } + + protected JobQueue getNonPersistentQueue(JobManager jobManager) { + return Reflection.field("nonPersistentJobQueue").ofType(JobQueue.class).in(jobManager).get(); } } diff --git a/jobqueue/test/com/path/android/jobqueue/test/jobmanager/GroupingTest.java b/jobqueue/test/com/path/android/jobqueue/test/jobmanager/GroupingTest.java index 00103e9..0739d3c 100644 --- a/jobqueue/test/com/path/android/jobqueue/test/jobmanager/GroupingTest.java +++ b/jobqueue/test/com/path/android/jobqueue/test/jobmanager/GroupingTest.java @@ -3,6 +3,7 @@ import com.path.android.jobqueue.JobHolder; import com.path.android.jobqueue.JobManager; import com.path.android.jobqueue.Params; +import com.path.android.jobqueue.config.Configuration; import com.path.android.jobqueue.test.jobs.DummyJob; import org.fest.reflect.method.*; import static org.hamcrest.CoreMatchers.*; @@ -11,6 +12,9 @@ import org.junit.runner.RunWith; import org.robolectric.*; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.atomic.AtomicInteger; + @RunWith(RobolectricTestRunner.class) public class GroupingTest extends JobManagerTestBase { @Test @@ -35,4 +39,68 @@ public void testGrouping() throws Exception { removeJobMethod.invoke(group2Job); MatcherAssert.assertThat("even after group2 job is complete, no jobs should be returned since we only have group1 jobs left", nextJobMethod.invoke(), is(nullValue())); } + + @Test + public void testGroupingRaceCondition() throws Exception { + DummyNetworkUtilWithConnectivityEventSupport dummyNetworkUtil = new DummyNetworkUtilWithConnectivityEventSupport(); + JobManager jobManager = createJobManager(new Configuration.Builder(Robolectric.application) + .minConsumerCount(5).maxConsumerCount(10) + .networkUtil(dummyNetworkUtil)); + dummyNetworkUtil.setHasNetwork(false, true); + //add a bunch of network requring jobs + final String GROUP_ID = "shared_group_id"; + final int AFTER_ADDED_JOBS_COUNT = 5; + final int NOT_SET_JOB_ID = -1; + final AtomicInteger firstRunJob = new AtomicInteger(NOT_SET_JOB_ID); + final int FIRST_JOB_ID = -10; + final CountDownLatch onAddedCalled = new CountDownLatch(1); + final CountDownLatch remainingJobsOnAddedCalled = new CountDownLatch(AFTER_ADDED_JOBS_COUNT); + jobManager.addJobInBackground(new DummyJob(new Params(10).requireNetwork().groupBy(GROUP_ID)) { + @Override + public void onAdded() { + super.onAdded(); + onAddedCalled.countDown(); + try { + //wait until all other jobs are added + remainingJobsOnAddedCalled.await(); + //wait a bit after all are added, + Thread.sleep(1000); + } catch (InterruptedException e) { + } + } + + @Override + public void onRun() throws Throwable { + super.onRun(); + firstRunJob.compareAndSet(NOT_SET_JOB_ID, FIRST_JOB_ID); + } + }); + //ensure first jobs on added is called + onAddedCalled.await(); + for(int i = 0; i < AFTER_ADDED_JOBS_COUNT; i ++) { + final int finalI = i; + jobManager.addJob(new DummyJob(new Params(5).groupBy(GROUP_ID).requireNetwork()) { + final int id = finalI + 1; + + @Override + public void onAdded() { + super.onAdded(); + remainingJobsOnAddedCalled.countDown(); + } + + @Override + public void onRun() throws Throwable { + super.onRun(); + firstRunJob.compareAndSet(NOT_SET_JOB_ID, id); + } + }); + } + dummyNetworkUtil.setHasNetwork(true, true); + //wait until all jobs are completed + while(firstRunJob.get() == NOT_SET_JOB_ID) { + Thread.sleep(100); + } + MatcherAssert.assertThat("highest priority job should run if it is added before others", firstRunJob.get(), is(FIRST_JOB_ID)); + + } } diff --git a/jobqueue/test/com/path/android/jobqueue/test/jobmanager/InjectorTest.java b/jobqueue/test/com/path/android/jobqueue/test/jobmanager/InjectorTest.java index ac6152f..7129710 100644 --- a/jobqueue/test/com/path/android/jobqueue/test/jobmanager/InjectorTest.java +++ b/jobqueue/test/com/path/android/jobqueue/test/jobmanager/InjectorTest.java @@ -6,6 +6,7 @@ import com.path.android.jobqueue.Params; import com.path.android.jobqueue.config.Configuration; import com.path.android.jobqueue.di.DependencyInjector; +import com.path.android.jobqueue.log.CustomLogger; import com.path.android.jobqueue.test.jobs.DummyJob; import static org.hamcrest.CoreMatchers.*; import org.hamcrest.*; @@ -13,6 +14,8 @@ import org.junit.runner.RunWith; import org.robolectric.*; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; @RunWith(RobolectricTestRunner.class) @@ -42,6 +45,55 @@ public void inject(BaseJob job) { holder = getNextJobMethod(jobManager).invoke(); MatcherAssert.assertThat("injection should be called for persistent job", holder.getBaseJob(), equalTo(injectedJobReference.getObject())); MatcherAssert.assertThat("injection should be called two times for persistent job", injectionCallCount.get(), equalTo(3)); + } + + @Test + public void testInjectorCrash() throws Exception { + final String EXCEPTION_MESSAGE = "could not inject for whatever reason :)"; + DependencyInjector dummyDependencyInjector = new DependencyInjector() { + @Override + public void inject(BaseJob baseJob) { + throw new RuntimeException(EXCEPTION_MESSAGE); + } + }; + + final ObjectReference objectReference = new ObjectReference(); + final CountDownLatch exceptionLatch = new CountDownLatch(1); + CustomLogger customLogger = new CustomLogger() { + @Override + public boolean isDebugEnabled() { + return false; + } + + @Override + public void d(String s, Object... objects) { + + } + + @Override + public void e(Throwable throwable, String s, Object... objects) { + objectReference.setObject(throwable); + exceptionLatch.countDown(); + } + + @Override + public void e(String s, Object... objects) { + // + } + }; + JobManager jobManager = createJobManager(new Configuration.Builder(Robolectric.application).injector(dummyDependencyInjector).customLogger(customLogger)); + Throwable addException = null; + try { + jobManager.addJob(new DummyJob(new Params(0))); + } catch (Throwable t) { + addException = t; + } + MatcherAssert.assertThat("addJob should throw exception if dependency injector throws exception", addException, notNullValue()); + jobManager.addJobInBackground(new DummyJob(new Params(0))); + exceptionLatch.await(2, TimeUnit.SECONDS); + MatcherAssert.assertThat("there should be a received exception", objectReference.getObject(), notNullValue()); + MatcherAssert.assertThat("logged exception should be a runtime exception", objectReference.getObject(), instanceOf(RuntimeException.class)); + MatcherAssert.assertThat("logged exception should have expected message", ((Throwable)objectReference.getObject()).getMessage(), is(EXCEPTION_MESSAGE)); } } diff --git a/jobqueue/test/com/path/android/jobqueue/test/jobmanager/JobManagerTestBase.java b/jobqueue/test/com/path/android/jobqueue/test/jobmanager/JobManagerTestBase.java index b44fc39..d416b76 100644 --- a/jobqueue/test/com/path/android/jobqueue/test/jobmanager/JobManagerTestBase.java +++ b/jobqueue/test/com/path/android/jobqueue/test/jobmanager/JobManagerTestBase.java @@ -32,6 +32,31 @@ protected JobManager createJobManager(Configuration.Builder configurationBuilder + protected static class DummyTwoLatchJob extends DummyJob { + private final CountDownLatch waitFor; + private final CountDownLatch trigger; + private final CountDownLatch onRunLatch; + + protected DummyTwoLatchJob(Params params, CountDownLatch waitFor, CountDownLatch trigger) { + super(params); + this.waitFor = waitFor; + this.trigger = trigger; + onRunLatch = new CountDownLatch(1); + } + + public void waitTillOnRun() throws InterruptedException { + onRunLatch.await(); + } + + @Override + public void onRun() throws Throwable { + onRunLatch.countDown(); + waitFor.await(); + super.onRun(); + trigger.countDown(); + } + } + protected static class DummyLatchJob extends DummyJob { private final CountDownLatch latch; @@ -108,6 +133,10 @@ public boolean isConnected(Context context) { public void setListener(Listener listener) { this.listener = listener; } + + public boolean isConnected() { + return hasNetwork; + } } protected static class ObjectReference { diff --git a/jobqueue/test/com/path/android/jobqueue/test/jobmanager/JobStatusTest.java b/jobqueue/test/com/path/android/jobqueue/test/jobmanager/JobStatusTest.java new file mode 100644 index 0000000..28656ba --- /dev/null +++ b/jobqueue/test/com/path/android/jobqueue/test/jobmanager/JobStatusTest.java @@ -0,0 +1,119 @@ +package com.path.android.jobqueue.test.jobmanager; + +import com.path.android.jobqueue.Job; +import com.path.android.jobqueue.JobManager; +import com.path.android.jobqueue.JobStatus; +import com.path.android.jobqueue.Params; +import com.path.android.jobqueue.config.Configuration; +import com.path.android.jobqueue.test.jobs.DummyJob; +import static org.hamcrest.CoreMatchers.*; +import static org.hamcrest.MatcherAssert.*; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.*; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CountDownLatch; + +@RunWith(RobolectricTestRunner.class) +public class JobStatusTest extends JobManagerTestBase { + @Test + public void testJobStatus() throws InterruptedException { + DummyNetworkUtilWithConnectivityEventSupport networkUtil = new DummyNetworkUtilWithConnectivityEventSupport(); + networkUtil.setHasNetwork(false, true); + JobManager jobManager = createJobManager(new Configuration.Builder(Robolectric.application).networkUtil(networkUtil)); + jobManager.stop(); + List networkRequiringJobIndices = new ArrayList(); + Job[] jobs = new Job[] { + new DummyJob(new Params(0)), + new DummyJob(new Params(0).persist()), + new DummyJob(new Params(0).persist().requireNetwork()) + }; + long[] ids = new long[jobs.length]; + for(int i = 0; i < jobs.length; i ++) { + ids[i] = jobManager.addJob(jobs[i]); + if(jobs[i].requiresNetwork()) { + networkRequiringJobIndices.add(i); + } + JobStatus expectedStatus = (networkUtil.isConnected() || jobs[i].requiresNetwork() == false) ? JobStatus.WAITING_READY : + JobStatus.WAITING_NOT_READY; + assertThat("job should have correct status after being added", + jobManager.getJobStatus(ids[i], jobs[i].isPersistent()), is(expectedStatus)); + } + + //create an unknown id, ensure status for that + + boolean exists; + long unknownId; + do { + unknownId = (long) (Math.random() * 10000 - 5000); + exists = false; + for(long id : ids) { + if(id == unknownId) { + exists = true; + continue; + } + } + } while (exists); + for(boolean persistent : new boolean[]{true, false}) { + assertThat("job with unknown id should return as expected", jobManager.getJobStatus(unknownId, persistent), is(JobStatus.UNKNOWN)); + } + + CountDownLatch startLatch = new CountDownLatch(1), endLatch = new CountDownLatch(1); + DummyTwoLatchJob twoLatchJob = new DummyTwoLatchJob(new Params(0), startLatch, endLatch); + jobManager.start(); + long jobId = jobManager.addJob(twoLatchJob); + twoLatchJob.waitTillOnRun(); + assertThat("job should be in running state", jobManager.getJobStatus(jobId, false), is(JobStatus.RUNNING)); + startLatch.countDown();//let it run + endLatch.await();//wait till it finishes + Thread.sleep(500);//give some time to job manager to clear the job + assertThat("finished job should go to unknown state", jobManager.getJobStatus(jobId, false), is(JobStatus.UNKNOWN)); + + //network requiring job should not be ready + for(Integer i : networkRequiringJobIndices) { + assertThat("network requiring job should still be not-ready", jobManager.getJobStatus(ids[i], jobs[i].isPersistent()), is(JobStatus.WAITING_NOT_READY)); + } + jobManager.stop(); + networkUtil.setHasNetwork(true, true); + for(Integer i : networkRequiringJobIndices) { + assertThat("network requiring job should still be ready after network is there", jobManager.getJobStatus(ids[i], jobs[i].isPersistent()), is(JobStatus.WAITING_READY)); + } + + jobManager.start(); + int limit = 10; + while (jobManager.count() > 0 && limit-- > 0) { + Thread.sleep(1000); + } + assertThat("jobs should finish", jobManager.count(), is(0)); + for(int i = 0; i < jobs.length; i ++) { + //after all jobs finish, state should be unknown + assertThat("all jobs finished, states should be unknown", jobManager.getJobStatus(ids[i], jobs[i].isPersistent()), is(JobStatus.UNKNOWN)); + } + final long SHORT_SLEEP = 1000; + Job[] delayedJobs = new Job[]{ + new DummyJob(new Params(0).delayInMs(SHORT_SLEEP)), + new DummyJob(new Params(0).delayInMs(SHORT_SLEEP).persist()), + new DummyJob(new Params(0).delayInMs(SHORT_SLEEP * 10)), + new DummyJob(new Params(0).delayInMs(SHORT_SLEEP * 10).persist())}; + long[] delayedIds = new long[delayedJobs.length]; + for(int i = 0; i < delayedJobs.length; i ++) { + delayedIds[i] = jobManager.addJob(delayedJobs[i]); + } + + for(int i = 0; i < delayedJobs.length; i ++) { + assertThat("delayed job(" + i + ") should receive not ready status", jobManager.getJobStatus(delayedIds[i], delayedJobs[i].isPersistent()), is(JobStatus.WAITING_NOT_READY)); + } + jobManager.stop(); + //sleep + Thread.sleep(SHORT_SLEEP * 2); + for(int i = 0; i < delayedJobs.length; i ++) { + if(delayedJobs[i].getDelayInMs() == SHORT_SLEEP) { + assertThat("when enough time passes, delayed jobs should move to ready state", jobManager.getJobStatus(delayedIds[i], delayedJobs[i].isPersistent()), is(JobStatus.WAITING_READY)); + } else { + assertThat("delayed job should receive not ready status until their time comes", jobManager.getJobStatus(delayedIds[i], delayedJobs[i].isPersistent()), is(JobStatus.WAITING_NOT_READY)); + } + } + } +} diff --git a/jobqueue/test/com/path/android/jobqueue/test/jobqueue/JobQueueTestBase.java b/jobqueue/test/com/path/android/jobqueue/test/jobqueue/JobQueueTestBase.java index 9c3c188..2805e54 100644 --- a/jobqueue/test/com/path/android/jobqueue/test/jobqueue/JobQueueTestBase.java +++ b/jobqueue/test/com/path/android/jobqueue/test/jobqueue/JobQueueTestBase.java @@ -9,7 +9,7 @@ import com.path.android.jobqueue.test.util.JobQueueFactory; import org.fest.reflect.core.*; import static org.hamcrest.CoreMatchers.*; -import org.hamcrest.*; +import static org.hamcrest.MatcherAssert.*; import org.junit.Ignore; import org.junit.Test; @@ -28,34 +28,34 @@ public JobQueueTestBase(JobQueueFactory factory) { public void testBasicAddRemoveCount() throws Exception { final int ADD_COUNT = 6; JobQueue jobQueue = createNewJobQueue(); - MatcherAssert.assertThat((int) jobQueue.count(), equalTo(0)); - MatcherAssert.assertThat(jobQueue.nextJobAndIncRunCount(true, null), nullValue()); + assertThat((int) jobQueue.count(), equalTo(0)); + assertThat(jobQueue.nextJobAndIncRunCount(true, null), nullValue()); for (int i = 0; i < ADD_COUNT; i++) { JobHolder holder = createNewJobHolder(); jobQueue.insert(holder); - MatcherAssert.assertThat((int) jobQueue.count(), equalTo(i + 1)); - MatcherAssert.assertThat(holder.getId(), notNullValue()); + assertThat((int) jobQueue.count(), equalTo(i + 1)); + assertThat(holder.getId(), notNullValue()); jobQueue.insertOrReplace(holder); - MatcherAssert.assertThat((int) jobQueue.count(), equalTo(i + 1)); + assertThat((int) jobQueue.count(), equalTo(i + 1)); } JobHolder firstHolder = jobQueue.nextJobAndIncRunCount(true, null); - MatcherAssert.assertThat(firstHolder.getRunCount(), equalTo(1)); + assertThat(firstHolder.getRunCount(), equalTo(1)); //size should be down 1 - MatcherAssert.assertThat((int) jobQueue.count(), equalTo(ADD_COUNT - 1)); + assertThat((int) jobQueue.count(), equalTo(ADD_COUNT - 1)); //should return another job JobHolder secondHolder = jobQueue.nextJobAndIncRunCount(true, null); - MatcherAssert.assertThat(secondHolder.getRunCount(), equalTo(1)); + assertThat(secondHolder.getRunCount(), equalTo(1)); //size should be down 2 - MatcherAssert.assertThat((int) jobQueue.count(), equalTo(ADD_COUNT - 2)); + assertThat((int) jobQueue.count(), equalTo(ADD_COUNT - 2)); //second holder and first holder should have different ids - MatcherAssert.assertThat(firstHolder.getId(), not(secondHolder.getId())); + assertThat(firstHolder.getId(), not(secondHolder.getId())); jobQueue.remove(secondHolder); - MatcherAssert.assertThat((int) jobQueue.count(), equalTo(ADD_COUNT - 2)); + assertThat((int) jobQueue.count(), equalTo(ADD_COUNT - 2)); jobQueue.remove(secondHolder); //non existed job removed, count should be the same - MatcherAssert.assertThat((int) jobQueue.count(), equalTo(ADD_COUNT - 2)); + assertThat((int) jobQueue.count(), equalTo(ADD_COUNT - 2)); jobQueue.remove(firstHolder); - MatcherAssert.assertThat((int) jobQueue.count(), equalTo(ADD_COUNT - 2)); + assertThat((int) jobQueue.count(), equalTo(ADD_COUNT - 2)); } @Test @@ -70,11 +70,12 @@ public void testPriority() throws Exception { int minPriority = Integer.MAX_VALUE; for (int i = 0; i < JOB_LIMIT; i++) { JobHolder holder = jobQueue.nextJobAndIncRunCount(true, null); - MatcherAssert.assertThat(holder.getPriority() <= minPriority, is(true)); + assertThat(holder.getPriority() <= minPriority, is(true)); } - MatcherAssert.assertThat(jobQueue.nextJobAndIncRunCount(true, null), nullValue()); + assertThat(jobQueue.nextJobAndIncRunCount(true, null), nullValue()); } + @Test public void testDelayUntilWithPriority() throws Exception { JobQueue jobQueue = createNewJobQueue(); @@ -83,7 +84,7 @@ public void testDelayUntilWithPriority() throws Exception { JobHolder highPriorityHolder = createNewJobHolderWithDelayUntil(new Params(10), now + 20000 * JobManager.NS_PER_MS); jobQueue.insert(lowPriorityHolder); jobQueue.insert(highPriorityHolder); - MatcherAssert.assertThat("when asked, if lower priority job has less delay until, we should return it", + assertThat("when asked, if lower priority job has less delay until, we should return it", jobQueue.getNextJobDelayUntilNs(true), equalTo(lowPriorityHolder.getDelayUntilNs())); } @@ -97,20 +98,20 @@ public void testGroupId() throws Exception { long jobId4 = jobQueue.insert(createNewJobHolder(new Params(0).groupBy("group2"))); long jobId5 = jobQueue.insert(createNewJobHolder(new Params(0).groupBy("group1"))); JobHolder holder1 = jobQueue.nextJobAndIncRunCount(true, Arrays.asList(new String[]{"group2"})); - MatcherAssert.assertThat("first jobs should be from group group2 if group1 is excluded", + assertThat("first jobs should be from group group2 if group1 is excluded", holder1.getBaseJob().getRunGroupId(), equalTo("group1")); - MatcherAssert.assertThat("correct job should be returned if groupId is provided", + assertThat("correct job should be returned if groupId is provided", holder1.getId(), equalTo(jobId1)); - MatcherAssert.assertThat("no jobs should be returned if all groups are excluded", + assertThat("no jobs should be returned if all groups are excluded", jobQueue.nextJobAndIncRunCount(true, Arrays.asList(new String[]{"group1", "group2"})), is(nullValue())); long jobId6 = jobQueue.insert(createNewJobHolder(new Params(0))); - MatcherAssert.assertThat("both groups are disabled, null group job should be returned", + assertThat("both groups are disabled, null group job should be returned", jobQueue.nextJobAndIncRunCount(true, Arrays.asList(new String[]{"group1", "group2"})).getId(), is(jobId6)); - MatcherAssert.assertThat("if group1 is excluded, next job should be from group2", + assertThat("if group1 is excluded, next job should be from group2", jobQueue.nextJobAndIncRunCount(true, Arrays.asList(new String[]{"group1"})).getBaseJob().getRunGroupId() , equalTo("group2")); @@ -118,12 +119,12 @@ public void testGroupId() throws Exception { jobQueue.insertOrReplace(holder1); //ask for it again, should return the same holder because it is grouped JobHolder holder2 = jobQueue.nextJobAndIncRunCount(true, null); - MatcherAssert.assertThat("for grouped jobs, re-fetching job should work fine", + assertThat("for grouped jobs, re-fetching job should work fine", holder2.getId(), equalTo(holder1.getId())); JobHolder holder3 = jobQueue.nextJobAndIncRunCount(true, Arrays.asList(new String[]{"group1"})); - MatcherAssert.assertThat("if a group it excluded, next available from another group should be returned", + assertThat("if a group it excluded, next available from another group should be returned", holder3.getId(), equalTo(jobId4)); //add two more non-grouped jobs @@ -131,12 +132,12 @@ public void testGroupId() throws Exception { long jobId8 = jobQueue.insert(createNewJobHolder(new Params(0))); JobHolder holder4 = jobQueue.nextJobAndIncRunCount(true, Arrays.asList(new String[]{"group1", "group2"})); - MatcherAssert.assertThat("if all grouped jobs are excluded, non-grouped jobs should be returned", + assertThat("if all grouped jobs are excluded, non-grouped jobs should be returned", holder4.getId(), equalTo(jobId7)); jobQueue.insertOrReplace(holder4); //for non-grouped jobs, run counts should be respected - MatcherAssert.assertThat("if all grouped jobs are excluded, re-inserted highest priority job should still be returned", + assertThat("if all grouped jobs are excluded, re-inserted highest priority job should still be returned", jobQueue.nextJobAndIncRunCount(true, Arrays.asList(new String[]{"group1", "group2"})).getId(), equalTo(jobId7)); @@ -153,13 +154,13 @@ public void testDueDelayUntilWithPriority() throws Exception { long soonJobDelay = 2000; JobHolder highestPriorityDelayedJob = createNewJobHolderWithDelayUntil(new Params(12), now + soonJobDelay * JobManager.NS_PER_MS); long highestPriorityDelayedJobId = jobQueue.insert(highestPriorityDelayedJob); - MatcherAssert.assertThat("when asked, if job's due has passed, highest priority jobs's delay until should be " + + assertThat("when asked, if job's due has passed, highest priority jobs's delay until should be " + "returned", jobQueue.getNextJobDelayUntilNs(true), equalTo(highPriorityHolder.getDelayUntilNs())); //make sure soon job is valid now Thread.sleep(soonJobDelay); - MatcherAssert.assertThat("when a job's time come, it should be returned", + assertThat("when a job's time come, it should be returned", jobQueue.nextJobAndIncRunCount(true, null).getId(), equalTo(highestPriorityDelayedJobId)); } @@ -174,16 +175,16 @@ public void testDelayUntil() throws Exception { jobQueue.insert(networkJobHolder); jobQueue.insert(noNetworkJobHolder); - MatcherAssert.assertThat("if there is no network, delay until should be provided for no network job", + assertThat("if there is no network, delay until should be provided for no network job", jobQueue.getNextJobDelayUntilNs(false), equalTo(noNetworkJobHolder.getDelayUntilNs())); - MatcherAssert.assertThat("if there is network, delay until should be provided for network job because it is " + + assertThat("if there is network, delay until should be provided for network job because it is " + "sooner", jobQueue.getNextJobDelayUntilNs(true), equalTo(networkJobHolder.getDelayUntilNs())); JobHolder noNetworkJobHolder2 = createNewJobHolderWithDelayUntil(new Params(0), now + 100000 * JobManager.NS_PER_MS); jobQueue.insert(noNetworkJobHolder2); - MatcherAssert.assertThat("if there is network, any job's delay until should be returned", + assertThat("if there is network, any job's delay until should be returned", jobQueue.getNextJobDelayUntilNs(true), equalTo(noNetworkJobHolder2.getDelayUntilNs())); } @@ -194,13 +195,13 @@ public void testTruncate() throws Exception { for(int i = 0; i < LIMIT; i ++) { jobQueue.insert(createNewJobHolder()); } - MatcherAssert.assertThat("queue should have all jobs", jobQueue.count(), equalTo(LIMIT)); + assertThat("queue should have all jobs", jobQueue.count(), equalTo(LIMIT)); jobQueue.clear(); - MatcherAssert.assertThat("after clear, queue should be empty", jobQueue.count(), equalTo(0)); + assertThat("after clear, queue should be empty", jobQueue.count(), equalTo(0)); for(int i = 0; i < LIMIT; i ++) { jobQueue.insert(createNewJobHolder()); } - MatcherAssert.assertThat("if we add jobs again, count should match", jobQueue.count(), equalTo(LIMIT)); + assertThat("if we add jobs again, count should match", jobQueue.count(), equalTo(LIMIT)); } @Test @@ -230,8 +231,8 @@ public void testPriorityWithDelayedJobs() throws Exception { int lastPriority = Integer.MAX_VALUE; for(int i = 0; i < 5; i++) { JobHolder next = jobQueue.nextJobAndIncRunCount(true, null); - MatcherAssert.assertThat("next job should not be null", next, notNullValue()); - MatcherAssert.assertThat("next job's priority should be lower then previous for job " + i, next.getPriority() <= lastPriority, is(true)); + assertThat("next job should not be null", next, notNullValue()); + assertThat("next job's priority should be lower then previous for job " + i, next.getPriority() <= lastPriority, is(true)); lastPriority = next.getPriority(); } @@ -260,7 +261,7 @@ public void testSessionId() throws Exception { JobHolder jobHolder = createNewJobHolder(); jobQueue.insert(jobHolder); jobHolder = jobQueue.nextJobAndIncRunCount(true, null); - MatcherAssert.assertThat("session id should be attached to next job", + assertThat("session id should be attached to next job", jobHolder.getRunningSessionId(), equalTo(sessionId)); } @@ -276,10 +277,10 @@ public void testPriorityWithReAdd() throws Exception { int minPriority = Integer.MAX_VALUE; for (int i = 0; i < JOB_LIMIT; i++) { JobHolder holder = jobQueue.nextJobAndIncRunCount(true, null); - MatcherAssert.assertThat(holder.getPriority() <= minPriority, is(true)); + assertThat(holder.getPriority() <= minPriority, is(true)); jobQueue.insertOrReplace(holder); } - MatcherAssert.assertThat(jobQueue.nextJobAndIncRunCount(true, null), notNullValue()); + assertThat(jobQueue.nextJobAndIncRunCount(true, null), notNullValue()); } @Test @@ -288,10 +289,10 @@ public void testRemove() throws Exception { JobHolder holder = createNewJobHolder(); jobQueue.insert(holder); Long jobId = holder.getId(); - MatcherAssert.assertThat(jobQueue.nextJobAndIncRunCount(true, null).getId(), equalTo(jobId)); - MatcherAssert.assertThat(jobQueue.nextJobAndIncRunCount(true, null), is(nullValue())); + assertThat(jobQueue.nextJobAndIncRunCount(true, null).getId(), equalTo(jobId)); + assertThat(jobQueue.nextJobAndIncRunCount(true, null), is(nullValue())); jobQueue.remove(holder); - MatcherAssert.assertThat(jobQueue.nextJobAndIncRunCount(true, null), is(nullValue())); + assertThat(jobQueue.nextJobAndIncRunCount(true, null), is(nullValue())); } @Test @@ -299,15 +300,15 @@ public void testNetwork() throws Exception { JobQueue jobQueue = createNewJobQueue(); JobHolder jobHolder = createNewJobHolder(new Params(0)); jobQueue.insert(jobHolder); - MatcherAssert.assertThat("no network job should be returned even if there is no netowrk", + assertThat("no network job should be returned even if there is no netowrk", jobQueue.nextJobAndIncRunCount(false, null), notNullValue()); jobQueue.remove(jobHolder); jobHolder = createNewJobHolder(new Params(0).requireNetwork()); - MatcherAssert.assertThat("if there isn't any network, job with network requirement should not return", + assertThat("if there isn't any network, job with network requirement should not return", jobQueue.nextJobAndIncRunCount(false, null), nullValue()); - MatcherAssert.assertThat("if there is network, job with network requirement should be returned", + assertThat("if there is network, job with network requirement should be returned", jobQueue.nextJobAndIncRunCount(true, null), nullValue()); jobQueue.remove(jobHolder); @@ -317,17 +318,17 @@ public void testNetwork() throws Exception { long firstJobId = jobQueue.insert(jobHolder); long secondJobId = jobQueue.insert(jobHolder2); JobHolder retrieved = jobQueue.nextJobAndIncRunCount(false, null); - MatcherAssert.assertThat("one job should be returned w/o network", retrieved, notNullValue()); + assertThat("one job should be returned w/o network", retrieved, notNullValue()); if(retrieved != null) { - MatcherAssert.assertThat("no network job should be returned although it has lower priority", retrieved.getId(), equalTo(firstJobId)); + assertThat("no network job should be returned although it has lower priority", retrieved.getId(), equalTo(firstJobId)); } - MatcherAssert.assertThat("no other job should be returned w/o network", jobQueue.nextJobAndIncRunCount(false, null), nullValue()); + assertThat("no other job should be returned w/o network", jobQueue.nextJobAndIncRunCount(false, null), nullValue()); retrieved = jobQueue.nextJobAndIncRunCount(true, null); - MatcherAssert.assertThat("if network is back, network requiring job should be returned", retrieved, notNullValue()); + assertThat("if network is back, network requiring job should be returned", retrieved, notNullValue()); if(retrieved != null) { - MatcherAssert.assertThat("when there is network, network job should be returned", retrieved.getId(), equalTo(secondJobId)); + assertThat("when there is network, network job should be returned", retrieved.getId(), equalTo(secondJobId)); } //add first job back jobQueue.insertOrReplace(jobHolder); @@ -335,18 +336,18 @@ public void testNetwork() throws Exception { jobQueue.insertOrReplace(jobHolder2); retrieved = jobQueue.nextJobAndIncRunCount(true, null); - MatcherAssert.assertThat("if network is back, job w/ higher priority should be returned", retrieved, notNullValue()); + assertThat("if network is back, job w/ higher priority should be returned", retrieved, notNullValue()); if(retrieved != null) { - MatcherAssert.assertThat("if network is back, job w/ higher priority should be returned", retrieved.getId(), equalTo(secondJobId)); + assertThat("if network is back, job w/ higher priority should be returned", retrieved.getId(), equalTo(secondJobId)); } jobQueue.insertOrReplace(jobHolder2); JobHolder highestPriorityJob = createNewJobHolder(new Params(10)); long highestPriorityJobId = jobQueue.insert(highestPriorityJob); retrieved = jobQueue.nextJobAndIncRunCount(true, null); - MatcherAssert.assertThat("w/ or w/o network, highest priority should be returned", retrieved, notNullValue()); + assertThat("w/ or w/o network, highest priority should be returned", retrieved, notNullValue()); if(retrieved != null) { - MatcherAssert.assertThat("w/ or w/o network, highest priority should be returned", retrieved.getId(), equalTo(highestPriorityJobId)); + assertThat("w/ or w/o network, highest priority should be returned", retrieved.getId(), equalTo(highestPriorityJobId)); } //TODO test delay until @@ -355,40 +356,40 @@ public void testNetwork() throws Exception { @Test public void testCountReadyJobs() throws Exception { JobQueue jobQueue = createNewJobQueue(); - MatcherAssert.assertThat("initial count should be 0 for ready jobs", jobQueue.countReadyJobs(true, null), equalTo(0)); + assertThat("initial count should be 0 for ready jobs", jobQueue.countReadyJobs(true, null), equalTo(0)); //add some jobs jobQueue.insert(createNewJobHolder()); jobQueue.insert(createNewJobHolder(new Params(0).requireNetwork())); long now = System.nanoTime(); long delay = 1000; jobQueue.insert(createNewJobHolderWithDelayUntil(new Params(0), now + TimeUnit.MILLISECONDS.toNanos(delay))); - MatcherAssert.assertThat("ready count should be 1 if there is no network", jobQueue.countReadyJobs(false, null), equalTo(1)); - MatcherAssert.assertThat("ready count should be 2 if there is network", jobQueue.countReadyJobs(true, null), equalTo(2)); + assertThat("ready count should be 1 if there is no network", jobQueue.countReadyJobs(false, null), equalTo(1)); + assertThat("ready count should be 2 if there is network", jobQueue.countReadyJobs(true, null), equalTo(2)); Thread.sleep(delay); - MatcherAssert.assertThat("when needed delay time passes, ready count should be 3", jobQueue.countReadyJobs(true, null), equalTo(3)); - MatcherAssert.assertThat("when needed delay time passes but no network, ready count should be 2", jobQueue.countReadyJobs(false, null), equalTo(2)); + assertThat("when needed delay time passes, ready count should be 3", jobQueue.countReadyJobs(true, null), equalTo(3)); + assertThat("when needed delay time passes but no network, ready count should be 2", jobQueue.countReadyJobs(false, null), equalTo(2)); jobQueue.insert(createNewJobHolder(new Params(5).groupBy("group1"))); jobQueue.insert(createNewJobHolder(new Params(5).groupBy("group1"))); - MatcherAssert.assertThat("when more than 1 job from same group is created, ready jobs should increment only by 1", + assertThat("when more than 1 job from same group is created, ready jobs should increment only by 1", jobQueue.countReadyJobs(true, null), equalTo(4)); - MatcherAssert.assertThat("excluding groups should work", + assertThat("excluding groups should work", jobQueue.countReadyJobs(true, Arrays.asList(new String[]{"group1"})), equalTo(3)); - MatcherAssert.assertThat("giving a non-existing group should not fool the count", + assertThat("giving a non-existing group should not fool the count", jobQueue.countReadyJobs(true, Arrays.asList(new String[]{"group3423"})), equalTo(4)); jobQueue.insert(createNewJobHolder(new Params(3).groupBy("group2"))); - MatcherAssert.assertThat("when a job from another group is added, ready job count should inc", + assertThat("when a job from another group is added, ready job count should inc", jobQueue.countReadyJobs(true, null), equalTo(5)); now = System.nanoTime(); jobQueue.insert(createNewJobHolderWithDelayUntil(new Params(3).groupBy("group3"), now + TimeUnit.MILLISECONDS.toNanos(delay))); - MatcherAssert.assertThat("when a delayed job from another group is added, ready count should not change", + assertThat("when a delayed job from another group is added, ready count should not change", jobQueue.countReadyJobs(true, null), equalTo(5)); jobQueue.insert(createNewJobHolder(new Params(3).groupBy("group3"))); - MatcherAssert.assertThat("when another job from delayed group is added, ready job count should inc", + assertThat("when another job from delayed group is added, ready job count should inc", jobQueue.countReadyJobs(true, null), equalTo(6)); Thread.sleep(delay); - MatcherAssert.assertThat("when delay passes and a job from existing group becomes available, ready job count should not change", + assertThat("when delay passes and a job from existing group becomes available, ready job count should not change", jobQueue.countReadyJobs(true, null), equalTo(6)); - MatcherAssert.assertThat("when some groups are excluded, count should be correct", + assertThat("when some groups are excluded, count should be correct", jobQueue.countReadyJobs(true, Arrays.asList(new String[]{"group1", "group3"})), equalTo(4)); //jobs w/ same group id but with different persistence constraints should not fool the count @@ -397,11 +398,11 @@ public void testCountReadyJobs() throws Exception { jobQueue.insert(createNewJobHolderWithDelayUntil(new Params(0).groupBy("group10"), now + 1000)); jobQueue.insert(createNewJobHolderWithDelayUntil(new Params(0).persist().groupBy("group10"), now - 1000)); jobQueue.insert(createNewJobHolderWithDelayUntil(new Params(0).groupBy("group10"), now - 1000)); - MatcherAssert.assertThat("when many jobs are added w/ different constraints but same group id, ready count should not be fooled", + assertThat("when many jobs are added w/ different constraints but same group id, ready count should not be fooled", jobQueue.countReadyJobs(true, Arrays.asList(new String[]{"group1", "group3"})), equalTo(5)); - MatcherAssert.assertThat("when many jobs are added w/ different constraints but same group id, ready count should not be fooled", + assertThat("when many jobs are added w/ different constraints but same group id, ready count should not be fooled", jobQueue.countReadyJobs(true, null), equalTo(7)); - MatcherAssert.assertThat("when many jobs are added w/ different constraints but same group id, ready count should not be fooled", + assertThat("when many jobs are added w/ different constraints but same group id, ready count should not be fooled", jobQueue.countReadyJobs(false, Arrays.asList(new String[]{"group1", "group3"})), equalTo(4)); } @@ -424,20 +425,72 @@ public void testJobFields() throws Exception { for (int i = 0; i < 2; i++) { JobHolder received = jobQueue.nextJobAndIncRunCount(true, null); - MatcherAssert.assertThat("job id should be preserved", received.getId(), equalTo(id)); - MatcherAssert.assertThat("job priority should be preserved", received.getPriority(), equalTo(priority)); - MatcherAssert.assertThat("job session id should be assigned", received.getRunningSessionId(), equalTo(sessionId)); - MatcherAssert.assertThat("job run count should be incremented", received.getRunCount(), equalTo(runCount + i + 1)); + assertThat("job id should be preserved", received.getId(), equalTo(id)); + assertThat("job priority should be preserved", received.getPriority(), equalTo(priority)); + assertThat("job session id should be assigned", received.getRunningSessionId(), equalTo(sessionId)); + assertThat("job run count should be incremented", received.getRunCount(), equalTo(runCount + i + 1)); jobQueue.insertOrReplace(received); } } + private void assertJob(JobQueue jobQueue, String msg, long id, /*nullable*/ JobHolder holder) { + if(holder == null) { + assertThat(msg, jobQueue.findJobById(id), nullValue()); + return; + } + assertThat(msg + "(existence check)", jobQueue.findJobById(id), notNullValue()); + assertThat(msg + "(id check)", jobQueue.findJobById(id).getId(), is(holder.getId())); + } + + @Test + public void testFindJobHolderById() { + JobQueue jobQueue = createNewJobQueue(); + assertJob(jobQueue, "non existing job (negative id)", -4, null); + assertJob(jobQueue, "non existing job (positive id)", +4, null); + final int LIMIT = 100; + JobHolder[] holders = new JobHolder[LIMIT]; + long[] ids = new long[LIMIT]; + for(int i = 0; i < LIMIT; i++) { + holders[i] = createNewJobHolder(new Params((int) (Math.random() * 50)).setPersistent(Math.random() < .5).setRequiresNetwork(Math.random() < .5)); + ids[i] = jobQueue.insert(holders[i]); + assertJob(jobQueue, "job by id should work for inserted job", ids[i], holders[i]); + } + final int REMOVE_CNT = LIMIT / 2; + for(int i = 0; i < REMOVE_CNT; i++) { + int ind = (int) (Math.random() * LIMIT); + if(holders[ind] == null) { + continue; + } + //remove some randomly, up to half + jobQueue.remove(holders[ind]); + holders[ind] = null; + } + //re-query all, ensure we can still find non-removed jobs and not find removed jobs + for(int i = 0; i < LIMIT; i++) { + if(holders[i] != null) { + assertJob(jobQueue, "if job is still in the Q, it should be returned", ids[i], holders[i]); + //re add job + jobQueue.insertOrReplace(holders[i]); + //re-test after re-add + assertJob(jobQueue, "after re-insert, if job is still in the Q, it should be returned", ids[i], holders[i]); + } else { + assertJob(jobQueue, "removed job should not be returned in id query", ids[i], null); + } + } + jobQueue.clear(); + for(int i = 0; i < LIMIT; i++) { + assertJob(jobQueue, "after clear, find by id should return null", ids[i], null); + } + } + protected JobHolder createNewJobHolder() { return createNewJobHolder(new Params(0)); } protected JobHolder createNewJobHolder(Params params) { - return new JobHolder(null, getPriorityField(params).get(), getGroupIdField(params).get(), 0, new DummyJob(params), System.nanoTime(), Long.MIN_VALUE, JobManager.NOT_RUNNING_SESSION_ID); + long delay = getDelayMsField(params).get(); + return new JobHolder(null, getPriorityField(params).get(), getGroupIdField(params).get(), 0, new DummyJob(params), System.nanoTime(), + delay > 0 ? System.nanoTime() + delay * JobManager.NS_PER_MS : JobManager.NOT_DELAYED_JOB_DELAY, JobManager.NOT_RUNNING_SESSION_ID); } private JobHolder createNewJobHolderWithDelayUntil(Params params, long delayUntil) { @@ -446,7 +499,7 @@ private JobHolder createNewJobHolderWithDelayUntil(Params params, long delayUnti return jobHolder; } - private JobQueue createNewJobQueue() { + protected JobQueue createNewJobQueue() { return createNewJobQueueWithSessionId(System.nanoTime()); } diff --git a/jobqueue/test/com/path/android/jobqueue/test/jobqueue/NonPersistentJobQueueTest.java b/jobqueue/test/com/path/android/jobqueue/test/jobqueue/NonPersistentJobQueueTest.java index 32ca824..aec3924 100644 --- a/jobqueue/test/com/path/android/jobqueue/test/jobqueue/NonPersistentJobQueueTest.java +++ b/jobqueue/test/com/path/android/jobqueue/test/jobqueue/NonPersistentJobQueueTest.java @@ -1,10 +1,16 @@ package com.path.android.jobqueue.test.jobqueue; +import com.path.android.jobqueue.JobHolder; +import com.path.android.jobqueue.JobManager; import com.path.android.jobqueue.JobQueue; +import com.path.android.jobqueue.Params; import com.path.android.jobqueue.nonPersistentQueue.NonPersistentPriorityQueue; import com.path.android.jobqueue.test.util.JobQueueFactory; +import static org.hamcrest.CoreMatchers.*; +import org.hamcrest.*; +import org.junit.Test; import org.junit.runner.RunWith; -import org.robolectric.RobolectricTestRunner; +import org.robolectric.*; @RunWith(RobolectricTestRunner.class) public class NonPersistentJobQueueTest extends JobQueueTestBase { @@ -16,4 +22,32 @@ public JobQueue createNew(long sessionId, String id) { } }); } + + /** + * issue #21 https://github.com/path/android-priority-jobqueue/issues/21 + */ + @Test + public void testTooManyQueueChanges() throws InterruptedException { + JobQueue jobQueue = createNewJobQueue(); + int limit = 10000; + long delayMs = 2000; + long then = System.nanoTime() + delayMs * JobManager.NS_PER_MS; + for(int i = 0; i < limit; i++) { + jobQueue.insert(createNewJobHolder(new Params(0).requireNetwork().delayInMs(delayMs))); + } + + MatcherAssert.assertThat("all jobs require network, should return null", jobQueue.nextJobAndIncRunCount(false, null), nullValue()); + long sleep = then - System.nanoTime(); + sleep += JobManager.NS_PER_MS * 1000; + if (sleep > 0) { + Thread.sleep(sleep / JobManager.NS_PER_MS); + } + //should be able to get it w/o an overflow + for(int i = 0; i < limit; i++) { + JobHolder holder = jobQueue.nextJobAndIncRunCount(true, null); + MatcherAssert.assertThat("should get a next job", holder, notNullValue()); + jobQueue.remove(holder); + } + + } }