From 26a98384f45e3b44277d5c3a03ecc4b21d5e1954 Mon Sep 17 00:00:00 2001 From: Ben Manes Date: Fri, 4 Aug 2017 11:23:19 -0700 Subject: [PATCH] Don't refresh automatically when waiting for a slow reload (fixed #175) If the load time exceeded the refreshAfterWrite time then a new refresh is triggered and the previous was ignored. This was due to how the write timestamp is used, which was set to the current time to delay expiration and refresh. When the refresh completes it CAS's to the new value only if no other writes occurred (so as to not stomp explicit writes and reload a stale read). The slow reload meant that only the last refresh would be honored, which may not occur for a long time on a busy system. This also removes a sync vs async difference that I never remembered why it was in place. Async didn't have this problem due to the timestamp being pushed out into the distant future. Since I didn't recall why these were special cased I had kept them as is, hoping the previous me was smarter than the current. Seems both were pretty dumb. --- .../caffeine/cache/BoundedLocalCache.java | 3 +- .../benmanes/caffeine/cache/Caffeine.java | 6 +- .../caffeine/cache/RefreshAfterWriteTest.java | 44 +++ .../cache/testing/TrackingExecutor.java | 8 +- config/findbugs/exclude.xml | 5 + gradle/dependencies.gradle | 18 +- gradle/gradle-completion.bash | 317 ++++++++++++------ 7 files changed, 272 insertions(+), 129 deletions(-) diff --git a/caffeine/src/main/java/com/github/benmanes/caffeine/cache/BoundedLocalCache.java b/caffeine/src/main/java/com/github/benmanes/caffeine/cache/BoundedLocalCache.java index 81e1110299..5a9706a633 100644 --- a/caffeine/src/main/java/com/github/benmanes/caffeine/cache/BoundedLocalCache.java +++ b/caffeine/src/main/java/com/github/benmanes/caffeine/cache/BoundedLocalCache.java @@ -739,6 +739,7 @@ boolean hasExpired(Node node, long now) { * @param node the entry to evict * @param cause the reason to evict * @param now the current time, used only if expiring + * @return if the entry was evicted */ @GuardedBy("evictionLock") @SuppressWarnings({"PMD.CollapsibleIfStatements", "GuardedByChecker"}) @@ -864,7 +865,7 @@ void refreshIfNeeded(Node node, long now) { K key; V oldValue; long oldWriteTime = node.getWriteTime(); - long refreshWriteTime = isAsync ? (now + Async.MAXIMUM_EXPIRY) : now; + long refreshWriteTime = (now + Async.MAXIMUM_EXPIRY); if (((now - oldWriteTime) > refreshAfterWriteNanos()) && ((key = node.getKey()) != null) && ((oldValue = node.getValue()) != null) && node.casWriteTime(oldWriteTime, refreshWriteTime)) { diff --git a/caffeine/src/main/java/com/github/benmanes/caffeine/cache/Caffeine.java b/caffeine/src/main/java/com/github/benmanes/caffeine/cache/Caffeine.java index 05e31e2edb..c846422567 100644 --- a/caffeine/src/main/java/com/github/benmanes/caffeine/cache/Caffeine.java +++ b/caffeine/src/main/java/com/github/benmanes/caffeine/cache/Caffeine.java @@ -20,6 +20,8 @@ import java.lang.ref.SoftReference; import java.lang.ref.WeakReference; import java.util.ConcurrentModificationException; +import java.util.IdentityHashMap; +import java.util.Map; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.Executor; @@ -411,7 +413,9 @@ Weigher getWeigher(boolean isAsync) { * {@link WeakReference} (by default, strong references are used). *

* Warning: when this method is used, the resulting cache will use identity ({@code ==}) - * comparison to determine equality of keys. + * comparison to determine equality of keys. Its {@link Cache#asMap} view will therefore + * technically violate the {@link Map} specification (in the same way that {@link IdentityHashMap} + * does). *

* Entries with keys that have been garbage collected may be counted in * {@link Cache#estimatedSize()}, but will never be visible to read or write operations; such diff --git a/caffeine/src/test/java/com/github/benmanes/caffeine/cache/RefreshAfterWriteTest.java b/caffeine/src/test/java/com/github/benmanes/caffeine/cache/RefreshAfterWriteTest.java index 393d0471da..c781d72d8f 100644 --- a/caffeine/src/test/java/com/github/benmanes/caffeine/cache/RefreshAfterWriteTest.java +++ b/caffeine/src/test/java/com/github/benmanes/caffeine/cache/RefreshAfterWriteTest.java @@ -26,8 +26,10 @@ import static org.hamcrest.Matchers.both; import static org.hamcrest.Matchers.contains; import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.Matchers.empty; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.not; import static org.hamcrest.Matchers.nullValue; import static org.hamcrest.Matchers.sameInstance; @@ -36,8 +38,10 @@ import java.util.Map; import java.util.Set; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Function; import org.testng.annotations.Listeners; @@ -60,6 +64,7 @@ import com.github.benmanes.caffeine.cache.testing.CheckNoWriter; import com.github.benmanes.caffeine.cache.testing.RefreshAfterWrite; import com.github.benmanes.caffeine.cache.testing.RemovalNotification; +import com.github.benmanes.caffeine.cache.testing.TrackingExecutor; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; @@ -206,6 +211,45 @@ public void get_sameFuture(CacheContext context) { await().until(() -> cache.synchronous().getIfPresent(key), is(-key)); } + @Test(dataProvider = "caches") + @CacheSpec(implementation = Implementation.Caffeine, population = Population.EMPTY, + refreshAfterWrite = Expire.ONE_MINUTE, executor = CacheExecutor.THREADED) + public void get_slowRefresh(CacheContext context) { + Integer key = context.absentKey(); + Integer originalValue = context.absentValue(); + AtomicBoolean reloaded = new AtomicBoolean(); + AtomicInteger reloading = new AtomicInteger(); + ThreadPoolExecutor executor = (ThreadPoolExecutor) + ((TrackingExecutor) context.executor()).delegate(); + LoadingCache cache = context.build(new CacheLoader() { + @Override public Integer load(Integer key) { + throw new AssertionError(); + } + @Override public Integer reload(Integer key, Integer oldValue) { + int count = reloading.incrementAndGet(); + await().untilTrue(reloaded); + return count; + } + }); + + cache.put(key, originalValue); + + context.ticker().advance(2, TimeUnit.MINUTES); + assertThat(cache.get(key), is(originalValue)); + + await().untilAtomic(reloading, is(1)); + assertThat(cache.getIfPresent(key), is(originalValue)); + + context.ticker().advance(2, TimeUnit.MINUTES); + assertThat(cache.get(key), is(originalValue)); + + reloaded.set(true); + await().until(() -> cache.get(key), is(not(originalValue))); + await().until(executor::getQueue, is(empty())); + assertThat(reloading.get(), is(1)); + assertThat(cache.get(key), is(1)); + } + @Test(dataProvider = "caches") @CacheSpec(refreshAfterWrite = Expire.ONE_MINUTE, loader = Loader.NULL) public void get_null(AsyncLoadingCache cache, CacheContext context) { diff --git a/caffeine/src/test/java/com/github/benmanes/caffeine/cache/testing/TrackingExecutor.java b/caffeine/src/test/java/com/github/benmanes/caffeine/cache/testing/TrackingExecutor.java index 5e1abd8308..3ed82ca245 100644 --- a/caffeine/src/test/java/com/github/benmanes/caffeine/cache/testing/TrackingExecutor.java +++ b/caffeine/src/test/java/com/github/benmanes/caffeine/cache/testing/TrackingExecutor.java @@ -29,12 +29,10 @@ */ public final class TrackingExecutor extends ForwardingExecutorService { private final ExecutorService delegate; - private final AtomicInteger totalTasks; private final AtomicInteger failures; public TrackingExecutor(ExecutorService executor) { delegate = requireNonNull(executor); - totalTasks = new AtomicInteger(); failures = new AtomicInteger(); } @@ -55,16 +53,12 @@ public void execute(Runnable command) { } } - public int totalTasksCount() { - return totalTasks.get(); - } - public int failureCount() { return failures.get(); } @Override - protected ExecutorService delegate() { + public ExecutorService delegate() { return delegate; } } diff --git a/config/findbugs/exclude.xml b/config/findbugs/exclude.xml index 1b4cf14d51..7380d6be8c 100644 --- a/config/findbugs/exclude.xml +++ b/config/findbugs/exclude.xml @@ -27,6 +27,11 @@ + + + + + diff --git a/gradle/dependencies.gradle b/gradle/dependencies.gradle index 216e60d1ae..afa04229da 100644 --- a/gradle/dependencies.gradle +++ b/gradle/dependencies.gradle @@ -38,7 +38,7 @@ ext { jsr305: '3.0.2', jsr330: '1', stream: '2.9.5', - univocityParsers: '2.4.1', + univocityParsers: '2.5.1', ycsb: '0.12.0', xz: '1.6', ] @@ -51,7 +51,7 @@ ext { junit: '4.12', mockito: '2.8.47', paxExam: '4.11.0', - testng: '6.11', + testng: '6.12', truth: '0.24', ] benchmarkVersions = [ @@ -62,33 +62,33 @@ ext { ehcache3: '3.3.1', elasticSearch: '5.4.0', expiringMap: '0.5.8', - jackrabbit: '1.7.3', - jamm: '0.3.1', + jackrabbit: '1.7.5', + jamm: '0.3.2', javaObjectLayout: '0.8', jmh: 1.19, koloboke: '0.6.8', ohc: '0.6.1', - rapidoid: '5.3.5', + rapidoid: '5.4.0', slf4j: '1.7.25', tcache: '1.0.3', ] pluginVersions = [ buildscan: '1.8', buildscanRecipes: '0.2.0', - checkstyle: '8.0', + checkstyle: '8.1', coveralls: '2.8.1', coverity: '1.0.10', errorProne: '0.0.10', jacoco: '0.7.9', jmh: '0.4.2', - jmhReport: '0.4.1', + jmhReport: '0.5.0', nexus: '2.3.1', pmd: '5.8.1', propdeps: '0.0.10.RELEASE', semanticVersioning: '1.1.0', shadow: '2.0.1', sonarqube: '2.5', - spotbugs: '1.0', + spotbugs: '1.2', stats: '0.2.0', versions: '0.15.0', ] @@ -130,7 +130,7 @@ ext { exclude group: 'org.hamcrest' }, osgiCompile: [ - 'org.apache.felix:org.apache.felix.framework:5.6.4', + 'org.apache.felix:org.apache.felix.framework:5.6.6', "org.ops4j.pax.exam:pax-exam-junit4:${testVersions.paxExam}", ], osgiRuntime: [ diff --git a/gradle/gradle-completion.bash b/gradle/gradle-completion.bash index f0347be136..d3ce6eb597 100644 --- a/gradle/gradle-completion.bash +++ b/gradle/gradle-completion.bash @@ -2,29 +2,66 @@ # Avoid inaccurate completions for subproject tasks COMP_WORDBREAKS=$(echo "$COMP_WORDBREAKS" | sed -e 's/://g') -_gradle() -{ - local cur=${COMP_WORDS[COMP_CWORD]} - local args - # Use Gradle wrapper when it exists. - local gradle_cmd='gradle' - if [[ -x ./gradlew ]]; then - gradle_cmd='./gradlew' - fi +__gradle-set-project-root-dir() { + local dir=`pwd` + project_root_dir=`pwd` + while [[ $dir != '/' ]]; do + if [[ -f "$dir/settings.gradle" || -f "$dir/gradlew" ]]; then + project_root_dir=$dir + return 0 + fi + dir="$(dirname "$dir")" + done + return 1 +} - local cache_dir="$HOME/.gradle/completion" +__gradle-init-cache-dir() { + cache_dir="$HOME/.gradle/completion" mkdir -p $cache_dir +} + +__gradle-set-build-file() { + # Look for default build script in the settings file (settings.gradle by default) + # Otherwise, default is the file 'build.gradle' in the current directory. + gradle_build_file="$project_root_dir/build.gradle" + if [[ -f "$project_root_dir/settings.gradle" ]]; then + local build_file_name=$(grep "^rootProject\.buildFileName" "$project_root_dir/settings.gradle" | \ + sed -n -e "s/rootProject\.buildFileName = [\'\"]\(.*\)[\'\"]/\1/p") + gradle_build_file="$project_root_dir/${build_file_name:-build.gradle}" + fi +} + +__gradle-set-cache-name() { + # Cache name is constructed from the absolute path of the build file. + cache_name=$(echo $gradle_build_file | sed -e 's/\//_/g') +} + +__gradle-set-files-checksum() { + # Cache MD5 sum of all Gradle scripts and modified timestamps + if builtin command -v md5 > /dev/null; then + gradle_files_checksum=$(md5 -q -s "$(cat "$cache_dir/$cache_name" | xargs ls -o 2>/dev/null)") + elif builtin command -v md5sum > /dev/null; then + gradle_files_checksum=$(cat "$cache_dir/$cache_name" | xargs ls -o 2>/dev/null | md5sum | awk '{print $1}') + else + echo "Cannot generate completions as neither md5 nor md5sum exist on \$PATH" + fi +} + +__gradle-generate-script-cache() { # Invalidate cache after 3 weeks by default local cache_ttl_mins=${GRADLE_CACHE_TTL_MINUTES:-30240} local script_exclude_pattern=${GRADLE_COMPLETION_EXCLUDE_PATTERN:-"/(build|integTest|out)/"} - # Set bash internal field separator to '\n' - # This allows us to provide descriptions for options and tasks - local OLDIFS="$IFS" - local IFS=$'\n' + if [[ ! $(find $cache_dir/$cache_name -mmin -$cache_ttl_mins 2>/dev/null) ]]; then + # Cache all Gradle scripts + local gradle_build_scripts=$(find $project_root_dir -type f -name "*.gradle" -o -name "*.gradle.kts" 2>/dev/null | egrep -v "$script_exclude_pattern") + printf "%s\n" "${gradle_build_scripts[@]}" > $cache_dir/$cache_name + fi +} - if [[ ${cur} == --* ]]; then - args="--build-file - Specifies the build file +__gradle-long-options() { + local args="--build-cache - Enables the Gradle build cache +--build-file - Specifies the build file --configure-on-demand - Only relevant projects are configured --console - Type of console output to generate (plain auto rich) --continue - Continues task execution after a task failure @@ -41,8 +78,10 @@ _gradle() --info - Set log level to INFO --init-script - Specifies an initialization script --max-workers - Set the maximum number of workers that Gradle may use +--no-build-cache - Do not use the Gradle build cache --no-daemon - Do not use the Gradle Daemon --no-rebuild - Do not rebuild project dependencies +--no-scan - Do not create a build scan --no-search-upwards - Do not search in parent directories for a settings.gradle --offline - Build without accessing network resources --parallel - Build projects in parallel @@ -53,26 +92,36 @@ _gradle() --quiet - Log errors only --recompile-scripts - Forces scripts to be recompiled, bypassing caching --refresh-dependencies - Refresh the state of dependencies ---rerun-task - Specifies that any task optimization is ignored +--rerun-tasks - Specifies that any task optimization is ignored +--scan - Create a build scan --settings-file - Specifies the settings file --stacktrace - Print out the stacktrace also for user exceptions --status - Print Gradle Daemon status --stop - Stop all Gradle Daemons --system-prop - Set a system property ---version - Prints Gradle version info" - COMPREPLY=( $(compgen -W "$args" -- "$cur") ) - elif [[ ${cur} == -D* ]]; then - args="-Dorg.gradle.cache.reserved.mb= - Reserve Gradle Daemon memory for operations +--version - Prints Gradle version info +--warn - Log warnings and errors only" + COMPREPLY=( $(compgen -W "$args" -- "${COMP_WORDS[COMP_CWORD]}") ) +} + +__gradle-properties() { + local args="-Dorg.gradle.cache.reserved.mb= - Reserve Gradle Daemon memory for operations +-Dorg.gradle.caching= - Set true to enable Gradle build cache -Dorg.gradle.daemon.debug= - Set true to debug Gradle Daemon -Dorg.gradle.daemon.idletimeout= - Kill Gradle Daemon after # idle millis -Dorg.gradle.debug= - Set true to debug Gradle Client -Dorg.gradle.jvmargs= - Set JVM arguments -Dorg.gradle.java.home= - Set JDK home dir +-Dorg.gradle.logging.level= - Set default Gradle log level (quiet warn lifecycle info debug) -Dorg.gradle.parallel= - Set true to enable parallel project builds (incubating) --Dorg.gradle.parallel.intra= - Set true to enable intra-project parallel builds (incubating)" - COMPREPLY=( $(compgen -W "$args" -- "$cur") ) - elif [[ ${cur} == -* ]]; then - args="-? - Shows a help message +-Dorg.gradle.parallel.intra= - Set true to enable intra-project parallel builds (incubating) +-Dorg.gradle.workers.max= - Set the number of workers Gradle is allowed to use" + COMPREPLY=( $(compgen -W "$args" -- "${COMP_WORDS[COMP_CWORD]}") ) + return 0 +} + +__gradle-short-options() { + local args="-? - Shows a help message -a - Do not rebuild project dependencies -b - Specifies the build file -c - Specifies the settings file @@ -87,107 +136,150 @@ _gradle() -t - Continuous mode. Automatically re-run build after changes -u - Do not search in parent directories for a settings.gradle -v - Prints Gradle version info +-w - Log warnings and errors only -x - Specify a task to be excluded -D - Set a system property -I - Specifies an initialization script -P - Sets a project property of the root project -S - Print out the full (very verbose) stacktrace" - COMPREPLY=( $(compgen -W "$args" -- "$cur") ) - else - # Look for default build script in the settings file (settings.gradle by default) - # Otherwise, default is the file 'build.gradle' in the current directory. - local gradle_buildfile=build.gradle - if [[ -f settings.gradle ]]; then - local build_file_name=$(grep "^rootProject\.buildFileName" settings.gradle | \ - sed -n -e "s/rootProject\.buildFileName = [\'\"]\(.*\)[\'\"]/\1/p") - gradle_buildfile=${build_file_name:-build.gradle} - fi + COMPREPLY=( $(compgen -W "$args" -- "${COMP_WORDS[COMP_CWORD]}") ) +} - local gradle_files_checksum - # If we're in a Gradle project, check if completion cache is up-to-date - if [[ -f $gradle_buildfile ]]; then - # Cache name is constructed from the absolute path of the build file. - local cache_name=$(echo $(pwd)/$gradle_buildfile | sed -e 's/\//_/g') - if [[ ! $(find $cache_dir/$cache_name -mmin -$cache_ttl_mins 2>/dev/null) ]]; then - # Cache all Gradle scripts - local gradle_build_scripts=$(find . -type f -name "*.gradle" -o -name "*.gradle.kts" 2>/dev/null | egrep -v "$script_exclude_pattern") - printf "%s\n" "${gradle_build_scripts[@]}" > $cache_dir/$cache_name - fi +__gradle-notify-tasks-cache-build() { + # Notify user of cache rebuild + echo -e " (Building completion cache. Please wait)\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\c" + __gradle-generate-tasks-cache + # Remove "please wait" message by writing a bunch of spaces then moving back to the left + echo -e " \b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\c" +} + +__gradle-generate-tasks-cache() { + __gradle-set-files-checksum + + # Use Gradle wrapper when it exists. + local gradle_cmd="gradle" + if [[ -x "$project_root_dir/gradlew" ]]; then + gradle_cmd="$project_root_dir/gradlew" + fi - # Cache MD5 sum of all Gradle scripts and modified timestamps - if builtin command -v md5 > /dev/null; then - gradle_files_checksum=$(md5 -q -s "$(cat "$cache_dir/$cache_name" | xargs ls -o 2>/dev/null)") - elif builtin command -v md5sum > /dev/null; then - gradle_files_checksum=$(cat "$cache_dir/$cache_name" | xargs ls -o 2>/dev/null | md5sum | awk '{print $1}') + # Run gradle to retrieve possible tasks and cache. + # Reuse Gradle Daemon if IDLE but don't start a new one. + local gradle_tasks_output + if [[ ! -z "$($gradle_cmd --status 2>/dev/null | grep IDLE)" ]]; then + gradle_tasks_output="$($gradle_cmd -b $gradle_build_file --daemon -q tasks --all)" + else + gradle_tasks_output="$($gradle_cmd -b $gradle_build_file --no-daemon -q tasks --all)" + fi + local output_line + local task_description + local -a gradle_all_tasks=() + local -a root_tasks=() + local -a subproject_tasks=() + for output_line in $gradle_tasks_output; do + if [[ $output_line =~ ^([[:lower:]][[:alnum:][:punct:]]*)([[:space:]]-[[:space:]]([[:print:]]*))? ]]; then + task_name="${BASH_REMATCH[1]}" + task_description="${BASH_REMATCH[3]}" + gradle_all_tasks+=( "$task_name - $task_description" ) + # Completion for subproject tasks with ':' prefix + if [[ $task_name =~ ^([[:alnum:][:punct:]]+):([[:alnum:]]+) ]]; then + gradle_all_tasks+=( ":$task_name - $task_description" ) + subproject_tasks+=( "${BASH_REMATCH[2]}" ) else - echo "Cannot generate completions as neither md5 nor md5sum exist on \$PATH" - return 1 + root_tasks+=( "$task_name" ) fi + fi + done - if [[ ! -f $cache_dir/$cache_name.md5 || $gradle_files_checksum != "$(cat $cache_dir/$cache_name.md5)" || ! -f $cache_dir/$gradle_files_checksum ]]; then - # Notify user of cache rebuild - echo -e " (Building completion cache. Please wait)\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\c" + # subproject tasks can be referenced implicitly from root project + if [[ $GRADLE_COMPLETION_UNQUALIFIED_TASKS == "true" ]]; then + local -a implicit_tasks=() + implicit_tasks=( $(comm -23 <(printf "%s\n" "${subproject_tasks[@]}" | sort) <(printf "%s\n" "${root_tasks[@]}" | sort)) ) + for task in $(printf "%s\n" "${implicit_tasks[@]}"); do + gradle_all_tasks+=( $task ) + done + fi - # Run gradle to retrieve possible tasks and cache. - # Reuse Gradle Daemon if IDLE but don't start a new one. - local gradle_tasks_output - if [[ ! -z "$($gradle_cmd --status 2>/dev/null | grep IDLE)" ]]; then - gradle_tasks_output="$($gradle_cmd --daemon -q tasks --all)" - else - gradle_tasks_output="$($gradle_cmd --no-daemon -q tasks --all)" - fi - local output_line - local task_description - local -a gradle_all_tasks=() - local -a root_tasks=() - local -a subproject_tasks=() - for output_line in $gradle_tasks_output; do - if [[ $output_line =~ ^([[:lower:]][[:alnum:][:punct:]]*)([[:space:]]-[[:space:]]([[:print:]]*))? ]]; then - task_name="${BASH_REMATCH[1]}" - task_description="${BASH_REMATCH[3]}" - gradle_all_tasks+=( "$task_name - $task_description" ) - # Completion for subproject tasks with ':' prefix - if [[ $task_name =~ ^([[:alnum:]:]+):([[:alnum:]]+) ]]; then - gradle_all_tasks+=( ":$task_name - $task_description" ) - subproject_tasks+=( "${BASH_REMATCH[2]}" ) - else - root_tasks+=( "$task_name" ) - fi - fi - done - - # subproject tasks can be referenced implicitly from root project - if [[ $GRADLE_COMPLETION_UNQUALIFIED_TASKS == "true" ]]; then - local -a implicit_tasks=() - implicit_tasks=( $(comm -23 <(printf "%s\n" "${subproject_tasks[@]}" | sort) <(printf "%s\n" "${root_tasks[@]}" | sort)) ) - for task in $(printf "%s\n" "${implicit_tasks[@]}"); do - gradle_all_tasks+=( $task ) - done - fi + printf "%s\n" "${gradle_all_tasks[@]}" > $cache_dir/$gradle_files_checksum + echo $gradle_files_checksum > $cache_dir/$cache_name.md5 +} - printf "%s\n" "${gradle_all_tasks[@]}" > $cache_dir/$gradle_files_checksum - echo $gradle_files_checksum > $cache_dir/$cache_name.md5 +__gradle-completion-init() { + local cache_dir cache_name gradle_build_file gradle_files_checksum project_root_dir - # Remove "please wait" message by writing a bunch of spaces then moving back to the left - echo -e " \b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\c" - fi - else - return 1 - fi + local OLDIFS="$IFS" + local IFS=$'\n' + + __gradle-init-cache-dir + __gradle-set-project-root-dir + __gradle-set-build-file + if [[ -f $gradle_build_file ]]; then + __gradle-set-cache-name + __gradle-generate-script-cache + __gradle-set-files-checksum + __gradle-notify-tasks-cache-build + fi + + IFS="$OLDIFS" - if [[ -f $cache_dir/$gradle_files_checksum ]]; then - # Optimize here - this is the slowest part of completion - local -a cached_tasks - if [[ -z $cur ]]; then - cached_tasks=( $(cat $cache_dir/$gradle_files_checksum) ) + return 0 +} + +_gradle() { + local cache_dir cache_name gradle_build_file gradle_files_checksum project_root_dir + local cur=${COMP_WORDS[COMP_CWORD]} + # Set bash internal field separator to '\n' + # This allows us to provide descriptions for options and tasks + local OLDIFS="$IFS" + local IFS=$'\n' + + if [[ ${cur} == --* ]]; then + __gradle-long-options + elif [[ ${cur} == -D* ]]; then + __gradle-properties + elif [[ ${cur} == -* ]]; then + __gradle-short-options + else + __gradle-init-cache-dir + __gradle-set-project-root-dir + __gradle-set-build-file + if [[ -f $gradle_build_file ]]; then + __gradle-set-cache-name + __gradle-generate-script-cache + __gradle-set-files-checksum + + # The cache key is md5 sum of all gradle scripts, so it's valid if it exists. + if [[ -f $cache_dir/$cache_name.md5 ]]; then + local cached_checksum="$(cat $cache_dir/$cache_name.md5)" + local -a cached_tasks + if [[ -z $cur ]]; then + cached_tasks=( $(cat $cache_dir/$cached_checksum) ) + else + cached_tasks=( $(grep "^$cur" $cache_dir/$cached_checksum) ) + fi + COMPREPLY=( $(compgen -W "${cached_tasks[*]}" -- "$cur") ) else - cached_tasks=( $(grep "^$cur" $cache_dir/$gradle_files_checksum) ) + __gradle-notify-tasks-cache-build + fi + + # Regenerate tasks cache in the background + if [[ $gradle_files_checksum != "$(cat $cache_dir/$cache_name.md5)" || ! -f $cache_dir/$gradle_files_checksum ]]; then + $(__gradle-generate-tasks-cache 1>&2 2>/dev/null &) fi - COMPREPLY=( $(compgen -W "${cached_tasks[*]}" -- "$cur") ) else - echo "D'oh! There's a bug in the completion script, please submit an issue to eriwen/gradle-completion" - # previous steps failed, maybe cache dir is not readable/writable - return 1 + # Default tasks available outside Gradle projects + local args="buildEnvironment - Displays all buildscript dependencies declared in root project. +components - Displays the components produced by root project. +dependencies - Displays all dependencies declared in root project. +dependencyInsight - Displays the insight into a specific dependency in root project. +dependentComponents - Displays the dependent components of components in root project. +help - Displays a help message. +init - Initializes a new Gradle build. +model - Displays the configuration model of root project. +projects - Displays the sub-projects of root project. +properties - Displays the properties of root project. +tasks - Displays the tasks runnable from root project. +wrapper - Generates Gradle wrapper files." + COMPREPLY=( $(compgen -W "$args" -- "${COMP_WORDS[COMP_CWORD]}") ) fi fi @@ -201,9 +293,12 @@ _gradle() return 0 } complete -F _gradle gradle +complete -F _gradle gradle.bat complete -F _gradle gradlew +complete -F _gradle gradlew.bat complete -F _gradle ./gradlew +complete -F _gradle ./gradlew.bat -if hash gw 2>/dev/null; then +if hash gw 2>/dev/null || alias gw >/dev/null 2>&1; then complete -F _gradle gw -fi \ No newline at end of file +fi