KeysDownloader { public String download() throws IOException { synchronized (instanceStateLock) { if (hasNonExpiredDataCached()) { if (shouldProactivelyRefreshDataInBackground()) { refreshInBackground(); } return cachedData; } } synchronized (fetchDataLock) { synchronized (instanceStateLock) { if (hasNonExpiredDataCached()) { return cachedData; } } return fetchAndCacheData(); } } KeysDownloader(Executor backgroundExecutor, HttpTransport httpTransport, String url); String download(); HttpTransport getHttpTransport(); String getUrl(); void refreshInBackground();  }
@Test public void shouldFetchKeys() throws Exception { httpResponseBuilder = new HttpResponseBuilder().setContent("keys"); assertEquals("keys", newInstanceForTests().download()); }
@Test public void shouldThrowOnSuccessHttpResponsesThatAreNotOk() throws Exception { httpResponseBuilder = new HttpResponseBuilder().setStatusCode(HttpStatusCodes.STATUS_CODE_NO_CONTENT); KeysDownloader instance = newInstanceForTests(); try { instance.download(); fail(); } catch (IOException expected) { assertEquals( "Unexpected status code = " + HttpStatusCodes.STATUS_CODE_NO_CONTENT, expected.getMessage()); } }
@Test public void shouldThrowOnNonSuccessHttpResponses() throws Exception { httpResponseBuilder = new HttpResponseBuilder().setStatusCode(HttpStatusCodes.STATUS_CODE_NO_CONTENT); KeysDownloader instance = newInstanceForTests(); try { instance.download(); fail(); } catch (IOException expected) { assertTrue( "Message " + expected.getMessage() + " should contain " + HttpStatusCodes.STATUS_CODE_NO_CONTENT, expected.getMessage().contains(Integer.toString(HttpStatusCodes.STATUS_CODE_NO_CONTENT))); } }
@Test public void shouldCacheKeysOnFetches() throws Exception { KeysDownloader instance = newInstanceForTests(); httpResponseBuilder = new HttpResponseBuilder().setContent("keys1"); assertEquals("keys1", instance.download()); httpResponseBuilder = new HttpResponseBuilder().setContent("keys2"); assertEquals("keys1", instance.download()); }
@Test public void shouldFetchKeysAgainIfNoCacheControlHeadersAreSent() throws Exception { KeysDownloader instance = newInstanceForTests(); httpResponseBuilder = new HttpResponseBuilder().setContent("keys1").clearCacheControl(); assertEquals("keys1", instance.download()); httpResponseBuilder = new HttpResponseBuilder().setContent("keys2"); assertEquals("keys2", instance.download()); }
@Test public void shouldFetchKeysAgainAfterExpiration() throws Exception { KeysDownloader instance = newInstanceForTests(); httpResponseBuilder = new HttpResponseBuilder().setContent("keys1").setCacheControlWithMaxAgeInSeconds(3L); assertEquals("keys1", instance.download()); httpResponseBuilder = new HttpResponseBuilder().setContent("keys2"); currentTimeInMillis += 3000L; assertEquals("keys2", instance.download()); }
@Test public void shouldReturnCachedKeysBeforeExpiration() throws Exception { KeysDownloader instance = newInstanceForTests(); httpResponseBuilder = new HttpResponseBuilder().setContent("keys1").setCacheControlWithMaxAgeInSeconds(3L); assertEquals("keys1", instance.download()); httpResponseBuilder = new HttpResponseBuilder().setContent("keys2"); currentTimeInMillis += 3000L - 1; assertEquals("keys1", instance.download()); }
@Test public void shouldFetchKeysAgainAfterExpirationAccountingForAgeHeader() throws Exception { KeysDownloader instance = newInstanceForTests(); httpResponseBuilder = new HttpResponseBuilder() .setContent("keys1") .setCacheControlWithMaxAgeInSeconds(3L) .setAgeInSeconds(1L); assertEquals("keys1", instance.download()); httpResponseBuilder = new HttpResponseBuilder().setContent("keys2"); currentTimeInMillis += 2000L; assertEquals("keys2", instance.download()); }
@Test public void shouldReturnCachedKeysBeforeExpirationAccountingForAgeHeader() throws Exception { KeysDownloader instance = newInstanceForTests(); httpResponseBuilder = new HttpResponseBuilder() .setContent("keys1") .setCacheControlWithMaxAgeInSeconds(3L) .setAgeInSeconds(1L); assertEquals("keys1", instance.download()); httpResponseBuilder = new HttpResponseBuilder().setContent("keys2"); currentTimeInMillis += 2000L - 1; assertEquals("keys1", instance.download()); }
@Test public void shouldTriggerBackgroundRefreshHalfWayThroughExpiration() throws Exception { KeysDownloader instance = newInstanceForTests(); httpResponseBuilder = new HttpResponseBuilder().setContent("keys1").setCacheControlWithMaxAgeInSeconds(3L); assertEquals("keys1", instance.download()); httpResponseBuilder = new HttpResponseBuilder().setContent("keys2"); currentTimeInMillis += 1500L; assertEquals("keys1", instance.download()); waitForLatch(backgroundFetchFinishedLatch); currentTimeInMillis += 10; httpResponseBuilder = new HttpResponseBuilder().setContent("keys3"); assertEquals("keys2", instance.download()); assertEquals(1, backgroundFetchStartedCount.get()); }
@Test public void shouldNotTriggerBackgroundRefreshBeforeHalfWayThroughExpiration() throws Exception { KeysDownloader instance = newInstanceForTests(); httpResponseBuilder = new HttpResponseBuilder().setContent("keys1").setCacheControlWithMaxAgeInSeconds(3L); assertEquals("keys1", instance.download()); httpResponseBuilder = new HttpResponseBuilder().setContent("keys2"); currentTimeInMillis += 1500L - 1; assertEquals("keys1", instance.download()); assertEquals(0, backgroundFetchStartedCount.get()); }
@Test public void shouldFetchOnlyOnceWhenMultipleThreadsTryToGetKeys() throws Exception { final KeysDownloader instance = newInstanceForTests(); httpResponseBuilder = new HttpResponseBuilder().setContent("keys"); List<FutureTask<String>> tasks = new ArrayList<>(); for (int i = 0; i < 10; i++) { tasks.add( new FutureTask<String>( new Callable<String>() { @Override public String call() throws Exception { return instance.download(); } })); } delayHttpResponseLatch = new CountDownLatch(1); for (FutureTask<String> task : tasks) { executor.execute(task); } delayHttpResponseLatch.countDown(); for (FutureTask<String> task : tasks) { assertEquals("keys", task.get(5, TimeUnit.SECONDS)); } assertEquals(1, httpTransportGetCount.get()); }
@Test public void shouldFetchOnlyOnceInBackgroundHalfWayThroughExpirationWhenMultipleThreadsTryToGetKeys() throws Exception { final KeysDownloader instance = newInstanceForTests(); httpResponseBuilder = new HttpResponseBuilder().setContent("keys1").setCacheControlWithMaxAgeInSeconds(3L); assertEquals("keys1", instance.download()); httpResponseBuilder = new HttpResponseBuilder().setContent("keys2"); currentTimeInMillis += 1500L; List<FutureTask<String>> tasks = new ArrayList<>(); for (int i = 0; i < 10; i++) { tasks.add( new FutureTask<String>( new Callable<String>() { @Override public String call() throws Exception { return instance.download(); } })); } httpTransportGetCount.set(0); backgroundFetchStartedCount.set(0); delayHttpResponseLatch = new CountDownLatch(1); for (FutureTask<String> task : tasks) { executor.execute(task); } for (FutureTask<String> task : tasks) { assertEquals("keys1", task.get(5, TimeUnit.SECONDS)); } delayHttpResponseLatch.countDown(); waitForLatch(backgroundFetchFinishedLatch); assertEquals(1, backgroundFetchStartedCount.get()); assertEquals(1, httpTransportGetCount.get()); }