From e52f4efe375dcd8157da935908538d858f41f97f Mon Sep 17 00:00:00 2001 From: Gavin Whelan Date: Mon, 29 Jul 2019 22:30:27 +0000 Subject: [PATCH] Release 2.8.5 (#82) ## [2.8.5] - 2019-07-29 ### Added: - Added a CircleCI badge to the project readme. ### Fixed - Fix a bug introduced in 2.8.0 that could cause the SDK to enter a bad state where it would no longer connect to the flag stream if `identify()` was called rapidly. - Reverted an unintentional behavior change introduced in 2.8.0 when `LDClient.init` is given zero as the timeout argument. Before 2.8.0, this would not wait for initialization and return the client immediately. For 2.8.0-2.8.4 this was changed to wait indefinitely for initialization, 2.8.5 restores the earlier behavior. --- .circleci/config.yml | 1 + CHANGELOG.md | 7 + README.md | 2 + example/build.gradle | 2 +- launchdarkly-android-client-sdk/build.gradle | 2 +- .../android/LDAwaitFutureTest.java | 122 ++++++++++++++++++ .../launchdarkly/android/LDAwaitFuture.java | 18 ++- .../android/StreamUpdateProcessor.java | 22 ++-- 8 files changed, 155 insertions(+), 21 deletions(-) create mode 100644 launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/android/LDAwaitFutureTest.java diff --git a/.circleci/config.yml b/.circleci/config.yml index 1600bc4a..8a78b8d9 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -27,6 +27,7 @@ jobs: name: Download Dependencies command: ./gradlew androidDependencies - run: sudo mkdir -p $CIRCLE_TEST_REPORTS + - run: sudo apt-get update - run: sudo apt-get -y -qq install awscli - run: sudo mkdir -p /usr/local/android-sdk-linux/licenses - run: sdkmanager "system-images;android-24;default;armeabi-v7a" diff --git a/CHANGELOG.md b/CHANGELOG.md index ba97a557..8ad425ea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,13 @@ All notable changes to the LaunchDarkly Android SDK will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org). +## [2.8.5] - 2019-07-29 +### Added: +- Added a CircleCI badge to the project readme. +### Fixed +- Fix a bug introduced in 2.8.0 that could cause the SDK to enter a bad state where it would no longer connect to the flag stream if `identify()` was called rapidly. +- Reverted an unintentional behavior change introduced in 2.8.0 when `LDClient.init` is given zero as the timeout argument. Before 2.8.0, this would not wait for initialization and return the client immediately. For 2.8.0-2.8.4 this was changed to wait indefinitely for initialization, 2.8.5 restores the earlier behavior. + ## [2.8.4] - 2019-06-14 ### Fixed - Deadlock when waiting on main thread for `identify` call. diff --git a/README.md b/README.md index f8e1a727..3e623ce3 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # LaunchDarkly SDK for Android +[![CircleCI](https://circleci.com/gh/launchdarkly/android-client-sdk.svg?style=svg)](https://circleci.com/gh/launchdarkly/android-client-sdk) + ## LaunchDarkly overview [LaunchDarkly](https://www.launchdarkly.com) is a feature management platform that serves over 100 billion feature flags daily to help teams build better software, faster. [Get started](https://docs.launchdarkly.com/docs/getting-started) using LaunchDarkly today! diff --git a/example/build.gradle b/example/build.gradle index c5f74e41..f282dfa8 100644 --- a/example/build.gradle +++ b/example/build.gradle @@ -46,7 +46,7 @@ dependencies { implementation 'com.android.support:appcompat-v7:26.1.0' implementation project(path: ':launchdarkly-android-client-sdk') // Comment the previous line and uncomment this one to depend on the published artifact: - //implementation 'com.launchdarkly:launchdarkly-android-client-sdk:2.8.4' + //implementation 'com.launchdarkly:launchdarkly-android-client-sdk:2.8.5' implementation 'com.jakewharton.timber:timber:4.7.1' diff --git a/launchdarkly-android-client-sdk/build.gradle b/launchdarkly-android-client-sdk/build.gradle index 34db5b0d..67099093 100644 --- a/launchdarkly-android-client-sdk/build.gradle +++ b/launchdarkly-android-client-sdk/build.gradle @@ -7,7 +7,7 @@ apply plugin: 'io.codearte.nexus-staging' allprojects { group = 'com.launchdarkly' - version = '2.8.4' + version = '2.8.5' sourceCompatibility = 1.7 targetCompatibility = 1.7 } diff --git a/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/android/LDAwaitFutureTest.java b/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/android/LDAwaitFutureTest.java new file mode 100644 index 00000000..219fac58 --- /dev/null +++ b/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/android/LDAwaitFutureTest.java @@ -0,0 +1,122 @@ +package com.launchdarkly.android; + +import android.support.test.runner.AndroidJUnit4; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertSame; + +@RunWith(AndroidJUnit4.class) +public class LDAwaitFutureTest { + + @Rule + public TimberLoggingRule timberLoggingRule = new TimberLoggingRule(); + + @Test + public void defaultCancelledValueIsFalse() { + LDAwaitFuture future = new LDAwaitFuture<>(); + assertFalse(future.isCancelled()); + } + + @Test + public void futureStartsIncomplete() { + LDAwaitFuture future = new LDAwaitFuture<>(); + assertFalse(future.isDone()); + } + + @Test(timeout = 500L) + public void futureThrowsTimeoutWhenNotSet() throws ExecutionException, InterruptedException { + LDAwaitFuture future = new LDAwaitFuture<>(); + try { + future.get(250, TimeUnit.MILLISECONDS); + } catch (TimeoutException ignored) { + } + } + + @Test(timeout = 500L) + public void futureThrowsTimeoutExceptionWithZeroTimeout() throws ExecutionException, + InterruptedException { + LDAwaitFuture future = new LDAwaitFuture<>(); + try { + future.get(0, TimeUnit.SECONDS); + } catch (TimeoutException ignored) { + } + } + + @Test(timeout = 500L) + public void futureDoesNotTimeoutOnSuccessfulFuture() throws InterruptedException, + ExecutionException, TimeoutException { + LDAwaitFuture future = new LDAwaitFuture<>(); + future.set(null); + future.get(0, TimeUnit.SECONDS); + } + + @Test(timeout = 500L) + public void futureThrowsExecutionExceptionOnFailedFuture() throws InterruptedException, + TimeoutException { + LDAwaitFuture future = new LDAwaitFuture<>(); + Throwable t = new Throwable(); + future.setException(t); + try { + future.get(0, TimeUnit.SECONDS); + } catch (ExecutionException ex) { + assertSame(t, ex.getCause()); + } + } + + @Test(timeout = 500L) + public void futureGetsSuccessfulFuture() throws InterruptedException, ExecutionException { + LDAwaitFuture future = new LDAwaitFuture<>(); + future.set(null); + future.get(); + } + + @Test(timeout = 500L) + public void futureWakesWaiterOnSuccess() throws Exception { + final LDAwaitFuture future = new LDAwaitFuture<>(); + new Callable() { + @Override + public Void call() throws Exception { + Thread.sleep(250); + future.set(null); + return null; + } + }.call(); + future.get(); + } + + @Test(timeout = 500L) + public void futureWakesWaiterOnFailure() throws Exception { + final Throwable t = new Throwable(); + final LDAwaitFuture future = new LDAwaitFuture<>(); + new Callable() { + @Override + public Void call() throws Exception { + Thread.sleep(250); + future.setException(t); + return null; + } + }.call(); + try { + future.get(); + } catch (ExecutionException ex) { + assertSame(t, ex.getCause()); + } + } + + @Test(timeout = 500L) + public void futureReturnsSetValue() throws ExecutionException, InterruptedException { + Object testObject = new Object(); + LDAwaitFuture future = new LDAwaitFuture<>(); + future.set(testObject); + assertSame(testObject, future.get()); + } +} diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/LDAwaitFuture.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/LDAwaitFuture.java index 6496bd27..d46ba478 100644 --- a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/LDAwaitFuture.java +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/LDAwaitFuture.java @@ -15,7 +15,8 @@ class LDAwaitFuture implements Future { private volatile boolean completed = false; private final Object notifier = new Object(); - LDAwaitFuture() {} + LDAwaitFuture() { + } synchronized void set(T result) { if (!completed) { @@ -29,7 +30,7 @@ synchronized void set(T result) { } } - synchronized void setException(Throwable error) { + synchronized void setException(@NonNull Throwable error) { if (!completed) { this.error = error; synchronized (notifier) { @@ -59,7 +60,7 @@ public boolean isDone() { @Override public T get() throws ExecutionException, InterruptedException { synchronized (notifier) { - if (!completed) { + while (!completed) { notifier.wait(); } } @@ -70,11 +71,14 @@ public T get() throws ExecutionException, InterruptedException { } @Override - public T get(long timeout, @NonNull TimeUnit unit) throws ExecutionException, TimeoutException, InterruptedException { + public T get(long timeout, @NonNull TimeUnit unit) throws ExecutionException, + TimeoutException, InterruptedException { + long remaining = unit.toNanos(timeout); + long doneAt = remaining + System.nanoTime(); synchronized (notifier) { - if (!completed) { - long millis = TimeUnit.MILLISECONDS.convert(timeout, unit); - notifier.wait(millis); + while (!completed & remaining > 0) { + TimeUnit.NANOSECONDS.timedWait(notifier, remaining); + remaining = doneAt - System.nanoTime(); } } if (!completed) { diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/StreamUpdateProcessor.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/StreamUpdateProcessor.java index caebf2af..1bf531d5 100644 --- a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/StreamUpdateProcessor.java +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/StreamUpdateProcessor.java @@ -171,19 +171,17 @@ public Void call() { synchronized void stop(final Util.ResultCallback onCompleteListener) { Timber.d("Stopping."); - if (es != null) { - // We do this in a separate thread because closing the stream involves a network - // operation and we don't want to do a network operation on the main thread. - executor.execute(new Runnable() { - @Override - public void run() { - stopSync(); - if (onCompleteListener != null) { - onCompleteListener.onSuccess(null); - } + // We do this in a separate thread because closing the stream involves a network + // operation and we don't want to do a network operation on the main thread. + executor.execute(new Runnable() { + @Override + public void run() { + stopSync(); + if (onCompleteListener != null) { + onCompleteListener.onSuccess(null); } - }); - } + } + }); } private synchronized void stopSync() {