From b4ae05c52f19b1015d48448dadb9b06173e344d5 Mon Sep 17 00:00:00 2001 From: liran2000 Date: Sun, 30 Mar 2025 15:13:17 +0300 Subject: [PATCH 01/40] adding http connector Signed-off-by: liran2000 --- providers/flagd/README.md | 14 +- .../storage/connector/sync/HttpConnector.java | 183 ++++++ .../providers/flagd/util/ConcurrentUtils.java | 61 ++ .../connector/sync/HttpConnectorTest.java | 543 ++++++++++++++++++ 4 files changed, 795 insertions(+), 6 deletions(-) create mode 100644 providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/connector/sync/HttpConnector.java create mode 100644 providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/util/ConcurrentUtils.java create mode 100644 providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/connector/sync/HttpConnectorTest.java diff --git a/providers/flagd/README.md b/providers/flagd/README.md index de0d8a091..4fee01e84 100644 --- a/providers/flagd/README.md +++ b/providers/flagd/README.md @@ -74,15 +74,17 @@ This mode is useful for local development, tests and offline applications. #### Custom Connector You can include a custom connector as a configuration option to customize how the in-process resolver fetches flags. -The custom connector must implement the [Connector interface](https://github.com/open-feature/java-sdk-contrib/blob/main/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/connector/Connector.java). +The custom connector must implement the [QueueSource interface](https://github.com/open-feature/java-sdk-contrib/blob/main/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/connector/QueueSource.java). ```java -Connector myCustomConnector = new MyCustomConnector(); +QueueSource connector = HttpConnector.builder() + .url(testUrl) + .build(); FlagdOptions options = - FlagdOptions.builder() - .resolverType(Config.Resolver.IN_PROCESS) - .customConnector(myCustomConnector) - .build(); + FlagdOptions.builder() + .resolverType(Config.Resolver.IN_PROCESS) + .customConnector(myCustomConnector) + .build(); FlagdProvider flagdProvider = new FlagdProvider(options); ``` diff --git a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/connector/sync/HttpConnector.java b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/connector/sync/HttpConnector.java new file mode 100644 index 000000000..9d92c83cb --- /dev/null +++ b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/connector/sync/HttpConnector.java @@ -0,0 +1,183 @@ +package dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync; + +import dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.QueuePayload; +import dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.QueuePayloadType; +import dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.QueueSource; +import dev.openfeature.contrib.providers.flagd.util.ConcurrentUtils; +import lombok.Builder; +import lombok.NonNull; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.net.ProxySelector; +import java.net.URI; +import java.net.URL; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.time.Duration; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +import static java.net.http.HttpClient.Builder.NO_PROXY; + +/** + * HttpConnector is responsible for managing HTTP connections and polling data from a specified URL + * at regular intervals. It implements the QueueSource interface to enqueue and dequeue change messages. + * The class supports configurable parameters such as poll interval, request timeout, and proxy settings. + * It uses a ScheduledExecutorService to schedule polling tasks and an ExecutorService for HTTP client execution. + * The class also provides methods to initialize, retrieve the stream queue, and shutdown the connector gracefully. + */ +@Slf4j +public class HttpConnector implements QueueSource { + + private static final int DEFAULT_POLL_INTERVAL_SECONDS = 60; + private static final int DEFAULT_LINKED_BLOCKING_QUEUE_CAPACITY = 100; + private static final int DEFAULT_SCHEDULED_THREAD_POOL_SIZE = 1; + private static final int DEFAULT_REQUEST_TIMEOUT_SECONDS = 10; + private static final int DEFAULT_CONNECT_TIMEOUT_SECONDS = 10; + + private Integer pollIntervalSeconds; + private Integer requestTimeoutSeconds; + private BlockingQueue queue; + private HttpClient client; + private ExecutorService httpClientExecutor; + private ScheduledExecutorService scheduler; + private Map headers; + @NonNull + private String url; + + // TODO init failure backup cache redis + + // todo update provider readme + + @Builder + public HttpConnector(Integer pollIntervalSeconds, Integer linkedBlockingQueueCapacity, + Integer scheduledThreadPoolSize, Integer requestTimeoutSeconds, Integer connectTimeoutSeconds, String url, + Map headers, ExecutorService httpClientExecutor, String proxyHost, Integer proxyPort) { + validate(url, pollIntervalSeconds, linkedBlockingQueueCapacity, scheduledThreadPoolSize, requestTimeoutSeconds, + connectTimeoutSeconds, proxyHost, proxyPort); + this.pollIntervalSeconds = pollIntervalSeconds == null ? DEFAULT_POLL_INTERVAL_SECONDS : pollIntervalSeconds; + int thisLinkedBlockingQueueCapacity = linkedBlockingQueueCapacity == null ? DEFAULT_LINKED_BLOCKING_QUEUE_CAPACITY : linkedBlockingQueueCapacity; + int thisScheduledThreadPoolSize = scheduledThreadPoolSize == null ? DEFAULT_SCHEDULED_THREAD_POOL_SIZE : scheduledThreadPoolSize; + this.requestTimeoutSeconds = requestTimeoutSeconds == null ? DEFAULT_REQUEST_TIMEOUT_SECONDS : requestTimeoutSeconds; + int thisConnectTimeoutSeconds = connectTimeoutSeconds == null ? DEFAULT_CONNECT_TIMEOUT_SECONDS : connectTimeoutSeconds; + ProxySelector proxySelector = NO_PROXY; + if (proxyHost != null && proxyPort != null) { + proxySelector = ProxySelector.of(new InetSocketAddress(proxyHost, proxyPort)); + } + + this.url = url; + this.headers = headers; + this.httpClientExecutor = httpClientExecutor == null ? Executors.newFixedThreadPool(1) : + httpClientExecutor; + scheduler = Executors.newScheduledThreadPool(thisScheduledThreadPoolSize); + if (headers == null) { + this.headers = new HashMap<>(); + } + this.client = HttpClient.newBuilder() + .connectTimeout(Duration.ofSeconds(thisConnectTimeoutSeconds)) + .proxy(proxySelector) + .executor(this.httpClientExecutor) + .build(); + this.queue = new LinkedBlockingQueue<>(thisLinkedBlockingQueueCapacity); + } + + @SneakyThrows + private void validate(String url, Integer pollIntervalSeconds, Integer linkedBlockingQueueCapacity, + Integer scheduledThreadPoolSize, Integer requestTimeoutSeconds, Integer connectTimeoutSeconds, + String proxyHost, Integer proxyPort) { + new URL(url).toURI(); + if (pollIntervalSeconds != null && (pollIntervalSeconds < 1 || pollIntervalSeconds > 600)) { + throw new IllegalArgumentException("pollIntervalSeconds must be between 1 and 600"); + } + if (linkedBlockingQueueCapacity != null && (linkedBlockingQueueCapacity < 1 || linkedBlockingQueueCapacity > 1000)) { + throw new IllegalArgumentException("linkedBlockingQueueCapacity must be between 1 and 1000"); + } + if (scheduledThreadPoolSize != null && (scheduledThreadPoolSize < 1 || scheduledThreadPoolSize > 10)) { + throw new IllegalArgumentException("scheduledThreadPoolSize must be between 1 and 10"); + } + if (requestTimeoutSeconds != null && (requestTimeoutSeconds < 1 || requestTimeoutSeconds > 60)) { + throw new IllegalArgumentException("requestTimeoutSeconds must be between 1 and 60"); + } + if (connectTimeoutSeconds != null && (connectTimeoutSeconds < 1 || connectTimeoutSeconds > 60)) { + throw new IllegalArgumentException("connectTimeoutSeconds must be between 1 and 60"); + } + if (proxyPort != null && (proxyPort < 1 || proxyPort > 65535)) { + throw new IllegalArgumentException("proxyPort must be between 1 and 65535"); + } + if (proxyHost != null && proxyPort == null ) { + throw new IllegalArgumentException("proxyPort must be set if proxyHost is set"); + } else if (proxyHost == null && proxyPort != null) { + throw new IllegalArgumentException("proxyHost must be set if proxyPort is set"); + } + } + + @Override + public void init() throws Exception { + log.info("init Http Connector"); + } + + @Override + public BlockingQueue getStreamQueue() { + Runnable pollTask = buildPollTask(); + + // run first poll immediately and wait for it to finish + pollTask.run(); + + scheduler.scheduleAtFixedRate(pollTask, pollIntervalSeconds, pollIntervalSeconds, TimeUnit.SECONDS); + return queue; + } + + protected Runnable buildPollTask() { + return () -> { + HttpRequest.Builder requestBuilder = HttpRequest.newBuilder() + .uri(URI.create(url)) + .timeout(Duration.ofSeconds(requestTimeoutSeconds)) + .GET(); + headers.forEach(requestBuilder::header); + HttpRequest request = requestBuilder + .build(); + + HttpResponse response; + try { + log.debug("fetching response"); + response = execute(request); + } catch (IOException e) { + log.info("could not fetch", e); + return; + } catch (Exception e) { + log.debug("exception", e); + return; + } + log.debug("fetched response"); + if (response.statusCode() != 200) { + log.info("received non-successful status code: {} {}", response.statusCode(), response.body()); + return; + } + if (!this.queue.offer(new QueuePayload(QueuePayloadType.DATA, response.body()))) { + log.warn("Unable to offer file content to queue: queue is full"); + } + }; + } + + protected HttpResponse execute(HttpRequest request) throws IOException, InterruptedException { + HttpResponse response; + response = client.send(request, HttpResponse.BodyHandlers.ofString()); + return response; + } + + @Override + public void shutdown() throws InterruptedException { + ConcurrentUtils.shutdownAndAwaitTermination(scheduler, 10); + ConcurrentUtils.shutdownAndAwaitTermination(httpClientExecutor, 10); + } +} diff --git a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/util/ConcurrentUtils.java b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/util/ConcurrentUtils.java new file mode 100644 index 000000000..b57faca77 --- /dev/null +++ b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/util/ConcurrentUtils.java @@ -0,0 +1,61 @@ +package dev.openfeature.contrib.providers.flagd.util; + +import java.util.concurrent.ExecutorService; +import java.util.concurrent.TimeUnit; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +/** + * Concurrent / Concurrency utilities. + * + * @author Liran Mendelovich + */ +@NoArgsConstructor(access = AccessLevel.PRIVATE) +@Slf4j +public class ConcurrentUtils { + + /** + * Graceful shutdown a thread pool.
+ * See + * https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/ExecutorService.html + * + * @param pool thread pool + * @param timeoutSeconds grace period timeout in seconds - timeout can be twice than this value, + * as first it waits for existing tasks to terminate, then waits for cancelled tasks to + * terminate. + */ + public static void shutdownAndAwaitTermination(ExecutorService pool, int timeoutSeconds) { + if (pool == null) { + return; + } + + // Disable new tasks from being submitted + pool.shutdown(); + try { + + // Wait a while for existing tasks to terminate + if (!pool.awaitTermination(timeoutSeconds, TimeUnit.SECONDS)) { + + // Cancel currently executing tasks - best effort, based on interrupt handling + // implementation. + pool.shutdownNow(); + + // Wait a while for tasks to respond to being cancelled + if (!pool.awaitTermination(timeoutSeconds, TimeUnit.SECONDS)) { + log.error("Thread pool did not shutdown all tasks after the timeout: {} seconds.", timeoutSeconds); + } + } + } catch (InterruptedException e) { + + log.info("Current thread interrupted during shutdownAndAwaitTermination, calling shutdownNow."); + + // (Re-)Cancel if current thread also interrupted + pool.shutdownNow(); + + // Preserve interrupt status + Thread.currentThread().interrupt(); + } + } +} diff --git a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/connector/sync/HttpConnectorTest.java b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/connector/sync/HttpConnectorTest.java new file mode 100644 index 000000000..60ffd05ef --- /dev/null +++ b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/connector/sync/HttpConnectorTest.java @@ -0,0 +1,543 @@ +package dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.when; + +import dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.QueuePayload; +import dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.QueuePayloadType; +import java.io.IOException; +import java.lang.reflect.Field; +import java.net.MalformedURLException; +import java.net.ProxySelector; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +@Slf4j +class HttpConnectorTest { + + @SneakyThrows + @Test + void testConstructorInitializesDefaultValues() { + String testUrl = "http://example.com"; + HttpConnector connector = HttpConnector.builder() + .url(testUrl) + .build(); + + Field pollIntervalField = HttpConnector.class.getDeclaredField("pollIntervalSeconds"); + pollIntervalField.setAccessible(true); + assertEquals(60, pollIntervalField.get(connector)); + + Field requestTimeoutField = HttpConnector.class.getDeclaredField("requestTimeoutSeconds"); + requestTimeoutField.setAccessible(true); + assertEquals(10, requestTimeoutField.get(connector)); + + Field queueField = HttpConnector.class.getDeclaredField("queue"); + queueField.setAccessible(true); + BlockingQueue queue = (BlockingQueue) queueField.get(connector); + assertEquals(100, queue.remainingCapacity() + queue.size()); + + Field headersField = HttpConnector.class.getDeclaredField("headers"); + headersField.setAccessible(true); + Map headers = (Map) headersField.get(connector); + assertNotNull(headers); + assertTrue(headers.isEmpty()); + } + + @SneakyThrows + @Test + void testConstructorValidationRejectsInvalidParameters() { + String testUrl = "http://example.com"; + + HttpConnector.HttpConnectorBuilder builder3 = HttpConnector.builder() + .url(testUrl) + .pollIntervalSeconds(0); + IllegalArgumentException pollIntervalException = assertThrows( + IllegalArgumentException.class, + builder3::build + ); + assertEquals("pollIntervalSeconds must be between 1 and 600", pollIntervalException.getMessage()); + + HttpConnector.HttpConnectorBuilder builder2 = HttpConnector.builder() + .url(testUrl) + .linkedBlockingQueueCapacity(1001); + IllegalArgumentException queueCapacityException = assertThrows( + IllegalArgumentException.class, + builder2::build + ); + assertEquals("linkedBlockingQueueCapacity must be between 1 and 1000", queueCapacityException.getMessage()); + + HttpConnector.HttpConnectorBuilder builder1 = HttpConnector.builder() + .url(testUrl) + .scheduledThreadPoolSize(11); + IllegalArgumentException threadPoolException = assertThrows( + IllegalArgumentException.class, + builder1::build + ); + assertEquals("scheduledThreadPoolSize must be between 1 and 10", threadPoolException.getMessage()); + + HttpConnector.HttpConnectorBuilder builder = HttpConnector.builder() + .url(testUrl) + .proxyHost("localhost"); + IllegalArgumentException proxyException = assertThrows( + IllegalArgumentException.class, + builder::build + ); + assertEquals("proxyPort must be set if proxyHost is set", proxyException.getMessage()); + } + + @SneakyThrows + @Test + void testGetStreamQueueInitialAndScheduledPolls() { + String testUrl = "http://example.com"; + HttpClient mockClient = mock(HttpClient.class); + HttpResponse mockResponse = mock(HttpResponse.class); + when(mockResponse.statusCode()).thenReturn(200); + when(mockResponse.body()).thenReturn("test data"); + when(mockClient.send(any(HttpRequest.class), any(HttpResponse.BodyHandler.class))) + .thenReturn(mockResponse); + + HttpConnector connector = HttpConnector.builder() + .url(testUrl) + .httpClientExecutor(Executors.newSingleThreadExecutor()) + .build(); + + Field clientField = HttpConnector.class.getDeclaredField("client"); + clientField.setAccessible(true); + clientField.set(connector, mockClient); + + BlockingQueue queue = connector.getStreamQueue(); + + assertFalse(queue.isEmpty()); + QueuePayload payload = queue.poll(); + assertNotNull(payload); + assertEquals(QueuePayloadType.DATA, payload.getType()); + assertEquals("test data", payload.getFlagData()); + + connector.shutdown(); + } + + @SneakyThrows + @Test + void testBuildPollTaskFetchesDataAndAddsToQueue() { + String testUrl = "http://example.com"; + HttpClient mockClient = mock(HttpClient.class); + HttpResponse mockResponse = mock(HttpResponse.class); + when(mockResponse.statusCode()).thenReturn(200); + when(mockResponse.body()).thenReturn("test data"); + when(mockClient.send(any(HttpRequest.class), any(HttpResponse.BodyHandler.class))) + .thenReturn(mockResponse); + + HttpConnector connector = HttpConnector.builder() + .url(testUrl) + .httpClientExecutor(Executors.newFixedThreadPool(1)) + .build(); + + Field clientField = HttpConnector.class.getDeclaredField("client"); + clientField.setAccessible(true); + clientField.set(connector, mockClient); + + Runnable pollTask = connector.buildPollTask(); + pollTask.run(); + + Field queueField = HttpConnector.class.getDeclaredField("queue"); + queueField.setAccessible(true); + BlockingQueue queue = (BlockingQueue) queueField.get(connector); + assertFalse(queue.isEmpty()); + QueuePayload payload = queue.poll(); + assertNotNull(payload); + assertEquals(QueuePayloadType.DATA, payload.getType()); + assertEquals("test data", payload.getFlagData()); + } + + @SneakyThrows + @Test + void testHttpRequestIncludesHeaders() { + String testUrl = "http://example.com"; + Map testHeaders = new HashMap<>(); + testHeaders.put("Authorization", "Bearer token"); + testHeaders.put("Content-Type", "application/json"); + + HttpConnector connector = HttpConnector.builder() + .url(testUrl) + .headers(testHeaders) + .build(); + + Field headersField = HttpConnector.class.getDeclaredField("headers"); + headersField.setAccessible(true); + Map headers = (Map) headersField.get(connector); + assertNotNull(headers); + assertEquals(2, headers.size()); + assertEquals("Bearer token", headers.get("Authorization")); + assertEquals("application/json", headers.get("Content-Type")); + } + + @SneakyThrows + @Test + void testConstructorInitializesWithProvidedValues() { + Integer pollIntervalSeconds = 120; + Integer linkedBlockingQueueCapacity = 200; + Integer scheduledThreadPoolSize = 2; + Integer requestTimeoutSeconds = 20; + Integer connectTimeoutSeconds = 15; + String url = "http://example.com"; + Map headers = new HashMap<>(); + headers.put("Authorization", "Bearer token"); + ExecutorService httpClientExecutor = Executors.newFixedThreadPool(2); + String proxyHost = "proxy.example.com"; + Integer proxyPort = 8080; + + HttpConnector connector = HttpConnector.builder() + .pollIntervalSeconds(pollIntervalSeconds) + .linkedBlockingQueueCapacity(linkedBlockingQueueCapacity) + .scheduledThreadPoolSize(scheduledThreadPoolSize) + .requestTimeoutSeconds(requestTimeoutSeconds) + .connectTimeoutSeconds(connectTimeoutSeconds) + .url(url) + .headers(headers) + .httpClientExecutor(httpClientExecutor) + .proxyHost(proxyHost) + .proxyPort(proxyPort) + .build(); + + Field pollIntervalField = HttpConnector.class.getDeclaredField("pollIntervalSeconds"); + pollIntervalField.setAccessible(true); + assertEquals(pollIntervalSeconds, pollIntervalField.get(connector)); + + Field requestTimeoutField = HttpConnector.class.getDeclaredField("requestTimeoutSeconds"); + requestTimeoutField.setAccessible(true); + assertEquals(requestTimeoutSeconds, requestTimeoutField.get(connector)); + + Field queueField = HttpConnector.class.getDeclaredField("queue"); + queueField.setAccessible(true); + BlockingQueue queue = (BlockingQueue) queueField.get(connector); + assertEquals(linkedBlockingQueueCapacity, queue.remainingCapacity() + queue.size()); + + Field headersField = HttpConnector.class.getDeclaredField("headers"); + headersField.setAccessible(true); + Map actualHeaders = (Map) headersField.get(connector); + assertEquals(headers, actualHeaders); + + Field urlField = HttpConnector.class.getDeclaredField("url"); + urlField.setAccessible(true); + assertEquals(url, urlField.get(connector)); + } + + @SneakyThrows + @Test + void testSuccessfulHttpResponseAddsDataToQueue() { + String testUrl = "http://example.com"; + HttpClient mockClient = mock(HttpClient.class); + HttpResponse mockResponse = mock(HttpResponse.class); + when(mockResponse.statusCode()).thenReturn(200); + when(mockResponse.body()).thenReturn("test data"); + when(mockClient.send(any(HttpRequest.class), any(HttpResponse.BodyHandler.class))) + .thenReturn(mockResponse); + + HttpConnector connector = HttpConnector.builder() + .url(testUrl) + .httpClientExecutor(Executors.newSingleThreadExecutor()) + .build(); + + Field clientField = HttpConnector.class.getDeclaredField("client"); + clientField.setAccessible(true); + clientField.set(connector, mockClient); + + BlockingQueue queue = connector.getStreamQueue(); + + assertFalse(queue.isEmpty()); + QueuePayload payload = queue.poll(); + assertNotNull(payload); + assertEquals(QueuePayloadType.DATA, payload.getType()); + assertEquals("test data", payload.getFlagData()); + } + + @SneakyThrows + @Test + void testQueueBecomesFull() { + String testUrl = "http://example.com"; + int queueCapacity = 1; + HttpConnector connector = HttpConnector.builder() + .url(testUrl) + .linkedBlockingQueueCapacity(queueCapacity) + .build(); + + BlockingQueue queue = connector.getStreamQueue(); + + queue.offer(new QueuePayload(QueuePayloadType.DATA, "test data 1")); + + boolean wasOffered = queue.offer(new QueuePayload(QueuePayloadType.DATA, "test data 2")); + + assertFalse(wasOffered, "Queue should be full and not accept more items"); + } + + @SneakyThrows + @Test + void testShutdownProperlyTerminatesSchedulerAndHttpClientExecutor() throws InterruptedException { + ExecutorService mockHttpClientExecutor = mock(ExecutorService.class); + ScheduledExecutorService mockScheduler = mock(ScheduledExecutorService.class); + String testUrl = "http://example.com"; + + HttpConnector connector = HttpConnector.builder() + .url(testUrl) + .httpClientExecutor(mockHttpClientExecutor) + .build(); + + Field schedulerField = HttpConnector.class.getDeclaredField("scheduler"); + schedulerField.setAccessible(true); + schedulerField.set(connector, mockScheduler); + + connector.shutdown(); + + Mockito.verify(mockScheduler).shutdown(); + Mockito.verify(mockHttpClientExecutor).shutdown(); + } + + @SneakyThrows + @Test + void testHttpResponseNonSuccessStatusCode() { + String testUrl = "http://example.com"; + HttpClient mockClient = mock(HttpClient.class); + HttpResponse mockResponse = mock(HttpResponse.class); + when(mockResponse.statusCode()).thenReturn(404); + when(mockClient.send(any(HttpRequest.class), any(HttpResponse.BodyHandler.class))) + .thenReturn(mockResponse); + + HttpConnector connector = HttpConnector.builder() + .url(testUrl) + .httpClientExecutor(Executors.newSingleThreadExecutor()) + .build(); + + Field clientField = HttpConnector.class.getDeclaredField("client"); + clientField.setAccessible(true); + clientField.set(connector, mockClient); + + BlockingQueue queue = connector.getStreamQueue(); + + assertTrue(queue.isEmpty(), "Queue should be empty when response status is non-200"); + } + + @SneakyThrows + @Test + void test_constructor_handles_proxy_configuration() { + String testUrl = "http://example.com"; + String proxyHost = "proxy.example.com"; + int proxyPort = 8080; + + HttpConnector connectorWithProxy = HttpConnector.builder() + .url(testUrl) + .proxyHost(proxyHost) + .proxyPort(proxyPort) + .build(); + + HttpConnector connectorWithoutProxy = HttpConnector.builder() + .url(testUrl) + .build(); + + Field clientFieldWithProxy = HttpConnector.class.getDeclaredField("client"); + clientFieldWithProxy.setAccessible(true); + HttpClient clientWithProxy = (HttpClient) clientFieldWithProxy.get(connectorWithProxy); + assertNotNull(clientWithProxy); + + Field clientFieldWithoutProxy = HttpConnector.class.getDeclaredField("client"); + clientFieldWithoutProxy.setAccessible(true); + HttpClient clientWithoutProxy = (HttpClient) clientFieldWithoutProxy.get(connectorWithoutProxy); + assertNotNull(clientWithoutProxy); + + Optional proxySelectorWithProxy = clientWithProxy.proxy(); + assertNotNull(proxySelectorWithProxy.get()); + } + + @SneakyThrows + @Test + void testHttpRequestFailsWithException() { + String testUrl = "http://example.com"; + HttpClient mockClient = mock(HttpClient.class); + HttpConnector connector = HttpConnector.builder() + .url(testUrl) + .httpClientExecutor(Executors.newSingleThreadExecutor()) + .build(); + + Field clientField = HttpConnector.class.getDeclaredField("client"); + clientField.setAccessible(true); + clientField.set(connector, mockClient); + + when(mockClient.send(any(HttpRequest.class), any(HttpResponse.BodyHandler.class))) + .thenThrow(new RuntimeException("Test exception")); + + BlockingQueue queue = connector.getStreamQueue(); + + assertTrue(queue.isEmpty(), "Queue should be empty when request fails with exception"); + } + + @SneakyThrows + @Test + void testHttpRequestFailsWithIoexception() { + String testUrl = "http://example.com"; + HttpClient mockClient = mock(HttpClient.class); + HttpConnector connector = HttpConnector.builder() + .url(testUrl) + .httpClientExecutor(Executors.newSingleThreadExecutor()) + .build(); + + Field clientField = HttpConnector.class.getDeclaredField("client"); + clientField.setAccessible(true); + clientField.set(connector, mockClient); + + when(mockClient.send(any(HttpRequest.class), any(HttpResponse.BodyHandler.class))) + .thenThrow(new IOException("Simulated IO Exception")); + + connector.getStreamQueue(); + + Field queueField = HttpConnector.class.getDeclaredField("queue"); + queueField.setAccessible(true); + BlockingQueue queue = (BlockingQueue) queueField.get(connector); + assertTrue(queue.isEmpty(), "Queue should be empty due to IOException"); + } + + @SneakyThrows + @Test + void testMalformedUrlThrowsException() { + String malformedUrl = "htp://invalid-url"; + + assertThrows(MalformedURLException.class, () -> { + HttpConnector.builder() + .url(malformedUrl) + .build(); + }); + } + + @SneakyThrows + @Test + void testHeadersInitializationWhenNull() { + String testUrl = "http://example.com"; + + HttpConnector connector = HttpConnector.builder() + .url(testUrl) + .headers(null) + .build(); + + Field headersField = HttpConnector.class.getDeclaredField("headers"); + headersField.setAccessible(true); + Map headers = (Map) headersField.get(connector); + assertNotNull(headers); + assertTrue(headers.isEmpty()); + } + + @SneakyThrows + @Test + void testScheduledPollingContinuesAtFixedIntervals() { + String testUrl = "http://exampOle.com"; + HttpResponse mockResponse = mock(HttpResponse.class); + when(mockResponse.statusCode()).thenReturn(200); + when(mockResponse.body()).thenReturn("test data"); + + HttpConnector connector = spy(HttpConnector.builder() + .url(testUrl) + .httpClientExecutor(Executors.newSingleThreadExecutor()) + .build()); + + doReturn(mockResponse).when(connector).execute(any()); + + BlockingQueue queue = connector.getStreamQueue(); + + delay(2000); + assertFalse(queue.isEmpty()); + QueuePayload payload = queue.poll(); + assertNotNull(payload); + assertEquals(QueuePayloadType.DATA, payload.getType()); + assertEquals("test data", payload.getFlagData()); + + connector.shutdown(); + } + + @SneakyThrows + @Test + void testDefaultValuesWhenOptionalParametersAreNull() { + String testUrl = "http://example.com"; + + HttpConnector connector = HttpConnector.builder() + .url(testUrl) + .build(); + + Field pollIntervalField = HttpConnector.class.getDeclaredField("pollIntervalSeconds"); + pollIntervalField.setAccessible(true); + assertEquals(60, pollIntervalField.get(connector)); + + Field requestTimeoutField = HttpConnector.class.getDeclaredField("requestTimeoutSeconds"); + requestTimeoutField.setAccessible(true); + assertEquals(10, requestTimeoutField.get(connector)); + + Field queueField = HttpConnector.class.getDeclaredField("queue"); + queueField.setAccessible(true); + BlockingQueue queue = (BlockingQueue) queueField.get(connector); + assertEquals(100, queue.remainingCapacity() + queue.size()); + + Field headersField = HttpConnector.class.getDeclaredField("headers"); + headersField.setAccessible(true); + Map headers = (Map) headersField.get(connector); + assertNotNull(headers); + assertTrue(headers.isEmpty()); + + Field httpClientExecutorField = HttpConnector.class.getDeclaredField("httpClientExecutor"); + httpClientExecutorField.setAccessible(true); + ExecutorService httpClientExecutor = (ExecutorService) httpClientExecutorField.get(connector); + assertNotNull(httpClientExecutor); + } + + @SneakyThrows + @Test + void testQueuePayloadTypeSetToDataOnSuccess() { + String testUrl = "http://example.com"; + HttpClient mockClient = mock(HttpClient.class); + HttpResponse mockResponse = mock(HttpResponse.class); + ExecutorService mockExecutor = Executors.newFixedThreadPool(1); + + when(mockResponse.statusCode()).thenReturn(200); + when(mockResponse.body()).thenReturn("response body"); + when(mockClient.send(any(HttpRequest.class), any(HttpResponse.BodyHandler.class))) + .thenReturn(mockResponse); + + HttpConnector connector = HttpConnector.builder() + .url(testUrl) + .httpClientExecutor(mockExecutor) + .build(); + + Field clientField = HttpConnector.class.getDeclaredField("client"); + clientField.setAccessible(true); + clientField.set(connector, mockClient); + + BlockingQueue queue = connector.getStreamQueue(); + + QueuePayload payload = queue.poll(1, TimeUnit.SECONDS); + assertNotNull(payload); + assertEquals(QueuePayloadType.DATA, payload.getType()); + assertEquals("response body", payload.getFlagData()); + } + + @SneakyThrows + private static void delay(long ms) { + Thread.sleep(ms); + } + +} From f93014ef89354d014db48e53dcdeaee833cc997a Mon Sep 17 00:00:00 2001 From: liran2000 Date: Sun, 30 Mar 2025 18:37:37 +0300 Subject: [PATCH 02/40] adding http connector - cont. Signed-off-by: liran2000 --- providers/flagd/README.md | 33 +++++ .../storage/connector/sync/HttpConnector.java | 138 ++++++++++++------ .../storage/connector/sync/PayloadCache.java | 6 + .../connector/sync/PayloadCacheOptions.java | 24 +++ .../connector/sync/PayloadCacheWrapper.java | 56 +++++++ .../connector/sync/HttpConnectorTest.java | 43 ++++++ 6 files changed, 254 insertions(+), 46 deletions(-) create mode 100644 providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/connector/sync/PayloadCache.java create mode 100644 providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/connector/sync/PayloadCacheOptions.java create mode 100644 providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/connector/sync/PayloadCacheWrapper.java diff --git a/providers/flagd/README.md b/providers/flagd/README.md index 4fee01e84..f0438f727 100644 --- a/providers/flagd/README.md +++ b/providers/flagd/README.md @@ -54,6 +54,39 @@ The value is updated with every (re)connection to the sync implementation. This can be used to enrich evaluations with such data. If the `in-process` mode is not used, and before the provider is ready, the `getSyncMetadata` returns an empty map. +#### Http Connector +HttpConnector is responsible for polling data from a specified URL at regular intervals. +The implementation is using Java HttpClient. + +##### What happens if the Http source is down when application is starting ? + +It supports optional fail-safe initialization via cache, such that on initial fetch error following by +source downtime window, initial payload is taken from cache to avoid starting with default values until +the source is back up. Therefore, the cache ttl expected to be higher than the expected source +down-time to recover from during initialization. + +##### Sample flow +Sample flow can use: +- Github as the flags payload source. +- Redis cache as a fail-safe initialization cache. + +Sample flow of initialization during Github down-time window, showing that application can still use flags +values as fetched from cache. +```mermaid +sequenceDiagram + participant Provider + participant Github + participant Redis + + break source downtime + Provider->>Github: initialize + Github->>Provider: failure + end + Provider->>Redis: fetch + Redis->>Provider: last payload + +``` + ### Offline mode (File resolver) In-process resolvers can also work in an offline mode. diff --git a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/connector/sync/HttpConnector.java b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/connector/sync/HttpConnector.java index 9d92c83cb..63311ede7 100644 --- a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/connector/sync/HttpConnector.java +++ b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/connector/sync/HttpConnector.java @@ -30,18 +30,22 @@ import static java.net.http.HttpClient.Builder.NO_PROXY; /** - * HttpConnector is responsible for managing HTTP connections and polling data from a specified URL - * at regular intervals. It implements the QueueSource interface to enqueue and dequeue change messages. + * HttpConnector is responsible for polling data from a specified URL at regular intervals. + * Notice rate limits for polling http sources like Github. + * It implements the QueueSource interface to enqueue and dequeue change messages. * The class supports configurable parameters such as poll interval, request timeout, and proxy settings. * It uses a ScheduledExecutorService to schedule polling tasks and an ExecutorService for HTTP client execution. * The class also provides methods to initialize, retrieve the stream queue, and shutdown the connector gracefully. + * It supports optional fail-safe initialization via cache. + * + * See readme - Http Connector section. */ @Slf4j public class HttpConnector implements QueueSource { private static final int DEFAULT_POLL_INTERVAL_SECONDS = 60; private static final int DEFAULT_LINKED_BLOCKING_QUEUE_CAPACITY = 100; - private static final int DEFAULT_SCHEDULED_THREAD_POOL_SIZE = 1; + private static final int DEFAULT_SCHEDULED_THREAD_POOL_SIZE = 2; private static final int DEFAULT_REQUEST_TIMEOUT_SECONDS = 10; private static final int DEFAULT_CONNECT_TIMEOUT_SECONDS = 10; @@ -52,19 +56,19 @@ public class HttpConnector implements QueueSource { private ExecutorService httpClientExecutor; private ScheduledExecutorService scheduler; private Map headers; + private PayloadCacheWrapper payloadCacheWrapper; + private PayloadCache payloadCache; + @NonNull private String url; - // TODO init failure backup cache redis - - // todo update provider readme - @Builder public HttpConnector(Integer pollIntervalSeconds, Integer linkedBlockingQueueCapacity, Integer scheduledThreadPoolSize, Integer requestTimeoutSeconds, Integer connectTimeoutSeconds, String url, - Map headers, ExecutorService httpClientExecutor, String proxyHost, Integer proxyPort) { + Map headers, ExecutorService httpClientExecutor, String proxyHost, Integer proxyPort, + PayloadCacheOptions payloadCacheOptions, PayloadCache payloadCache) { validate(url, pollIntervalSeconds, linkedBlockingQueueCapacity, scheduledThreadPoolSize, requestTimeoutSeconds, - connectTimeoutSeconds, proxyHost, proxyPort); + connectTimeoutSeconds, proxyHost, proxyPort, payloadCacheOptions, payloadCache); this.pollIntervalSeconds = pollIntervalSeconds == null ? DEFAULT_POLL_INTERVAL_SECONDS : pollIntervalSeconds; int thisLinkedBlockingQueueCapacity = linkedBlockingQueueCapacity == null ? DEFAULT_LINKED_BLOCKING_QUEUE_CAPACITY : linkedBlockingQueueCapacity; int thisScheduledThreadPoolSize = scheduledThreadPoolSize == null ? DEFAULT_SCHEDULED_THREAD_POOL_SIZE : scheduledThreadPoolSize; @@ -89,12 +93,20 @@ public HttpConnector(Integer pollIntervalSeconds, Integer linkedBlockingQueueCap .executor(this.httpClientExecutor) .build(); this.queue = new LinkedBlockingQueue<>(thisLinkedBlockingQueueCapacity); + this.payloadCache = payloadCache; + if (payloadCache != null) { + this.payloadCacheWrapper = PayloadCacheWrapper.builder() + .payloadCache(payloadCache) + .payloadCacheOptions(payloadCacheOptions) + .build(); + } } @SneakyThrows private void validate(String url, Integer pollIntervalSeconds, Integer linkedBlockingQueueCapacity, - Integer scheduledThreadPoolSize, Integer requestTimeoutSeconds, Integer connectTimeoutSeconds, - String proxyHost, Integer proxyPort) { + Integer scheduledThreadPoolSize, Integer requestTimeoutSeconds, Integer connectTimeoutSeconds, + String proxyHost, Integer proxyPort, PayloadCacheOptions payloadCacheOptions, + PayloadCache payloadCache) { new URL(url).toURI(); if (pollIntervalSeconds != null && (pollIntervalSeconds < 1 || pollIntervalSeconds > 600)) { throw new IllegalArgumentException("pollIntervalSeconds must be between 1 and 600"); @@ -119,6 +131,12 @@ private void validate(String url, Integer pollIntervalSeconds, Integer linkedBlo } else if (proxyHost == null && proxyPort != null) { throw new IllegalArgumentException("proxyHost must be set if proxyPort is set"); } + if (payloadCacheOptions != null && payloadCache == null) { + throw new IllegalArgumentException("payloadCache must be set if payloadCacheOptions is set"); + } + if (payloadCache != null && payloadCacheOptions == null) { + throw new IllegalArgumentException("payloadCacheOptions must be set if payloadCache is set"); + } } @Override @@ -128,51 +146,79 @@ public void init() throws Exception { @Override public BlockingQueue getStreamQueue() { + boolean success = fetchAndUpdate(); + if (!success) { + log.info("failed initial fetch"); + if (payloadCache != null) { + updateFromCache(); + } + } Runnable pollTask = buildPollTask(); - - // run first poll immediately and wait for it to finish - pollTask.run(); - scheduler.scheduleAtFixedRate(pollTask, pollIntervalSeconds, pollIntervalSeconds, TimeUnit.SECONDS); return queue; } + private void updateFromCache() { + log.info("taking initial payload from cache to avoid starting with default values"); + String flagData = payloadCache.get(); + if (flagData == null) { + log.debug("got null from cache"); + return; + } + if (!this.queue.offer(new QueuePayload(QueuePayloadType.DATA, flagData))) { + log.warn("init: Unable to offer file content to queue: queue is full"); + } + } + protected Runnable buildPollTask() { - return () -> { - HttpRequest.Builder requestBuilder = HttpRequest.newBuilder() - .uri(URI.create(url)) - .timeout(Duration.ofSeconds(requestTimeoutSeconds)) - .GET(); - headers.forEach(requestBuilder::header); - HttpRequest request = requestBuilder - .build(); + return this::fetchAndUpdate; + } - HttpResponse response; - try { - log.debug("fetching response"); - response = execute(request); - } catch (IOException e) { - log.info("could not fetch", e); - return; - } catch (Exception e) { - log.debug("exception", e); - return; - } - log.debug("fetched response"); - if (response.statusCode() != 200) { - log.info("received non-successful status code: {} {}", response.statusCode(), response.body()); - return; - } - if (!this.queue.offer(new QueuePayload(QueuePayloadType.DATA, response.body()))) { - log.warn("Unable to offer file content to queue: queue is full"); - } - }; + private boolean fetchAndUpdate() { + HttpRequest.Builder requestBuilder = HttpRequest.newBuilder() + .uri(URI.create(url)) + .timeout(Duration.ofSeconds(requestTimeoutSeconds)) + .GET(); + headers.forEach(requestBuilder::header); + HttpRequest request = requestBuilder + .build(); + + HttpResponse response; + try { + log.debug("fetching response"); + response = execute(request); + } catch (IOException e) { + log.info("could not fetch", e); + return false; + } catch (Exception e) { + log.debug("exception", e); + return false; + } + log.debug("fetched response"); + String payload = response.body(); + if (response.statusCode() != 200) { + log.info("received non-successful status code: {} {}", response.statusCode(), payload); + return false; + } + if (payload == null) { + log.debug("payload is null"); + return false; + } + if (!this.queue.offer(new QueuePayload(QueuePayloadType.DATA, payload))) { + log.warn("Unable to offer file content to queue: queue is full"); + return false; + } + if (payloadCacheWrapper != null) { + log.debug("scheduling cache update if needed"); + scheduler.execute(() -> + payloadCacheWrapper.updatePayloadIfNeeded(payload) + ); + } + return payload != null; } protected HttpResponse execute(HttpRequest request) throws IOException, InterruptedException { - HttpResponse response; - response = client.send(request, HttpResponse.BodyHandlers.ofString()); - return response; + return client.send(request, HttpResponse.BodyHandlers.ofString()); } @Override diff --git a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/connector/sync/PayloadCache.java b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/connector/sync/PayloadCache.java new file mode 100644 index 000000000..c2e8f62c4 --- /dev/null +++ b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/connector/sync/PayloadCache.java @@ -0,0 +1,6 @@ +package dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync; + +public interface PayloadCache { + public void put(String payload); + public String get(); +} diff --git a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/connector/sync/PayloadCacheOptions.java b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/connector/sync/PayloadCacheOptions.java new file mode 100644 index 000000000..50b614c3b --- /dev/null +++ b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/connector/sync/PayloadCacheOptions.java @@ -0,0 +1,24 @@ +package dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync; + +import lombok.Builder; +import lombok.Getter; + +/** + * Represents configuration options for caching payloads. + *

+ * This class provides options to configure the caching behavior, + * specifically the interval at which the cache should be updated. + *

+ *

+ * The default update interval is set to 30 minutes. + * Change it typically to a value according to cache ttl and tradeoff with not updating it too much for + * corner cases. + *

+ */ +@Builder +@Getter +public class PayloadCacheOptions { + + @Builder.Default + private int updateIntervalSeconds = 60 * 30; // 30 minutes +} diff --git a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/connector/sync/PayloadCacheWrapper.java b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/connector/sync/PayloadCacheWrapper.java new file mode 100644 index 000000000..1b84b7d35 --- /dev/null +++ b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/connector/sync/PayloadCacheWrapper.java @@ -0,0 +1,56 @@ +package dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync; + +import lombok.Builder; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; + +/** + * A wrapper class for managing a payload cache with a specified update interval. + * This class ensures that the cache is only updated if the specified time interval + * has passed since the last update. It logs debug messages when updates are skipped + * and error messages if the update process fails. + * Not thread-safe. + * + *

Usage involves creating an instance with {@link PayloadCacheOptions} to set + * the update interval, and then using {@link #updatePayloadIfNeeded(String)} to + * conditionally update the cache and {@link #get()} to retrieve the cached payload.

+ */ +@Slf4j +public class PayloadCacheWrapper { + private long lastUpdateTimeMs; + private long updateIntervalMs; + private PayloadCache payloadCache; + + @Builder + public PayloadCacheWrapper(PayloadCache payloadCache, PayloadCacheOptions payloadCacheOptions) { + if (payloadCacheOptions.getUpdateIntervalSeconds() < 500) { + throw new IllegalArgumentException("pollIntervalSeconds must be larger than 500"); + } + this.updateIntervalMs = payloadCacheOptions.getUpdateIntervalSeconds() * 1000; + this.payloadCache = payloadCache; + } + + public void updatePayloadIfNeeded(String payload) { + if ((System.currentTimeMillis() - lastUpdateTimeMs) < updateIntervalMs) { + log.debug("not updating payload, updateIntervalMs not reached"); + return; + } + + try { + log.debug("updating payload"); + payloadCache.put(payload); + lastUpdateTimeMs = System.currentTimeMillis(); + } catch (Exception e) { + log.error("failed updating cache", e); + } + } + + public String get() { + try { + return payloadCache.get(); + } catch (Exception e) { + log.error("failed getting from cache", e); + return null; + } + } +} diff --git a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/connector/sync/HttpConnectorTest.java b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/connector/sync/HttpConnectorTest.java index 60ffd05ef..6bae6b627 100644 --- a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/connector/sync/HttpConnectorTest.java +++ b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/connector/sync/HttpConnectorTest.java @@ -272,6 +272,49 @@ void testSuccessfulHttpResponseAddsDataToQueue() { assertEquals("test data", payload.getFlagData()); } + @SneakyThrows + @Test + void testInitFailureUsingCache() { + String testUrl = "http://example.com"; + HttpClient mockClient = mock(HttpClient.class); + HttpResponse mockResponse = mock(HttpResponse.class); + when(mockResponse.statusCode()).thenReturn(200); + when(mockClient.send(any(HttpRequest.class), any(HttpResponse.BodyHandler.class))) + .thenThrow(new IOException("Simulated IO Exception")); + + final String cachedData = "cached data"; + PayloadCache payloadCache = new PayloadCache() { + @Override + public void put(String payload) { + // do nothing + } + + @Override + public String get() { + return cachedData; + } + }; + + HttpConnector connector = HttpConnector.builder() + .url(testUrl) + .httpClientExecutor(Executors.newSingleThreadExecutor()) + .payloadCache(payloadCache) + .payloadCacheOptions(PayloadCacheOptions.builder().build()) + .build(); + + Field clientField = HttpConnector.class.getDeclaredField("client"); + clientField.setAccessible(true); + clientField.set(connector, mockClient); + + BlockingQueue queue = connector.getStreamQueue(); + + assertFalse(queue.isEmpty()); + QueuePayload payload = queue.poll(); + assertNotNull(payload); + assertEquals(QueuePayloadType.DATA, payload.getType()); + assertEquals(cachedData, payload.getFlagData()); + } + @SneakyThrows @Test void testQueueBecomesFull() { From d8f6943ca8e38fd4c973e66ff086cbd0cc7b7fb8 Mon Sep 17 00:00:00 2001 From: liran2000 Date: Mon, 31 Mar 2025 09:10:32 +0300 Subject: [PATCH 03/40] adding http cache Signed-off-by: liran2000 --- providers/flagd/README.md | 5 +- .../connector/sync/HttpCacheFetcher.java | 46 +++++++++++ .../storage/connector/sync/HttpConnector.java | 27 +++++-- .../sync/HttpConnectorIntegrationTest.java | 78 +++++++++++++++++++ .../connector/sync/HttpConnectorTest.java | 2 +- .../test/resources/simplelogger.properties | 2 +- 6 files changed, 150 insertions(+), 10 deletions(-) create mode 100644 providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/connector/sync/HttpCacheFetcher.java create mode 100644 providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/connector/sync/HttpConnectorIntegrationTest.java diff --git a/providers/flagd/README.md b/providers/flagd/README.md index f0438f727..1f2e6de0a 100644 --- a/providers/flagd/README.md +++ b/providers/flagd/README.md @@ -55,7 +55,10 @@ This can be used to enrich evaluations with such data. If the `in-process` mode is not used, and before the provider is ready, the `getSyncMetadata` returns an empty map. #### Http Connector -HttpConnector is responsible for polling data from a specified URL at regular intervals. +HttpConnector is responsible for polling data from a specified URL at regular intervals. +It is implementing Http cache mechanism with 'ETag' header, then when receiving 304 Not Modified response, reducing traffic and +changes updates. Can be enabled via useHttpCache option. +One of its benefits is to reduce infrastructure/devops work, without additional containers needed. The implementation is using Java HttpClient. ##### What happens if the Http source is down when application is starting ? diff --git a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/connector/sync/HttpCacheFetcher.java b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/connector/sync/HttpCacheFetcher.java new file mode 100644 index 000000000..97fb17b18 --- /dev/null +++ b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/connector/sync/HttpCacheFetcher.java @@ -0,0 +1,46 @@ +package dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync; + +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; + +/** + * Fetches content from a given HTTP endpoint using caching headers to optimize network usage. + * If cached ETag or Last-Modified values are available, they are included in the request headers + * to potentially receive a 304 Not Modified response, reducing data transfer. + * Updates the cached ETag and Last-Modified values upon receiving a 200 OK response. + * It does not store the cached response, assuming not needed after first successful fetching. + * + * @param httpClient the HTTP client used to send the request + * @param httpRequestBuilder the builder for constructing the HTTP request + * @return the HTTP response received from the server + */ +@Slf4j +public class HttpCacheFetcher { + private static String cachedETag = null; + private static String cachedLastModified = null; + + @SneakyThrows + public HttpResponse fetchContent(HttpClient httpClient, HttpRequest.Builder httpRequestBuilder) { + if (cachedETag != null) { + httpRequestBuilder.header("If-None-Match", cachedETag); + } + if (cachedLastModified != null) { + httpRequestBuilder.header("If-Modified-Since", cachedLastModified); + } + + HttpRequest request = httpRequestBuilder.build(); + HttpResponse httpResponse = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); + + if (httpResponse.statusCode() == 200) { + cachedETag = httpResponse.headers().firstValue("ETag").orElse(null); + cachedLastModified = httpResponse.headers().firstValue("Last-Modified").orElse(null); + log.debug("fetched new content"); + } else if (httpResponse.statusCode() == 304) { + log.debug("got 304 Not Modified"); + } + return httpResponse; + } +} diff --git a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/connector/sync/HttpConnector.java b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/connector/sync/HttpConnector.java index 63311ede7..abb0989c0 100644 --- a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/connector/sync/HttpConnector.java +++ b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/connector/sync/HttpConnector.java @@ -58,6 +58,7 @@ public class HttpConnector implements QueueSource { private Map headers; private PayloadCacheWrapper payloadCacheWrapper; private PayloadCache payloadCache; + private HttpCacheFetcher httpCacheFetcher; @NonNull private String url; @@ -66,7 +67,7 @@ public class HttpConnector implements QueueSource { public HttpConnector(Integer pollIntervalSeconds, Integer linkedBlockingQueueCapacity, Integer scheduledThreadPoolSize, Integer requestTimeoutSeconds, Integer connectTimeoutSeconds, String url, Map headers, ExecutorService httpClientExecutor, String proxyHost, Integer proxyPort, - PayloadCacheOptions payloadCacheOptions, PayloadCache payloadCache) { + PayloadCacheOptions payloadCacheOptions, PayloadCache payloadCache, Boolean useHttpCache) { validate(url, pollIntervalSeconds, linkedBlockingQueueCapacity, scheduledThreadPoolSize, requestTimeoutSeconds, connectTimeoutSeconds, proxyHost, proxyPort, payloadCacheOptions, payloadCache); this.pollIntervalSeconds = pollIntervalSeconds == null ? DEFAULT_POLL_INTERVAL_SECONDS : pollIntervalSeconds; @@ -100,6 +101,9 @@ public HttpConnector(Integer pollIntervalSeconds, Integer linkedBlockingQueueCap .payloadCacheOptions(payloadCacheOptions) .build(); } + if (Boolean.TRUE.equals(useHttpCache)) { + httpCacheFetcher = new HttpCacheFetcher(); + } } @SneakyThrows @@ -180,13 +184,11 @@ private boolean fetchAndUpdate() { .timeout(Duration.ofSeconds(requestTimeoutSeconds)) .GET(); headers.forEach(requestBuilder::header); - HttpRequest request = requestBuilder - .build(); HttpResponse response; try { log.debug("fetching response"); - response = execute(request); + response = execute(requestBuilder); } catch (IOException e) { log.info("could not fetch", e); return false; @@ -196,14 +198,18 @@ private boolean fetchAndUpdate() { } log.debug("fetched response"); String payload = response.body(); - if (response.statusCode() != 200) { + if (!isSuccessful(response)) { log.info("received non-successful status code: {} {}", response.statusCode(), payload); return false; + } else if (response.statusCode() == 304) { + log.debug("got 304 Not Modified, skipping update"); + return false; } if (payload == null) { log.debug("payload is null"); return false; } + log.debug("adding payload to queue"); if (!this.queue.offer(new QueuePayload(QueuePayloadType.DATA, payload))) { log.warn("Unable to offer file content to queue: queue is full"); return false; @@ -217,8 +223,15 @@ private boolean fetchAndUpdate() { return payload != null; } - protected HttpResponse execute(HttpRequest request) throws IOException, InterruptedException { - return client.send(request, HttpResponse.BodyHandlers.ofString()); + private static boolean isSuccessful(HttpResponse response) { + return response.statusCode() == 200 || response.statusCode() == 304; + } + + protected HttpResponse execute(HttpRequest.Builder requestBuilder) throws IOException, InterruptedException { + if (httpCacheFetcher != null) { + return httpCacheFetcher.fetchContent(client, requestBuilder); + } + return client.send(requestBuilder.build(), HttpResponse.BodyHandlers.ofString()); } @Override diff --git a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/connector/sync/HttpConnectorIntegrationTest.java b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/connector/sync/HttpConnectorIntegrationTest.java new file mode 100644 index 000000000..a870ff62a --- /dev/null +++ b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/connector/sync/HttpConnectorIntegrationTest.java @@ -0,0 +1,78 @@ +package dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync; + +import static dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.HttpConnectorTest.delay; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assumptions.assumeTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.when; + +import dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.QueuePayload; +import dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.QueuePayloadType; +import java.io.IOException; +import java.lang.reflect.Field; +import java.net.MalformedURLException; +import java.net.ProxySelector; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +/** + * Integration test for the HttpConnector class, specifically testing the ability to fetch + * raw content from a GitHub URL. This test assumes that integration tests are enabled + * and verifies that the HttpConnector can successfully enqueue data from the specified URL. + * The test initializes the HttpConnector with specific configurations, waits for data + * to be enqueued, and asserts the expected queue size. The connector is shut down + * gracefully after the test execution. + * As this integration test using external request, it is disabled by default, and not part of the CI build. + */ +@Slf4j +class HttpConnectorIntegrationTest { + + @SneakyThrows + @Test + void testGithubRawContent() { + assumeTrue(parseBoolean("integrationTestsEnabled")); + HttpConnector connector = null; + try { + String testUrl = "https://raw.githubusercontent.com/open-feature/java-sdk-contrib/58fe5da7d4e2f6f4ae2c1caf3411a01e84a1dc1a/providers/flagd/version.txt"; + connector = HttpConnector.builder() + .url(testUrl) + .connectTimeoutSeconds(10) + .requestTimeoutSeconds(10) + .useHttpCache(true) + .pollIntervalSeconds(5) + .build(); + BlockingQueue queue = connector.getStreamQueue(); + delay(20000); + assertEquals(1, queue.size()); + } finally { + if (connector != null) { + connector.shutdown(); + } + } + } + + public static boolean parseBoolean(String key) { + return Boolean.parseBoolean(System.getProperty(key, System.getenv(key))); + } + +} diff --git a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/connector/sync/HttpConnectorTest.java b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/connector/sync/HttpConnectorTest.java index 6bae6b627..910fa3480 100644 --- a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/connector/sync/HttpConnectorTest.java +++ b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/connector/sync/HttpConnectorTest.java @@ -579,7 +579,7 @@ void testQueuePayloadTypeSetToDataOnSuccess() { } @SneakyThrows - private static void delay(long ms) { + protected static void delay(long ms) { Thread.sleep(ms); } diff --git a/providers/flagd/src/test/resources/simplelogger.properties b/providers/flagd/src/test/resources/simplelogger.properties index d2ca1bbdc..769e4e8bf 100644 --- a/providers/flagd/src/test/resources/simplelogger.properties +++ b/providers/flagd/src/test/resources/simplelogger.properties @@ -1,4 +1,4 @@ -org.org.slf4j.simpleLogger.defaultLogLevel=debug +org.slf4j.simpleLogger.defaultLogLevel=debug org.slf4j.simpleLogger.showDateTime= io.grpc.level=trace From c5d93b5d558bb0560215202ebeee106f608edb63 Mon Sep 17 00:00:00 2001 From: liran2000 Date: Mon, 31 Mar 2025 12:40:39 +0300 Subject: [PATCH 04/40] refactor for using options Signed-off-by: liran2000 --- providers/flagd/README.md | 2 +- .../sync/{ => http}/HttpCacheFetcher.java | 13 +- .../sync/{ => http}/HttpConnector.java | 94 +--- .../sync/http/HttpConnectorOptions.java | 125 ++++++ .../sync/{ => http}/PayloadCache.java | 2 +- .../sync/{ => http}/PayloadCacheOptions.java | 2 +- .../sync/{ => http}/PayloadCacheWrapper.java | 17 +- .../sync/http/HttpCacheFetcherTest.java | 308 +++++++++++++ .../HttpConnectorIntegrationTest.java | 35 +- .../sync/http/HttpConnectorOptionsTest.java | 406 ++++++++++++++++++ .../sync/{ => http}/HttpConnectorTest.java | 316 +++----------- .../sync/http/PayloadCacheWrapperTest.java | 267 ++++++++++++ 12 files changed, 1223 insertions(+), 364 deletions(-) rename providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/connector/sync/{ => http}/HttpCacheFetcher.java (81%) rename providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/connector/sync/{ => http}/HttpConnector.java (55%) create mode 100644 providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/connector/sync/http/HttpConnectorOptions.java rename providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/connector/sync/{ => http}/PayloadCache.java (84%) rename providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/connector/sync/{ => http}/PayloadCacheOptions.java (95%) rename providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/connector/sync/{ => http}/PayloadCacheWrapper.java (82%) create mode 100644 providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/connector/sync/http/HttpCacheFetcherTest.java rename providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/connector/sync/{ => http}/HttpConnectorIntegrationTest.java (65%) create mode 100644 providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/connector/sync/http/HttpConnectorOptionsTest.java rename providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/connector/sync/{ => http}/HttpConnectorTest.java (57%) create mode 100644 providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/connector/sync/http/PayloadCacheWrapperTest.java diff --git a/providers/flagd/README.md b/providers/flagd/README.md index 1f2e6de0a..d26ef6433 100644 --- a/providers/flagd/README.md +++ b/providers/flagd/README.md @@ -56,7 +56,7 @@ If the `in-process` mode is not used, and before the provider is ready, the `get #### Http Connector HttpConnector is responsible for polling data from a specified URL at regular intervals. -It is implementing Http cache mechanism with 'ETag' header, then when receiving 304 Not Modified response, reducing traffic and +It is leveraging Http cache mechanism with 'ETag' header, then when receiving 304 Not Modified response, reducing traffic and changes updates. Can be enabled via useHttpCache option. One of its benefits is to reduce infrastructure/devops work, without additional containers needed. The implementation is using Java HttpClient. diff --git a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/connector/sync/HttpCacheFetcher.java b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/connector/sync/http/HttpCacheFetcher.java similarity index 81% rename from providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/connector/sync/HttpCacheFetcher.java rename to providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/connector/sync/http/HttpCacheFetcher.java index 97fb17b18..e2a59a9fc 100644 --- a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/connector/sync/HttpCacheFetcher.java +++ b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/connector/sync/http/HttpCacheFetcher.java @@ -1,4 +1,4 @@ -package dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync; +package dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http; import java.net.http.HttpClient; import java.net.http.HttpRequest; @@ -12,6 +12,7 @@ * to potentially receive a 304 Not Modified response, reducing data transfer. * Updates the cached ETag and Last-Modified values upon receiving a 200 OK response. * It does not store the cached response, assuming not needed after first successful fetching. + * Non thread-safe. * * @param httpClient the HTTP client used to send the request * @param httpRequestBuilder the builder for constructing the HTTP request @@ -19,8 +20,8 @@ */ @Slf4j public class HttpCacheFetcher { - private static String cachedETag = null; - private static String cachedLastModified = null; + private String cachedETag = null; + private String cachedLastModified = null; @SneakyThrows public HttpResponse fetchContent(HttpClient httpClient, HttpRequest.Builder httpRequestBuilder) { @@ -35,8 +36,10 @@ public HttpResponse fetchContent(HttpClient httpClient, HttpRequest.Buil HttpResponse httpResponse = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); if (httpResponse.statusCode() == 200) { - cachedETag = httpResponse.headers().firstValue("ETag").orElse(null); - cachedLastModified = httpResponse.headers().firstValue("Last-Modified").orElse(null); + if (httpResponse.headers() != null) { + cachedETag = httpResponse.headers().firstValue("ETag").orElse(null); + cachedLastModified = httpResponse.headers().firstValue("Last-Modified").orElse(null); + } log.debug("fetched new content"); } else if (httpResponse.statusCode() == 304) { log.debug("got 304 Not Modified"); diff --git a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/connector/sync/HttpConnector.java b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/connector/sync/http/HttpConnector.java similarity index 55% rename from providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/connector/sync/HttpConnector.java rename to providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/connector/sync/http/HttpConnector.java index abb0989c0..b72b5c035 100644 --- a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/connector/sync/HttpConnector.java +++ b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/connector/sync/http/HttpConnector.java @@ -1,4 +1,4 @@ -package dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync; +package dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http; import dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.QueuePayload; import dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.QueuePayloadType; @@ -6,19 +6,16 @@ import dev.openfeature.contrib.providers.flagd.util.ConcurrentUtils; import lombok.Builder; import lombok.NonNull; -import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; import java.io.IOException; import java.net.InetSocketAddress; import java.net.ProxySelector; import java.net.URI; -import java.net.URL; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.time.Duration; -import java.util.HashMap; import java.util.Map; import java.util.concurrent.BlockingQueue; import java.util.concurrent.ExecutorService; @@ -43,12 +40,6 @@ @Slf4j public class HttpConnector implements QueueSource { - private static final int DEFAULT_POLL_INTERVAL_SECONDS = 60; - private static final int DEFAULT_LINKED_BLOCKING_QUEUE_CAPACITY = 100; - private static final int DEFAULT_SCHEDULED_THREAD_POOL_SIZE = 2; - private static final int DEFAULT_REQUEST_TIMEOUT_SECONDS = 10; - private static final int DEFAULT_CONNECT_TIMEOUT_SECONDS = 10; - private Integer pollIntervalSeconds; private Integer requestTimeoutSeconds; private BlockingQueue queue; @@ -64,85 +55,36 @@ public class HttpConnector implements QueueSource { private String url; @Builder - public HttpConnector(Integer pollIntervalSeconds, Integer linkedBlockingQueueCapacity, - Integer scheduledThreadPoolSize, Integer requestTimeoutSeconds, Integer connectTimeoutSeconds, String url, - Map headers, ExecutorService httpClientExecutor, String proxyHost, Integer proxyPort, - PayloadCacheOptions payloadCacheOptions, PayloadCache payloadCache, Boolean useHttpCache) { - validate(url, pollIntervalSeconds, linkedBlockingQueueCapacity, scheduledThreadPoolSize, requestTimeoutSeconds, - connectTimeoutSeconds, proxyHost, proxyPort, payloadCacheOptions, payloadCache); - this.pollIntervalSeconds = pollIntervalSeconds == null ? DEFAULT_POLL_INTERVAL_SECONDS : pollIntervalSeconds; - int thisLinkedBlockingQueueCapacity = linkedBlockingQueueCapacity == null ? DEFAULT_LINKED_BLOCKING_QUEUE_CAPACITY : linkedBlockingQueueCapacity; - int thisScheduledThreadPoolSize = scheduledThreadPoolSize == null ? DEFAULT_SCHEDULED_THREAD_POOL_SIZE : scheduledThreadPoolSize; - this.requestTimeoutSeconds = requestTimeoutSeconds == null ? DEFAULT_REQUEST_TIMEOUT_SECONDS : requestTimeoutSeconds; - int thisConnectTimeoutSeconds = connectTimeoutSeconds == null ? DEFAULT_CONNECT_TIMEOUT_SECONDS : connectTimeoutSeconds; + public HttpConnector(HttpConnectorOptions httpConnectorOptions) { + this.pollIntervalSeconds = httpConnectorOptions.getPollIntervalSeconds(); + this.requestTimeoutSeconds = httpConnectorOptions.getRequestTimeoutSeconds(); ProxySelector proxySelector = NO_PROXY; - if (proxyHost != null && proxyPort != null) { - proxySelector = ProxySelector.of(new InetSocketAddress(proxyHost, proxyPort)); - } - - this.url = url; - this.headers = headers; - this.httpClientExecutor = httpClientExecutor == null ? Executors.newFixedThreadPool(1) : - httpClientExecutor; - scheduler = Executors.newScheduledThreadPool(thisScheduledThreadPoolSize); - if (headers == null) { - this.headers = new HashMap<>(); - } + if (httpConnectorOptions.getProxyHost() != null && httpConnectorOptions.getProxyPort() != null) { + proxySelector = ProxySelector.of(new InetSocketAddress(httpConnectorOptions.getProxyHost(), + httpConnectorOptions.getProxyPort())); + } + this.url = httpConnectorOptions.getUrl(); + this.headers = httpConnectorOptions.getHeaders(); + this.httpClientExecutor = httpConnectorOptions.getHttpClientExecutor(); + scheduler = Executors.newScheduledThreadPool(httpConnectorOptions.getScheduledThreadPoolSize()); this.client = HttpClient.newBuilder() - .connectTimeout(Duration.ofSeconds(thisConnectTimeoutSeconds)) + .connectTimeout(Duration.ofSeconds(httpConnectorOptions.getConnectTimeoutSeconds())) .proxy(proxySelector) .executor(this.httpClientExecutor) .build(); - this.queue = new LinkedBlockingQueue<>(thisLinkedBlockingQueueCapacity); - this.payloadCache = payloadCache; + this.queue = new LinkedBlockingQueue<>(httpConnectorOptions.getLinkedBlockingQueueCapacity()); + this.payloadCache = httpConnectorOptions.getPayloadCache(); if (payloadCache != null) { this.payloadCacheWrapper = PayloadCacheWrapper.builder() .payloadCache(payloadCache) - .payloadCacheOptions(payloadCacheOptions) + .payloadCacheOptions(httpConnectorOptions.getPayloadCacheOptions()) .build(); } - if (Boolean.TRUE.equals(useHttpCache)) { + if (Boolean.TRUE.equals(httpConnectorOptions.getUseHttpCache())) { httpCacheFetcher = new HttpCacheFetcher(); } } - @SneakyThrows - private void validate(String url, Integer pollIntervalSeconds, Integer linkedBlockingQueueCapacity, - Integer scheduledThreadPoolSize, Integer requestTimeoutSeconds, Integer connectTimeoutSeconds, - String proxyHost, Integer proxyPort, PayloadCacheOptions payloadCacheOptions, - PayloadCache payloadCache) { - new URL(url).toURI(); - if (pollIntervalSeconds != null && (pollIntervalSeconds < 1 || pollIntervalSeconds > 600)) { - throw new IllegalArgumentException("pollIntervalSeconds must be between 1 and 600"); - } - if (linkedBlockingQueueCapacity != null && (linkedBlockingQueueCapacity < 1 || linkedBlockingQueueCapacity > 1000)) { - throw new IllegalArgumentException("linkedBlockingQueueCapacity must be between 1 and 1000"); - } - if (scheduledThreadPoolSize != null && (scheduledThreadPoolSize < 1 || scheduledThreadPoolSize > 10)) { - throw new IllegalArgumentException("scheduledThreadPoolSize must be between 1 and 10"); - } - if (requestTimeoutSeconds != null && (requestTimeoutSeconds < 1 || requestTimeoutSeconds > 60)) { - throw new IllegalArgumentException("requestTimeoutSeconds must be between 1 and 60"); - } - if (connectTimeoutSeconds != null && (connectTimeoutSeconds < 1 || connectTimeoutSeconds > 60)) { - throw new IllegalArgumentException("connectTimeoutSeconds must be between 1 and 60"); - } - if (proxyPort != null && (proxyPort < 1 || proxyPort > 65535)) { - throw new IllegalArgumentException("proxyPort must be between 1 and 65535"); - } - if (proxyHost != null && proxyPort == null ) { - throw new IllegalArgumentException("proxyPort must be set if proxyHost is set"); - } else if (proxyHost == null && proxyPort != null) { - throw new IllegalArgumentException("proxyHost must be set if proxyPort is set"); - } - if (payloadCacheOptions != null && payloadCache == null) { - throw new IllegalArgumentException("payloadCache must be set if payloadCacheOptions is set"); - } - if (payloadCache != null && payloadCacheOptions == null) { - throw new IllegalArgumentException("payloadCacheOptions must be set if payloadCache is set"); - } - } - @Override public void init() throws Exception { log.info("init Http Connector"); @@ -158,7 +100,7 @@ public BlockingQueue getStreamQueue() { } } Runnable pollTask = buildPollTask(); - scheduler.scheduleAtFixedRate(pollTask, pollIntervalSeconds, pollIntervalSeconds, TimeUnit.SECONDS); + scheduler.scheduleWithFixedDelay(pollTask, pollIntervalSeconds, pollIntervalSeconds, TimeUnit.SECONDS); return queue; } diff --git a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/connector/sync/http/HttpConnectorOptions.java b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/connector/sync/http/HttpConnectorOptions.java new file mode 100644 index 000000000..0f8ff0186 --- /dev/null +++ b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/connector/sync/http/HttpConnectorOptions.java @@ -0,0 +1,125 @@ +package dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http; + +import java.net.URL; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import lombok.Builder; +import lombok.Getter; +import lombok.NonNull; +import lombok.SneakyThrows; + +@Getter +public class HttpConnectorOptions { + + @Builder.Default + private Integer pollIntervalSeconds = 60; + @Builder.Default + private Integer connectTimeoutSeconds = 10; + @Builder.Default + private Integer requestTimeoutSeconds = 10; + @Builder.Default + private Integer linkedBlockingQueueCapacity = 100; + @Builder.Default + private Integer scheduledThreadPoolSize = 2; + @Builder.Default + private Map headers = new HashMap<>(); + @Builder.Default + private ExecutorService httpClientExecutor = Executors.newFixedThreadPool(1); + @Builder.Default + private String proxyHost; + @Builder.Default + private Integer proxyPort; + @Builder.Default + private PayloadCacheOptions payloadCacheOptions; + @Builder.Default + private PayloadCache payloadCache; + @Builder.Default + private Boolean useHttpCache; + @NonNull + private String url; + + @Builder + public HttpConnectorOptions(Integer pollIntervalSeconds, Integer linkedBlockingQueueCapacity, + Integer scheduledThreadPoolSize, Integer requestTimeoutSeconds, Integer connectTimeoutSeconds, String url, + Map headers, ExecutorService httpClientExecutor, String proxyHost, Integer proxyPort, + PayloadCacheOptions payloadCacheOptions, PayloadCache payloadCache, Boolean useHttpCache) { + validate(url, pollIntervalSeconds, linkedBlockingQueueCapacity, scheduledThreadPoolSize, requestTimeoutSeconds, + connectTimeoutSeconds, proxyHost, proxyPort, payloadCacheOptions, payloadCache); + if (pollIntervalSeconds != null) { + this.pollIntervalSeconds = pollIntervalSeconds; + } + if (linkedBlockingQueueCapacity != null) { + this.linkedBlockingQueueCapacity = linkedBlockingQueueCapacity; + } + if (scheduledThreadPoolSize != null) { + this.scheduledThreadPoolSize = scheduledThreadPoolSize; + } + if (requestTimeoutSeconds != null) { + this.requestTimeoutSeconds = requestTimeoutSeconds; + } + if (connectTimeoutSeconds != null) { + this.connectTimeoutSeconds = connectTimeoutSeconds; + } + this.url = url; + if (headers != null) { + this.headers = headers; + } + if (httpClientExecutor != null) { + this.httpClientExecutor = httpClientExecutor; + } + if (proxyHost != null) { + this.proxyHost = proxyHost; + } + if (proxyPort != null) { + this.proxyPort = proxyPort; + } + if (payloadCache != null) { + this.payloadCache = payloadCache; + } + if (payloadCacheOptions != null) { + this.payloadCacheOptions = payloadCacheOptions; + } + if (useHttpCache != null) { + this.useHttpCache = useHttpCache; + } + } + + @SneakyThrows + private void validate(String url, Integer pollIntervalSeconds, Integer linkedBlockingQueueCapacity, + Integer scheduledThreadPoolSize, Integer requestTimeoutSeconds, Integer connectTimeoutSeconds, + String proxyHost, Integer proxyPort, PayloadCacheOptions payloadCacheOptions, + PayloadCache payloadCache) { + new URL(url).toURI(); + if (linkedBlockingQueueCapacity != null && (linkedBlockingQueueCapacity < 1 || linkedBlockingQueueCapacity > 1000)) { + throw new IllegalArgumentException("linkedBlockingQueueCapacity must be between 1 and 1000"); + } + if (scheduledThreadPoolSize != null && (scheduledThreadPoolSize < 1 || scheduledThreadPoolSize > 10)) { + throw new IllegalArgumentException("scheduledThreadPoolSize must be between 1 and 10"); + } + if (requestTimeoutSeconds != null && (requestTimeoutSeconds < 1 || requestTimeoutSeconds > 60)) { + throw new IllegalArgumentException("requestTimeoutSeconds must be between 1 and 60"); + } + if (connectTimeoutSeconds != null && (connectTimeoutSeconds < 1 || connectTimeoutSeconds > 60)) { + throw new IllegalArgumentException("connectTimeoutSeconds must be between 1 and 60"); + } + if (pollIntervalSeconds != null && (pollIntervalSeconds < 1 || pollIntervalSeconds > 600)) { + throw new IllegalArgumentException("pollIntervalSeconds must be between 1 and 600"); + } + if (proxyPort != null && (proxyPort < 1 || proxyPort > 65535)) { + throw new IllegalArgumentException("proxyPort must be between 1 and 65535"); + } + if (proxyHost != null && proxyPort == null ) { + throw new IllegalArgumentException("proxyPort must be set if proxyHost is set"); + } else if (proxyHost == null && proxyPort != null) { + throw new IllegalArgumentException("proxyHost must be set if proxyPort is set"); + } + if (payloadCacheOptions != null && payloadCache == null) { + throw new IllegalArgumentException("payloadCache must be set if payloadCacheOptions is set"); + } + if (payloadCache != null && payloadCacheOptions == null) { + throw new IllegalArgumentException("payloadCacheOptions must be set if payloadCache is set"); + } + } +} diff --git a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/connector/sync/PayloadCache.java b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/connector/sync/http/PayloadCache.java similarity index 84% rename from providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/connector/sync/PayloadCache.java rename to providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/connector/sync/http/PayloadCache.java index c2e8f62c4..31416af1e 100644 --- a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/connector/sync/PayloadCache.java +++ b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/connector/sync/http/PayloadCache.java @@ -1,4 +1,4 @@ -package dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync; +package dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http; public interface PayloadCache { public void put(String payload); diff --git a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/connector/sync/PayloadCacheOptions.java b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/connector/sync/http/PayloadCacheOptions.java similarity index 95% rename from providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/connector/sync/PayloadCacheOptions.java rename to providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/connector/sync/http/PayloadCacheOptions.java index 50b614c3b..d29ed115d 100644 --- a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/connector/sync/PayloadCacheOptions.java +++ b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/connector/sync/http/PayloadCacheOptions.java @@ -1,4 +1,4 @@ -package dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync; +package dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http; import lombok.Builder; import lombok.Getter; diff --git a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/connector/sync/PayloadCacheWrapper.java b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/connector/sync/http/PayloadCacheWrapper.java similarity index 82% rename from providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/connector/sync/PayloadCacheWrapper.java rename to providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/connector/sync/http/PayloadCacheWrapper.java index 1b84b7d35..449cf1969 100644 --- a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/connector/sync/PayloadCacheWrapper.java +++ b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/connector/sync/http/PayloadCacheWrapper.java @@ -1,7 +1,6 @@ -package dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync; +package dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http; import lombok.Builder; -import lombok.Getter; import lombok.extern.slf4j.Slf4j; /** @@ -23,15 +22,15 @@ public class PayloadCacheWrapper { @Builder public PayloadCacheWrapper(PayloadCache payloadCache, PayloadCacheOptions payloadCacheOptions) { - if (payloadCacheOptions.getUpdateIntervalSeconds() < 500) { - throw new IllegalArgumentException("pollIntervalSeconds must be larger than 500"); + if (payloadCacheOptions.getUpdateIntervalSeconds() < 1) { + throw new IllegalArgumentException("pollIntervalSeconds must be larger than 0"); } - this.updateIntervalMs = payloadCacheOptions.getUpdateIntervalSeconds() * 1000; + this.updateIntervalMs = payloadCacheOptions.getUpdateIntervalSeconds() * 1000L; this.payloadCache = payloadCache; } public void updatePayloadIfNeeded(String payload) { - if ((System.currentTimeMillis() - lastUpdateTimeMs) < updateIntervalMs) { + if ((getCurrentTimeMillis() - lastUpdateTimeMs) < updateIntervalMs) { log.debug("not updating payload, updateIntervalMs not reached"); return; } @@ -39,12 +38,16 @@ public void updatePayloadIfNeeded(String payload) { try { log.debug("updating payload"); payloadCache.put(payload); - lastUpdateTimeMs = System.currentTimeMillis(); + lastUpdateTimeMs = getCurrentTimeMillis(); } catch (Exception e) { log.error("failed updating cache", e); } } + protected long getCurrentTimeMillis() { + return System.currentTimeMillis(); + } + public String get() { try { return payloadCache.get(); diff --git a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/connector/sync/http/HttpCacheFetcherTest.java b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/connector/sync/http/HttpCacheFetcherTest.java new file mode 100644 index 000000000..a6d0b859e --- /dev/null +++ b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/connector/sync/http/HttpCacheFetcherTest.java @@ -0,0 +1,308 @@ +package dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http; + +import static org.junit.Assert.assertNull; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.io.IOException; +import java.lang.reflect.Field; +import java.net.http.HttpClient; +import java.net.http.HttpHeaders; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import lombok.SneakyThrows; +import org.junit.jupiter.api.Test; +public class HttpCacheFetcherTest { + + @Test + public void testFirstRequestSendsNoCacheHeaders() throws Exception { + HttpClient httpClientMock = mock(HttpClient.class); + HttpRequest.Builder requestBuilderMock = mock(HttpRequest.Builder.class); + HttpRequest requestMock = mock(HttpRequest.class); + HttpResponse responseMock = spy(HttpResponse.class); + doReturn(HttpHeaders.of(new HashMap<>(), (a, b) -> true)).when(responseMock).headers(); + + when(requestBuilderMock.build()).thenReturn(requestMock); + when(httpClientMock.send(eq(requestMock), any(HttpResponse.BodyHandler.class))).thenReturn(responseMock); + when(responseMock.statusCode()).thenReturn(200); + + HttpCacheFetcher fetcher = new HttpCacheFetcher(); + fetcher.fetchContent(httpClientMock, requestBuilderMock); + + verify(requestBuilderMock, never()).header(eq("If-None-Match"), anyString()); + verify(requestBuilderMock, never()).header(eq("If-Modified-Since"), anyString()); + } + + @Test + public void testResponseWith200ButNoCacheHeaders() throws Exception { + HttpClient httpClientMock = mock(HttpClient.class); + HttpRequest.Builder requestBuilderMock = mock(HttpRequest.Builder.class); + HttpRequest requestMock = mock(HttpRequest.class); + HttpResponse responseMock = mock(HttpResponse.class); + HttpHeaders headersMock = mock(HttpHeaders.class); + + when(requestBuilderMock.build()).thenReturn(requestMock); + when(httpClientMock.send(eq(requestMock), any(HttpResponse.BodyHandler.class))).thenReturn(responseMock); + when(responseMock.statusCode()).thenReturn(200); + when(responseMock.headers()).thenReturn(headersMock); + when(headersMock.firstValue("ETag")).thenReturn(Optional.empty()); + when(headersMock.firstValue("Last-Modified")).thenReturn(Optional.empty()); + + HttpCacheFetcher fetcher = new HttpCacheFetcher(); + HttpResponse response = fetcher.fetchContent(httpClientMock, requestBuilderMock); + + assertEquals(200, response.statusCode()); + + HttpRequest.Builder secondRequestBuilderMock = mock(HttpRequest.Builder.class); + when(secondRequestBuilderMock.build()).thenReturn(requestMock); + + fetcher.fetchContent(httpClientMock, secondRequestBuilderMock); + + verify(secondRequestBuilderMock, never()).header(eq("If-None-Match"), anyString()); + verify(secondRequestBuilderMock, never()).header(eq("If-Modified-Since"), anyString()); + } + + @Test + public void testFetchContentReturnsHttpResponse() throws Exception { + HttpClient httpClientMock = mock(HttpClient.class); + HttpRequest.Builder requestBuilderMock = mock(HttpRequest.Builder.class); + HttpRequest requestMock = mock(HttpRequest.class); + HttpResponse responseMock = spy(HttpResponse.class); + doReturn(HttpHeaders.of(new HashMap<>(), (a, b) -> true)).when(responseMock).headers(); + + when(requestBuilderMock.build()).thenReturn(requestMock); + when(httpClientMock.send(eq(requestMock), any(HttpResponse.BodyHandler.class))).thenReturn(responseMock); + when(responseMock.statusCode()).thenReturn(404); + + HttpCacheFetcher fetcher = new HttpCacheFetcher(); + HttpResponse result = fetcher.fetchContent(httpClientMock, requestBuilderMock); + + assertEquals(responseMock, result); + } + + @Test + public void test200ResponseNoEtagOrLastModified() throws Exception { + HttpClient httpClientMock = mock(HttpClient.class); + HttpRequest.Builder requestBuilderMock = mock(HttpRequest.Builder.class); + HttpRequest requestMock = mock(HttpRequest.class); + HttpResponse responseMock = spy(HttpResponse.class); + doReturn(HttpHeaders.of(new HashMap<>(), (a, b) -> true)).when(responseMock).headers(); + + when(requestBuilderMock.build()).thenReturn(requestMock); + when(httpClientMock.send(eq(requestMock), any(HttpResponse.BodyHandler.class))).thenReturn(responseMock); + when(responseMock.statusCode()).thenReturn(200); + + HttpCacheFetcher fetcher = new HttpCacheFetcher(); + fetcher.fetchContent(httpClientMock, requestBuilderMock); + + Field cachedETagField = HttpCacheFetcher.class.getDeclaredField("cachedETag"); + cachedETagField.setAccessible(true); + assertNull(cachedETagField.get(fetcher)); + Field cachedLastModifiedField = HttpCacheFetcher.class.getDeclaredField("cachedLastModified"); + cachedLastModifiedField.setAccessible(true); + assertNull(cachedLastModifiedField.get(fetcher)); + } + + @Test + public void testUpdateCacheOn200Response() throws Exception { + HttpClient httpClientMock = mock(HttpClient.class); + HttpRequest.Builder requestBuilderMock = mock(HttpRequest.Builder.class); + HttpRequest requestMock = mock(HttpRequest.class); + HttpResponse responseMock = spy(HttpResponse.class); + doReturn(HttpHeaders.of( + Map.of("Last-Modified", Arrays.asList("Wed, 21 Oct 2015 07:28:00 GMT"), + "ETag", Arrays.asList("etag-value")), + (a, b) -> true)).when(responseMock).headers(); + + when(requestBuilderMock.build()).thenReturn(requestMock); + when(httpClientMock.send(eq(requestMock), any(HttpResponse.BodyHandler.class))).thenReturn(responseMock); + when(responseMock.statusCode()).thenReturn(200); + HttpCacheFetcher fetcher = new HttpCacheFetcher(); + fetcher.fetchContent(httpClientMock, requestBuilderMock); + + Field cachedETagField = HttpCacheFetcher.class.getDeclaredField("cachedETag"); + cachedETagField.setAccessible(true); + assertEquals("etag-value", cachedETagField.get(fetcher)); + Field cachedLastModifiedField = HttpCacheFetcher.class.getDeclaredField("cachedLastModified"); + cachedLastModifiedField.setAccessible(true); + assertEquals("Wed, 21 Oct 2015 07:28:00 GMT", cachedLastModifiedField.get(fetcher)); + } + + @Test + public void testRequestWithCachedEtagIncludesIfNoneMatchHeader() throws Exception { + HttpClient httpClientMock = mock(HttpClient.class); + HttpRequest.Builder requestBuilderMock = mock(HttpRequest.Builder.class); + HttpRequest requestMock = mock(HttpRequest.class); + HttpResponse responseMock = spy(HttpResponse.class); + doReturn(HttpHeaders.of( + Map.of("ETag", Arrays.asList("12345")), + (a, b) -> true)).when(responseMock).headers(); + + when(requestBuilderMock.build()).thenReturn(requestMock); + when(httpClientMock.send(eq(requestMock), any(HttpResponse.BodyHandler.class))).thenReturn(responseMock); + when(responseMock.statusCode()).thenReturn(200); + + HttpCacheFetcher fetcher = new HttpCacheFetcher(); + fetcher.fetchContent(httpClientMock, requestBuilderMock); + fetcher.fetchContent(httpClientMock, requestBuilderMock); + + verify(requestBuilderMock, times(1)).header("If-None-Match", "12345"); + } + + @Test + public void testNullHttpClientOrRequestBuilder() { + HttpCacheFetcher fetcher = new HttpCacheFetcher(); + HttpRequest.Builder requestBuilderMock = mock(HttpRequest.Builder.class); + + assertThrows(NullPointerException.class, () -> { + fetcher.fetchContent(null, requestBuilderMock); + }); + + assertThrows(NullPointerException.class, () -> { + fetcher.fetchContent(mock(HttpClient.class), null); + }); + } + + @Test + public void testResponseWithUnexpectedStatusCode() throws Exception { + HttpClient httpClientMock = mock(HttpClient.class); + HttpRequest.Builder requestBuilderMock = mock(HttpRequest.Builder.class); + HttpRequest requestMock = mock(HttpRequest.class); + HttpResponse responseMock = spy(HttpResponse.class); + doReturn(HttpHeaders.of(new HashMap<>(), (a, b) -> true)).when(responseMock).headers(); + + when(requestBuilderMock.build()).thenReturn(requestMock); + when(httpClientMock.send(eq(requestMock), any(HttpResponse.BodyHandler.class))).thenReturn(responseMock); + when(responseMock.statusCode()).thenReturn(500); + + HttpCacheFetcher fetcher = new HttpCacheFetcher(); + HttpResponse response = fetcher.fetchContent(httpClientMock, requestBuilderMock); + + assertEquals(500, response.statusCode()); + verify(requestBuilderMock, never()).header(eq("If-None-Match"), anyString()); + verify(requestBuilderMock, never()).header(eq("If-Modified-Since"), anyString()); + } + + @Test + public void testRequestIncludesIfModifiedSinceHeaderWhenLastModifiedCached() throws Exception { + HttpClient httpClientMock = mock(HttpClient.class); + HttpRequest.Builder requestBuilderMock = mock(HttpRequest.Builder.class); + HttpRequest requestMock = mock(HttpRequest.class); + HttpResponse responseMock = spy(HttpResponse.class); + doReturn(HttpHeaders.of( + Map.of("Last-Modified", Arrays.asList("Wed, 21 Oct 2015 07:28:00 GMT")), + (a, b) -> true)).when(responseMock).headers(); + + when(requestBuilderMock.build()).thenReturn(requestMock); + when(httpClientMock.send(eq(requestMock), any(HttpResponse.BodyHandler.class))).thenReturn(responseMock); + when(responseMock.statusCode()).thenReturn(200); + + HttpCacheFetcher fetcher = new HttpCacheFetcher(); + fetcher.fetchContent(httpClientMock, requestBuilderMock); + fetcher.fetchContent(httpClientMock, requestBuilderMock); + + verify(requestBuilderMock).header(eq("If-Modified-Since"), eq("Wed, 21 Oct 2015 07:28:00 GMT")); + } + + @Test + public void testCalls200And304Responses() throws Exception { + HttpClient httpClientMock = mock(HttpClient.class); + HttpRequest.Builder requestBuilderMock = mock(HttpRequest.Builder.class); + HttpRequest requestMock = mock(HttpRequest.class); + HttpResponse responseMock200 = mock(HttpResponse.class); + HttpResponse responseMock304 = mock(HttpResponse.class); + + when(requestBuilderMock.build()).thenReturn(requestMock); + when(httpClientMock.send(eq(requestMock), any(HttpResponse.BodyHandler.class))) + .thenReturn(responseMock200) + .thenReturn(responseMock304); + when(responseMock200.statusCode()).thenReturn(200); + when(responseMock304.statusCode()).thenReturn(304); + + HttpCacheFetcher fetcher = new HttpCacheFetcher(); + fetcher.fetchContent(httpClientMock, requestBuilderMock); + fetcher.fetchContent(httpClientMock, requestBuilderMock); + + verify(responseMock200, times(1)).statusCode(); + verify(responseMock304, times(2)).statusCode(); + } + + @Test + public void testRequestIncludesBothEtagAndLastModifiedHeaders() throws Exception { + HttpClient httpClientMock = mock(HttpClient.class); + HttpRequest.Builder requestBuilderMock = mock(HttpRequest.Builder.class); + HttpRequest requestMock = mock(HttpRequest.class); + HttpResponse responseMock = spy(HttpResponse.class); + doReturn(HttpHeaders.of(new HashMap<>(), (a, b) -> true)).when(responseMock).headers(); + + when(requestBuilderMock.build()).thenReturn(requestMock); + when(httpClientMock.send(eq(requestMock), any(HttpResponse.BodyHandler.class))).thenReturn(responseMock); + when(responseMock.statusCode()).thenReturn(200); + + HttpCacheFetcher fetcher = new HttpCacheFetcher(); + Field cachedETagField = HttpCacheFetcher.class.getDeclaredField("cachedETag"); + cachedETagField.setAccessible(true); + cachedETagField.set(fetcher, "test-etag"); + Field cachedLastModifiedField = HttpCacheFetcher.class.getDeclaredField("cachedLastModified"); + cachedLastModifiedField.setAccessible(true); + cachedLastModifiedField.set(fetcher, "test-last-modified"); + + fetcher.fetchContent(httpClientMock, requestBuilderMock); + + verify(requestBuilderMock).header("If-None-Match", "test-etag"); + verify(requestBuilderMock).header("If-Modified-Since", "test-last-modified"); + } + + @SneakyThrows + @Test + public void testHttpClientSendExceptionPropagation() { + HttpClient httpClientMock = mock(HttpClient.class); + HttpRequest.Builder requestBuilderMock = mock(HttpRequest.Builder.class); + HttpRequest requestMock = mock(HttpRequest.class); + + when(requestBuilderMock.build()).thenReturn(requestMock); + when(httpClientMock.send(eq(requestMock), any(HttpResponse.BodyHandler.class))) + .thenThrow(new IOException("Network error")); + + HttpCacheFetcher fetcher = new HttpCacheFetcher(); + assertThrows(IOException.class, () -> { + fetcher.fetchContent(httpClientMock, requestBuilderMock); + }); + } + + @Test + public void testOnlyEtagAndLastModifiedHeadersCached() throws Exception { + HttpClient httpClientMock = mock(HttpClient.class); + HttpRequest.Builder requestBuilderMock = mock(HttpRequest.Builder.class); + HttpRequest requestMock = mock(HttpRequest.class); + HttpResponse responseMock = spy(HttpResponse.class); + doReturn(HttpHeaders.of( + Map.of("Last-Modified", Arrays.asList("last-modified-value"), + "ETag", Arrays.asList("etag-value")), + (a, b) -> true)).when(responseMock).headers(); + + when(requestBuilderMock.build()).thenReturn(requestMock); + when(httpClientMock.send(eq(requestMock), any(HttpResponse.BodyHandler.class))).thenReturn(responseMock); + when(responseMock.statusCode()).thenReturn(200); + + HttpCacheFetcher fetcher = new HttpCacheFetcher(); + fetcher.fetchContent(httpClientMock, requestBuilderMock); + + verify(requestBuilderMock, never()).header(eq("Some-Other-Header"), anyString()); + } + +} diff --git a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/connector/sync/HttpConnectorIntegrationTest.java b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/connector/sync/http/HttpConnectorIntegrationTest.java similarity index 65% rename from providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/connector/sync/HttpConnectorIntegrationTest.java rename to providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/connector/sync/http/HttpConnectorIntegrationTest.java index a870ff62a..e27a37b0c 100644 --- a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/connector/sync/HttpConnectorIntegrationTest.java +++ b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/connector/sync/http/HttpConnectorIntegrationTest.java @@ -1,39 +1,14 @@ -package dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync; +package dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http; -import static dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.HttpConnectorTest.delay; +import static dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpConnectorTest.delay; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assumptions.assumeTrue; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.doReturn; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.when; import dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.QueuePayload; -import dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.QueuePayloadType; -import java.io.IOException; -import java.lang.reflect.Field; -import java.net.MalformedURLException; -import java.net.ProxySelector; -import java.net.http.HttpClient; -import java.net.http.HttpRequest; -import java.net.http.HttpResponse; -import java.util.HashMap; -import java.util.Map; -import java.util.Optional; import java.util.concurrent.BlockingQueue; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.TimeUnit; import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; import org.junit.jupiter.api.Test; -import org.mockito.Mockito; /** * Integration test for the HttpConnector class, specifically testing the ability to fetch @@ -54,13 +29,17 @@ void testGithubRawContent() { HttpConnector connector = null; try { String testUrl = "https://raw.githubusercontent.com/open-feature/java-sdk-contrib/58fe5da7d4e2f6f4ae2c1caf3411a01e84a1dc1a/providers/flagd/version.txt"; - connector = HttpConnector.builder() + + HttpConnectorOptions httpConnectorOptions = HttpConnectorOptions.builder() .url(testUrl) .connectTimeoutSeconds(10) .requestTimeoutSeconds(10) .useHttpCache(true) .pollIntervalSeconds(5) .build(); + connector = HttpConnector.builder() + .httpConnectorOptions(httpConnectorOptions) + .build(); BlockingQueue queue = connector.getStreamQueue(); delay(20000); assertEquals(1, queue.size()); diff --git a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/connector/sync/http/HttpConnectorOptionsTest.java b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/connector/sync/http/HttpConnectorOptionsTest.java new file mode 100644 index 000000000..8b6f87b77 --- /dev/null +++ b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/connector/sync/http/HttpConnectorOptionsTest.java @@ -0,0 +1,406 @@ +package dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; + +import java.net.MalformedURLException; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import org.junit.Test; + +public class HttpConnectorOptionsTest { + + + @Test + public void testDefaultValuesInitialization() { + HttpConnectorOptions options = HttpConnectorOptions.builder() + .url("https://example.com") + .build(); + + assertEquals(60, options.getPollIntervalSeconds().intValue()); + assertEquals(10, options.getConnectTimeoutSeconds().intValue()); + assertEquals(10, options.getRequestTimeoutSeconds().intValue()); + assertEquals(100, options.getLinkedBlockingQueueCapacity().intValue()); + assertEquals(2, options.getScheduledThreadPoolSize().intValue()); + assertNotNull(options.getHeaders()); + assertTrue(options.getHeaders().isEmpty()); + assertNotNull(options.getHttpClientExecutor()); + assertNull(options.getProxyHost()); + assertNull(options.getProxyPort()); + assertNull(options.getPayloadCacheOptions()); + assertNull(options.getPayloadCache()); + assertNull(options.getUseHttpCache()); + assertEquals("https://example.com", options.getUrl()); + } + + @Test + public void testInvalidUrlFormat() { + MalformedURLException exception = assertThrows( + MalformedURLException.class, + () -> HttpConnectorOptions.builder() + .url("invalid-url") + .build() + ); + + assertNotNull(exception); + } + + @Test + public void testCustomValuesInitialization() { + HttpConnectorOptions options = HttpConnectorOptions.builder() + .pollIntervalSeconds(120) + .connectTimeoutSeconds(20) + .requestTimeoutSeconds(30) + .linkedBlockingQueueCapacity(200) + .scheduledThreadPoolSize(5) + .url("http://example.com") + .build(); + + assertEquals(120, options.getPollIntervalSeconds().intValue()); + assertEquals(20, options.getConnectTimeoutSeconds().intValue()); + assertEquals(30, options.getRequestTimeoutSeconds().intValue()); + assertEquals(200, options.getLinkedBlockingQueueCapacity().intValue()); + assertEquals(5, options.getScheduledThreadPoolSize().intValue()); + assertEquals("http://example.com", options.getUrl()); + } + + @Test + public void testCustomHeadersMap() { + Map customHeaders = new HashMap<>(); + customHeaders.put("Authorization", "Bearer token"); + customHeaders.put("Content-Type", "application/json"); + + HttpConnectorOptions options = HttpConnectorOptions.builder() + .url("http://example.com") + .headers(customHeaders) + .build(); + + assertEquals("Bearer token", options.getHeaders().get("Authorization")); + assertEquals("application/json", options.getHeaders().get("Content-Type")); + } + + @Test + public void testCustomExecutorService() { + ExecutorService customExecutor = Executors.newFixedThreadPool(5); + HttpConnectorOptions options = HttpConnectorOptions.builder() + .url("https://example.com") + .httpClientExecutor(customExecutor) + .build(); + + assertEquals(customExecutor, options.getHttpClientExecutor()); + } + + @Test + public void testSettingPayloadCacheWithValidOptions() { + PayloadCacheOptions cacheOptions = PayloadCacheOptions.builder() + .updateIntervalSeconds(1800) + .build(); + PayloadCache payloadCache = new PayloadCache() { + private String payload; + + @Override + public void put(String payload) { + this.payload = payload; + } + + @Override + public String get() { + return this.payload; + } + }; + + HttpConnectorOptions options = HttpConnectorOptions.builder() + .url("https://example.com") + .payloadCacheOptions(cacheOptions) + .payloadCache(payloadCache) + .build(); + + assertNotNull(options.getPayloadCacheOptions()); + assertNotNull(options.getPayloadCache()); + assertEquals(1800, options.getPayloadCacheOptions().getUpdateIntervalSeconds()); + } + + @Test + public void testProxyConfigurationWithValidHostAndPort() { + HttpConnectorOptions options = HttpConnectorOptions.builder() + .url("https://example.com") + .proxyHost("proxy.example.com") + .proxyPort(8080) + .build(); + + assertEquals("proxy.example.com", options.getProxyHost()); + assertEquals(8080, options.getProxyPort().intValue()); + } + + @Test + public void testLinkedBlockingQueueCapacityOutOfRange() { + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> { + HttpConnectorOptions.builder() + .url("https://example.com") + .linkedBlockingQueueCapacity(0) + .build(); + }); + assertEquals("linkedBlockingQueueCapacity must be between 1 and 1000", exception.getMessage()); + + exception = assertThrows(IllegalArgumentException.class, () -> { + HttpConnectorOptions.builder() + .url("https://example.com") + .linkedBlockingQueueCapacity(1001) + .build(); + }); + assertEquals("linkedBlockingQueueCapacity must be between 1 and 1000", exception.getMessage()); + } + + @Test + public void testPollIntervalSecondsOutOfRange() { + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> { + HttpConnectorOptions.builder() + .url("https://example.com") + .pollIntervalSeconds(700) + .build(); + }); + assertEquals("pollIntervalSeconds must be between 1 and 600", exception.getMessage()); + } + + @Test + public void testAdditionalCustomValuesInitialization() { + Map headers = new HashMap<>(); + headers.put("Authorization", "Bearer token"); + ExecutorService executorService = Executors.newFixedThreadPool(2); + PayloadCacheOptions cacheOptions = PayloadCacheOptions.builder().build(); + PayloadCache cache = new PayloadCache() { + @Override + public void put(String payload) { + // do nothing + } + @Override + public String get() { return null; } + }; + + HttpConnectorOptions options = HttpConnectorOptions.builder() + .url("https://example.com") + .pollIntervalSeconds(120) + .connectTimeoutSeconds(20) + .requestTimeoutSeconds(30) + .linkedBlockingQueueCapacity(200) + .scheduledThreadPoolSize(4) + .headers(headers) + .httpClientExecutor(executorService) + .proxyHost("proxy.example.com") + .proxyPort(8080) + .payloadCacheOptions(cacheOptions) + .payloadCache(cache) + .useHttpCache(true) + .build(); + + assertEquals(120, options.getPollIntervalSeconds().intValue()); + assertEquals(20, options.getConnectTimeoutSeconds().intValue()); + assertEquals(30, options.getRequestTimeoutSeconds().intValue()); + assertEquals(200, options.getLinkedBlockingQueueCapacity().intValue()); + assertEquals(4, options.getScheduledThreadPoolSize().intValue()); + assertNotNull(options.getHeaders()); + assertEquals("Bearer token", options.getHeaders().get("Authorization")); + assertNotNull(options.getHttpClientExecutor()); + assertEquals("proxy.example.com", options.getProxyHost()); + assertEquals(8080, options.getProxyPort().intValue()); + assertNotNull(options.getPayloadCacheOptions()); + assertNotNull(options.getPayloadCache()); + assertTrue(options.getUseHttpCache()); + assertEquals("https://example.com", options.getUrl()); + } + + @Test + public void testRequestTimeoutSecondsOutOfRange() { + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> { + HttpConnectorOptions.builder() + .url("https://example.com") + .requestTimeoutSeconds(61) + .build(); + }); + assertEquals("requestTimeoutSeconds must be between 1 and 60", exception.getMessage()); + } + + @Test + public void testBuilderInitializesAllFields() { + Map headers = new HashMap<>(); + headers.put("Authorization", "Bearer token"); + ExecutorService executorService = Executors.newFixedThreadPool(2); + PayloadCacheOptions cacheOptions = PayloadCacheOptions.builder().build(); + PayloadCache cache = new PayloadCache() { + @Override + public void put(String payload) { + // do nothing + } + @Override + public String get() { return null; } + }; + + HttpConnectorOptions options = HttpConnectorOptions.builder() + .pollIntervalSeconds(120) + .connectTimeoutSeconds(20) + .requestTimeoutSeconds(30) + .linkedBlockingQueueCapacity(200) + .scheduledThreadPoolSize(4) + .headers(headers) + .httpClientExecutor(executorService) + .proxyHost("proxy.example.com") + .proxyPort(8080) + .payloadCacheOptions(cacheOptions) + .payloadCache(cache) + .useHttpCache(true) + .url("https://example.com") + .build(); + + assertEquals(120, options.getPollIntervalSeconds().intValue()); + assertEquals(20, options.getConnectTimeoutSeconds().intValue()); + assertEquals(30, options.getRequestTimeoutSeconds().intValue()); + assertEquals(200, options.getLinkedBlockingQueueCapacity().intValue()); + assertEquals(4, options.getScheduledThreadPoolSize().intValue()); + assertEquals(headers, options.getHeaders()); + assertEquals(executorService, options.getHttpClientExecutor()); + assertEquals("proxy.example.com", options.getProxyHost()); + assertEquals(8080, options.getProxyPort().intValue()); + assertEquals(cacheOptions, options.getPayloadCacheOptions()); + assertEquals(cache, options.getPayloadCache()); + assertTrue(options.getUseHttpCache()); + assertEquals("https://example.com", options.getUrl()); + } + + @Test + public void testScheduledThreadPoolSizeOutOfRange() { + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> { + HttpConnectorOptions.builder() + .url("https://example.com") + .scheduledThreadPoolSize(11) + .build(); + }); + assertEquals("scheduledThreadPoolSize must be between 1 and 10", exception.getMessage()); + } + + @Test + public void testProxyPortOutOfRange() { + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> { + HttpConnectorOptions.builder() + .url("https://example.com") + .proxyHost("proxy.example.com") + .proxyPort(70000) // Invalid port, out of range + .build(); + }); + assertEquals("proxyPort must be between 1 and 65535", exception.getMessage()); + } + + @Test + public void testConnectTimeoutSecondsOutOfRange() { + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> { + HttpConnectorOptions.builder() + .url("https://example.com") + .connectTimeoutSeconds(0) + .build(); + }); + assertEquals("connectTimeoutSeconds must be between 1 and 60", exception.getMessage()); + + exception = assertThrows(IllegalArgumentException.class, () -> { + HttpConnectorOptions.builder() + .url("https://example.com") + .connectTimeoutSeconds(61) + .build(); + }); + assertEquals("connectTimeoutSeconds must be between 1 and 60", exception.getMessage()); + } + + @Test + public void testProxyPortWithoutProxyHost() { + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> { + HttpConnectorOptions.builder() + .url("https://example.com") + .proxyPort(8080) + .build(); + }); + assertEquals("proxyHost must be set if proxyPort is set", exception.getMessage()); + } + + @Test + public void testDefaultValuesWhenNullParametersProvided() { + HttpConnectorOptions options = HttpConnectorOptions.builder() + .url("https://example.com") + .pollIntervalSeconds(null) + .linkedBlockingQueueCapacity(null) + .scheduledThreadPoolSize(null) + .requestTimeoutSeconds(null) + .connectTimeoutSeconds(null) + .headers(null) + .httpClientExecutor(null) + .proxyHost(null) + .proxyPort(null) + .payloadCacheOptions(null) + .payloadCache(null) + .useHttpCache(null) + .build(); + + assertEquals(60, options.getPollIntervalSeconds().intValue()); + assertEquals(10, options.getConnectTimeoutSeconds().intValue()); + assertEquals(10, options.getRequestTimeoutSeconds().intValue()); + assertEquals(100, options.getLinkedBlockingQueueCapacity().intValue()); + assertEquals(2, options.getScheduledThreadPoolSize().intValue()); + assertNotNull(options.getHeaders()); + assertTrue(options.getHeaders().isEmpty()); + assertNotNull(options.getHttpClientExecutor()); + assertNull(options.getProxyHost()); + assertNull(options.getProxyPort()); + assertNull(options.getPayloadCacheOptions()); + assertNull(options.getPayloadCache()); + assertNull(options.getUseHttpCache()); + assertEquals("https://example.com", options.getUrl()); + } + + @Test + public void testProxyHostWithoutProxyPort() { + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> { + HttpConnectorOptions.builder() + .url("https://example.com") + .proxyHost("proxy.example.com") + .build(); + }); + assertEquals("proxyPort must be set if proxyHost is set", exception.getMessage()); + } + + @Test + public void testSettingPayloadCacheWithoutOptions() { + PayloadCache mockPayloadCache = new PayloadCache() { + @Override + public void put(String payload) { + // Mock implementation + } + + @Override + public String get() { + return "mockPayload"; + } + }; + + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> { + HttpConnectorOptions.builder() + .url("https://example.com") + .payloadCache(mockPayloadCache) + .build(); + }); + + assertEquals("payloadCacheOptions must be set if payloadCache is set", exception.getMessage()); + } + + @Test + public void testPayloadCacheOptionsWithoutPayloadCache() { + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> { + HttpConnectorOptions.builder() + .url("https://example.com") + .payloadCacheOptions(PayloadCacheOptions.builder().build()) + .build(); + }); + assertEquals("payloadCache must be set if payloadCacheOptions is set", exception.getMessage()); + } +} diff --git a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/connector/sync/HttpConnectorTest.java b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/connector/sync/http/HttpConnectorTest.java similarity index 57% rename from providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/connector/sync/HttpConnectorTest.java rename to providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/connector/sync/http/HttpConnectorTest.java index 910fa3480..a62bc8ecd 100644 --- a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/connector/sync/HttpConnectorTest.java +++ b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/connector/sync/http/HttpConnectorTest.java @@ -1,9 +1,8 @@ -package dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync; +package dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.doReturn; @@ -15,14 +14,11 @@ import dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.QueuePayloadType; import java.io.IOException; import java.lang.reflect.Field; -import java.net.MalformedURLException; -import java.net.ProxySelector; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.util.HashMap; import java.util.Map; -import java.util.Optional; import java.util.concurrent.BlockingQueue; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; @@ -36,76 +32,6 @@ @Slf4j class HttpConnectorTest { - @SneakyThrows - @Test - void testConstructorInitializesDefaultValues() { - String testUrl = "http://example.com"; - HttpConnector connector = HttpConnector.builder() - .url(testUrl) - .build(); - - Field pollIntervalField = HttpConnector.class.getDeclaredField("pollIntervalSeconds"); - pollIntervalField.setAccessible(true); - assertEquals(60, pollIntervalField.get(connector)); - - Field requestTimeoutField = HttpConnector.class.getDeclaredField("requestTimeoutSeconds"); - requestTimeoutField.setAccessible(true); - assertEquals(10, requestTimeoutField.get(connector)); - - Field queueField = HttpConnector.class.getDeclaredField("queue"); - queueField.setAccessible(true); - BlockingQueue queue = (BlockingQueue) queueField.get(connector); - assertEquals(100, queue.remainingCapacity() + queue.size()); - - Field headersField = HttpConnector.class.getDeclaredField("headers"); - headersField.setAccessible(true); - Map headers = (Map) headersField.get(connector); - assertNotNull(headers); - assertTrue(headers.isEmpty()); - } - - @SneakyThrows - @Test - void testConstructorValidationRejectsInvalidParameters() { - String testUrl = "http://example.com"; - - HttpConnector.HttpConnectorBuilder builder3 = HttpConnector.builder() - .url(testUrl) - .pollIntervalSeconds(0); - IllegalArgumentException pollIntervalException = assertThrows( - IllegalArgumentException.class, - builder3::build - ); - assertEquals("pollIntervalSeconds must be between 1 and 600", pollIntervalException.getMessage()); - - HttpConnector.HttpConnectorBuilder builder2 = HttpConnector.builder() - .url(testUrl) - .linkedBlockingQueueCapacity(1001); - IllegalArgumentException queueCapacityException = assertThrows( - IllegalArgumentException.class, - builder2::build - ); - assertEquals("linkedBlockingQueueCapacity must be between 1 and 1000", queueCapacityException.getMessage()); - - HttpConnector.HttpConnectorBuilder builder1 = HttpConnector.builder() - .url(testUrl) - .scheduledThreadPoolSize(11); - IllegalArgumentException threadPoolException = assertThrows( - IllegalArgumentException.class, - builder1::build - ); - assertEquals("scheduledThreadPoolSize must be between 1 and 10", threadPoolException.getMessage()); - - HttpConnector.HttpConnectorBuilder builder = HttpConnector.builder() - .url(testUrl) - .proxyHost("localhost"); - IllegalArgumentException proxyException = assertThrows( - IllegalArgumentException.class, - builder::build - ); - assertEquals("proxyPort must be set if proxyHost is set", proxyException.getMessage()); - } - @SneakyThrows @Test void testGetStreamQueueInitialAndScheduledPolls() { @@ -117,10 +43,13 @@ void testGetStreamQueueInitialAndScheduledPolls() { when(mockClient.send(any(HttpRequest.class), any(HttpResponse.BodyHandler.class))) .thenReturn(mockResponse); - HttpConnector connector = HttpConnector.builder() + HttpConnectorOptions httpConnectorOptions = HttpConnectorOptions.builder() .url(testUrl) .httpClientExecutor(Executors.newSingleThreadExecutor()) .build(); + HttpConnector connector = HttpConnector.builder() + .httpConnectorOptions(httpConnectorOptions) + .build(); Field clientField = HttpConnector.class.getDeclaredField("client"); clientField.setAccessible(true); @@ -148,10 +77,30 @@ void testBuildPollTaskFetchesDataAndAddsToQueue() { when(mockClient.send(any(HttpRequest.class), any(HttpResponse.BodyHandler.class))) .thenReturn(mockResponse); - HttpConnector connector = HttpConnector.builder() + PayloadCache payloadCache = new PayloadCache() { + private String payload; + @Override + public void put(String payload) { + this.payload = payload; + } + + @Override + public String get() { + return payload; + } + }; + HttpConnectorOptions httpConnectorOptions = HttpConnectorOptions.builder() .url(testUrl) - .httpClientExecutor(Executors.newFixedThreadPool(1)) + .proxyHost("proxy-host") + .proxyPort(8080) + .useHttpCache(true) + .payloadCache(payloadCache) + .payloadCacheOptions(PayloadCacheOptions.builder().build()) + .build(); + HttpConnector connector = HttpConnector.builder() + .httpConnectorOptions(httpConnectorOptions) .build(); + connector.init(); Field clientField = HttpConnector.class.getDeclaredField("client"); clientField.setAccessible(true); @@ -178,10 +127,13 @@ void testHttpRequestIncludesHeaders() { testHeaders.put("Authorization", "Bearer token"); testHeaders.put("Content-Type", "application/json"); - HttpConnector connector = HttpConnector.builder() + HttpConnectorOptions httpConnectorOptions = HttpConnectorOptions.builder() .url(testUrl) .headers(testHeaders) .build(); + HttpConnector connector = HttpConnector.builder() + .httpConnectorOptions(httpConnectorOptions) + .build(); Field headersField = HttpConnector.class.getDeclaredField("headers"); headersField.setAccessible(true); @@ -192,57 +144,6 @@ void testHttpRequestIncludesHeaders() { assertEquals("application/json", headers.get("Content-Type")); } - @SneakyThrows - @Test - void testConstructorInitializesWithProvidedValues() { - Integer pollIntervalSeconds = 120; - Integer linkedBlockingQueueCapacity = 200; - Integer scheduledThreadPoolSize = 2; - Integer requestTimeoutSeconds = 20; - Integer connectTimeoutSeconds = 15; - String url = "http://example.com"; - Map headers = new HashMap<>(); - headers.put("Authorization", "Bearer token"); - ExecutorService httpClientExecutor = Executors.newFixedThreadPool(2); - String proxyHost = "proxy.example.com"; - Integer proxyPort = 8080; - - HttpConnector connector = HttpConnector.builder() - .pollIntervalSeconds(pollIntervalSeconds) - .linkedBlockingQueueCapacity(linkedBlockingQueueCapacity) - .scheduledThreadPoolSize(scheduledThreadPoolSize) - .requestTimeoutSeconds(requestTimeoutSeconds) - .connectTimeoutSeconds(connectTimeoutSeconds) - .url(url) - .headers(headers) - .httpClientExecutor(httpClientExecutor) - .proxyHost(proxyHost) - .proxyPort(proxyPort) - .build(); - - Field pollIntervalField = HttpConnector.class.getDeclaredField("pollIntervalSeconds"); - pollIntervalField.setAccessible(true); - assertEquals(pollIntervalSeconds, pollIntervalField.get(connector)); - - Field requestTimeoutField = HttpConnector.class.getDeclaredField("requestTimeoutSeconds"); - requestTimeoutField.setAccessible(true); - assertEquals(requestTimeoutSeconds, requestTimeoutField.get(connector)); - - Field queueField = HttpConnector.class.getDeclaredField("queue"); - queueField.setAccessible(true); - BlockingQueue queue = (BlockingQueue) queueField.get(connector); - assertEquals(linkedBlockingQueueCapacity, queue.remainingCapacity() + queue.size()); - - Field headersField = HttpConnector.class.getDeclaredField("headers"); - headersField.setAccessible(true); - Map actualHeaders = (Map) headersField.get(connector); - assertEquals(headers, actualHeaders); - - Field urlField = HttpConnector.class.getDeclaredField("url"); - urlField.setAccessible(true); - assertEquals(url, urlField.get(connector)); - } - @SneakyThrows @Test void testSuccessfulHttpResponseAddsDataToQueue() { @@ -254,9 +155,11 @@ void testSuccessfulHttpResponseAddsDataToQueue() { when(mockClient.send(any(HttpRequest.class), any(HttpResponse.BodyHandler.class))) .thenReturn(mockResponse); - HttpConnector connector = HttpConnector.builder() + HttpConnectorOptions httpConnectorOptions = HttpConnectorOptions.builder() .url(testUrl) - .httpClientExecutor(Executors.newSingleThreadExecutor()) + .build(); + HttpConnector connector = HttpConnector.builder() + .httpConnectorOptions(httpConnectorOptions) .build(); Field clientField = HttpConnector.class.getDeclaredField("client"); @@ -295,11 +198,13 @@ public String get() { } }; - HttpConnector connector = HttpConnector.builder() + HttpConnectorOptions httpConnectorOptions = HttpConnectorOptions.builder() .url(testUrl) - .httpClientExecutor(Executors.newSingleThreadExecutor()) - .payloadCache(payloadCache) - .payloadCacheOptions(PayloadCacheOptions.builder().build()) + .payloadCache(payloadCache) + .payloadCacheOptions(PayloadCacheOptions.builder().build()) + .build(); + HttpConnector connector = HttpConnector.builder() + .httpConnectorOptions(httpConnectorOptions) .build(); Field clientField = HttpConnector.class.getDeclaredField("client"); @@ -320,10 +225,13 @@ public String get() { void testQueueBecomesFull() { String testUrl = "http://example.com"; int queueCapacity = 1; + HttpConnectorOptions httpConnectorOptions = HttpConnectorOptions.builder() + .url(testUrl) + .linkedBlockingQueueCapacity(queueCapacity) + .build(); HttpConnector connector = HttpConnector.builder() - .url(testUrl) - .linkedBlockingQueueCapacity(queueCapacity) - .build(); + .httpConnectorOptions(httpConnectorOptions) + .build(); BlockingQueue queue = connector.getStreamQueue(); @@ -341,10 +249,13 @@ void testShutdownProperlyTerminatesSchedulerAndHttpClientExecutor() throws Inter ScheduledExecutorService mockScheduler = mock(ScheduledExecutorService.class); String testUrl = "http://example.com"; - HttpConnector connector = HttpConnector.builder() + HttpConnectorOptions httpConnectorOptions = HttpConnectorOptions.builder() .url(testUrl) .httpClientExecutor(mockHttpClientExecutor) .build(); + HttpConnector connector = HttpConnector.builder() + .httpConnectorOptions(httpConnectorOptions) + .build(); Field schedulerField = HttpConnector.class.getDeclaredField("scheduler"); schedulerField.setAccessible(true); @@ -366,9 +277,11 @@ void testHttpResponseNonSuccessStatusCode() { when(mockClient.send(any(HttpRequest.class), any(HttpResponse.BodyHandler.class))) .thenReturn(mockResponse); - HttpConnector connector = HttpConnector.builder() + HttpConnectorOptions httpConnectorOptions = HttpConnectorOptions.builder() .url(testUrl) - .httpClientExecutor(Executors.newSingleThreadExecutor()) + .build(); + HttpConnector connector = HttpConnector.builder() + .httpConnectorOptions(httpConnectorOptions) .build(); Field clientField = HttpConnector.class.getDeclaredField("client"); @@ -380,45 +293,16 @@ void testHttpResponseNonSuccessStatusCode() { assertTrue(queue.isEmpty(), "Queue should be empty when response status is non-200"); } - @SneakyThrows - @Test - void test_constructor_handles_proxy_configuration() { - String testUrl = "http://example.com"; - String proxyHost = "proxy.example.com"; - int proxyPort = 8080; - - HttpConnector connectorWithProxy = HttpConnector.builder() - .url(testUrl) - .proxyHost(proxyHost) - .proxyPort(proxyPort) - .build(); - - HttpConnector connectorWithoutProxy = HttpConnector.builder() - .url(testUrl) - .build(); - - Field clientFieldWithProxy = HttpConnector.class.getDeclaredField("client"); - clientFieldWithProxy.setAccessible(true); - HttpClient clientWithProxy = (HttpClient) clientFieldWithProxy.get(connectorWithProxy); - assertNotNull(clientWithProxy); - - Field clientFieldWithoutProxy = HttpConnector.class.getDeclaredField("client"); - clientFieldWithoutProxy.setAccessible(true); - HttpClient clientWithoutProxy = (HttpClient) clientFieldWithoutProxy.get(connectorWithoutProxy); - assertNotNull(clientWithoutProxy); - - Optional proxySelectorWithProxy = clientWithProxy.proxy(); - assertNotNull(proxySelectorWithProxy.get()); - } - @SneakyThrows @Test void testHttpRequestFailsWithException() { String testUrl = "http://example.com"; HttpClient mockClient = mock(HttpClient.class); - HttpConnector connector = HttpConnector.builder() + HttpConnectorOptions httpConnectorOptions = HttpConnectorOptions.builder() .url(testUrl) - .httpClientExecutor(Executors.newSingleThreadExecutor()) + .build(); + HttpConnector connector = HttpConnector.builder() + .httpConnectorOptions(httpConnectorOptions) .build(); Field clientField = HttpConnector.class.getDeclaredField("client"); @@ -438,9 +322,11 @@ void testHttpRequestFailsWithException() { void testHttpRequestFailsWithIoexception() { String testUrl = "http://example.com"; HttpClient mockClient = mock(HttpClient.class); - HttpConnector connector = HttpConnector.builder() + HttpConnectorOptions httpConnectorOptions = HttpConnectorOptions.builder() .url(testUrl) - .httpClientExecutor(Executors.newSingleThreadExecutor()) + .build(); + HttpConnector connector = HttpConnector.builder() + .httpConnectorOptions(httpConnectorOptions) .build(); Field clientField = HttpConnector.class.getDeclaredField("client"); @@ -458,35 +344,6 @@ void testHttpRequestFailsWithIoexception() { assertTrue(queue.isEmpty(), "Queue should be empty due to IOException"); } - @SneakyThrows - @Test - void testMalformedUrlThrowsException() { - String malformedUrl = "htp://invalid-url"; - - assertThrows(MalformedURLException.class, () -> { - HttpConnector.builder() - .url(malformedUrl) - .build(); - }); - } - - @SneakyThrows - @Test - void testHeadersInitializationWhenNull() { - String testUrl = "http://example.com"; - - HttpConnector connector = HttpConnector.builder() - .url(testUrl) - .headers(null) - .build(); - - Field headersField = HttpConnector.class.getDeclaredField("headers"); - headersField.setAccessible(true); - Map headers = (Map) headersField.get(connector); - assertNotNull(headers); - assertTrue(headers.isEmpty()); - } - @SneakyThrows @Test void testScheduledPollingContinuesAtFixedIntervals() { @@ -495,9 +352,11 @@ void testScheduledPollingContinuesAtFixedIntervals() { when(mockResponse.statusCode()).thenReturn(200); when(mockResponse.body()).thenReturn("test data"); - HttpConnector connector = spy(HttpConnector.builder() + HttpConnectorOptions httpConnectorOptions = HttpConnectorOptions.builder() .url(testUrl) - .httpClientExecutor(Executors.newSingleThreadExecutor()) + .build(); + HttpConnector connector = spy(HttpConnector.builder() + .httpConnectorOptions(httpConnectorOptions) .build()); doReturn(mockResponse).when(connector).execute(any()); @@ -514,56 +373,23 @@ void testScheduledPollingContinuesAtFixedIntervals() { connector.shutdown(); } - @SneakyThrows - @Test - void testDefaultValuesWhenOptionalParametersAreNull() { - String testUrl = "http://example.com"; - - HttpConnector connector = HttpConnector.builder() - .url(testUrl) - .build(); - - Field pollIntervalField = HttpConnector.class.getDeclaredField("pollIntervalSeconds"); - pollIntervalField.setAccessible(true); - assertEquals(60, pollIntervalField.get(connector)); - - Field requestTimeoutField = HttpConnector.class.getDeclaredField("requestTimeoutSeconds"); - requestTimeoutField.setAccessible(true); - assertEquals(10, requestTimeoutField.get(connector)); - - Field queueField = HttpConnector.class.getDeclaredField("queue"); - queueField.setAccessible(true); - BlockingQueue queue = (BlockingQueue) queueField.get(connector); - assertEquals(100, queue.remainingCapacity() + queue.size()); - - Field headersField = HttpConnector.class.getDeclaredField("headers"); - headersField.setAccessible(true); - Map headers = (Map) headersField.get(connector); - assertNotNull(headers); - assertTrue(headers.isEmpty()); - - Field httpClientExecutorField = HttpConnector.class.getDeclaredField("httpClientExecutor"); - httpClientExecutorField.setAccessible(true); - ExecutorService httpClientExecutor = (ExecutorService) httpClientExecutorField.get(connector); - assertNotNull(httpClientExecutor); - } - @SneakyThrows @Test void testQueuePayloadTypeSetToDataOnSuccess() { String testUrl = "http://example.com"; HttpClient mockClient = mock(HttpClient.class); HttpResponse mockResponse = mock(HttpResponse.class); - ExecutorService mockExecutor = Executors.newFixedThreadPool(1); when(mockResponse.statusCode()).thenReturn(200); when(mockResponse.body()).thenReturn("response body"); when(mockClient.send(any(HttpRequest.class), any(HttpResponse.BodyHandler.class))) .thenReturn(mockResponse); - HttpConnector connector = HttpConnector.builder() + HttpConnectorOptions httpConnectorOptions = HttpConnectorOptions.builder() .url(testUrl) - .httpClientExecutor(mockExecutor) + .build(); + HttpConnector connector = HttpConnector.builder() + .httpConnectorOptions(httpConnectorOptions) .build(); Field clientField = HttpConnector.class.getDeclaredField("client"); diff --git a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/connector/sync/http/PayloadCacheWrapperTest.java b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/connector/sync/http/PayloadCacheWrapperTest.java new file mode 100644 index 000000000..65f92e0ae --- /dev/null +++ b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/connector/sync/http/PayloadCacheWrapperTest.java @@ -0,0 +1,267 @@ +package dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http; + +import static dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpConnectorTest.delay; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.lang.reflect.Field; +import lombok.SneakyThrows; +import org.junit.jupiter.api.Test; + + +public class PayloadCacheWrapperTest { + + @Test + public void testConstructorInitializesWithValidParameters() { + PayloadCache mockCache = mock(PayloadCache.class); + PayloadCacheOptions options = PayloadCacheOptions.builder() + .updateIntervalSeconds(600) + .build(); + + PayloadCacheWrapper wrapper = PayloadCacheWrapper.builder() + .payloadCache(mockCache) + .payloadCacheOptions(options) + .build(); + + assertNotNull(wrapper); + + String testPayload = "test-payload"; + wrapper.updatePayloadIfNeeded(testPayload); + wrapper.get(); + + verify(mockCache).put(testPayload); + verify(mockCache).get(); + } + + @Test + public void testConstructorThrowsExceptionForInvalidInterval() { + PayloadCache mockCache = mock(PayloadCache.class); + PayloadCacheOptions options = PayloadCacheOptions.builder() + .updateIntervalSeconds(0) + .build(); + + PayloadCacheWrapper.PayloadCacheWrapperBuilder payloadCacheWrapperBuilder = PayloadCacheWrapper.builder() + .payloadCache(mockCache) + .payloadCacheOptions(options); + IllegalArgumentException exception = assertThrows( + IllegalArgumentException.class, + payloadCacheWrapperBuilder::build + ); + + assertEquals("pollIntervalSeconds must be larger than 0", exception.getMessage()); + } + + @Test + public void testUpdateSkipsWhenIntervalNotPassed() { + PayloadCache mockCache = mock(PayloadCache.class); + PayloadCacheOptions options = PayloadCacheOptions.builder() + .updateIntervalSeconds(600) + .build(); + PayloadCacheWrapper wrapper = PayloadCacheWrapper.builder() + .payloadCache(mockCache) + .payloadCacheOptions(options) + .build(); + + String initialPayload = "initial-payload"; + wrapper.updatePayloadIfNeeded(initialPayload); + + String newPayload = "new-payload"; + wrapper.updatePayloadIfNeeded(newPayload); + + verify(mockCache, times(1)).put(initialPayload); + verify(mockCache, never()).put(newPayload); + } + + @Test + public void testUpdatePayloadIfNeededHandlesPutException() { + PayloadCache mockCache = mock(PayloadCache.class); + PayloadCacheOptions options = PayloadCacheOptions.builder() + .updateIntervalSeconds(600) + .build(); + PayloadCacheWrapper wrapper = PayloadCacheWrapper.builder() + .payloadCache(mockCache) + .payloadCacheOptions(options) + .build(); + String testPayload = "test-payload"; + + doThrow(new RuntimeException("put exception")).when(mockCache).put(testPayload); + + wrapper.updatePayloadIfNeeded(testPayload); + + verify(mockCache).put(testPayload); + } + + @Test + public void testUpdatePayloadIfNeededUpdatesCacheAfterInterval() { + PayloadCache mockCache = mock(PayloadCache.class); + PayloadCacheOptions options = PayloadCacheOptions.builder() + .updateIntervalSeconds(1) // 1 second interval for quick test + .build(); + PayloadCacheWrapper wrapper = PayloadCacheWrapper.builder() + .payloadCache(mockCache) + .payloadCacheOptions(options) + .build(); + + String initialPayload = "initial-payload"; + String newPayload = "new-payload"; + + wrapper.updatePayloadIfNeeded(initialPayload); + delay(1100); + wrapper.updatePayloadIfNeeded(newPayload); + + verify(mockCache).put(initialPayload); + verify(mockCache).put(newPayload); + } + + @Test + public void testGetReturnsNullWhenCacheGetThrowsException() { + PayloadCache mockCache = mock(PayloadCache.class); + PayloadCacheOptions options = PayloadCacheOptions.builder() + .updateIntervalSeconds(600) + .build(); + PayloadCacheWrapper wrapper = PayloadCacheWrapper.builder() + .payloadCache(mockCache) + .payloadCacheOptions(options) + .build(); + + when(mockCache.get()).thenThrow(new RuntimeException("Cache get failed")); + + String result = wrapper.get(); + + assertNull(result); + + verify(mockCache).get(); + } + + @Test + public void test_get_returns_cached_payload() { + PayloadCache mockCache = mock(PayloadCache.class); + PayloadCacheOptions options = PayloadCacheOptions.builder() + .updateIntervalSeconds(600) + .build(); + PayloadCacheWrapper wrapper = PayloadCacheWrapper.builder() + .payloadCache(mockCache) + .payloadCacheOptions(options) + .build(); + String expectedPayload = "cached-payload"; + when(mockCache.get()).thenReturn(expectedPayload); + + String actualPayload = wrapper.get(); + + assertEquals(expectedPayload, actualPayload); + + verify(mockCache).get(); + } + + @Test + public void test_first_call_updates_cache() { + PayloadCache mockCache = mock(PayloadCache.class); + PayloadCacheOptions options = PayloadCacheOptions.builder() + .updateIntervalSeconds(600) + .build(); + PayloadCacheWrapper wrapper = PayloadCacheWrapper.builder() + .payloadCache(mockCache) + .payloadCacheOptions(options) + .build(); + + String testPayload = "initial-payload"; + + wrapper.updatePayloadIfNeeded(testPayload); + + verify(mockCache).put(testPayload); + } + + @Test + public void test_update_payload_once_within_interval() { + PayloadCache mockCache = mock(PayloadCache.class); + PayloadCacheOptions options = PayloadCacheOptions.builder() + .updateIntervalSeconds(1) // 1 second interval + .build(); + PayloadCacheWrapper wrapper = PayloadCacheWrapper.builder() + .payloadCache(mockCache) + .payloadCacheOptions(options) + .build(); + + String testPayload = "test-payload"; + + wrapper.updatePayloadIfNeeded(testPayload); + wrapper.updatePayloadIfNeeded(testPayload); + + verify(mockCache, times(1)).put(testPayload); + } + + @SneakyThrows + @Test + public void test_last_update_time_ms_updated_after_successful_cache_update() { + PayloadCache mockCache = mock(PayloadCache.class); + PayloadCacheOptions options = PayloadCacheOptions.builder() + .updateIntervalSeconds(600) + .build(); + PayloadCacheWrapper wrapper = PayloadCacheWrapper.builder() + .payloadCache(mockCache) + .payloadCacheOptions(options) + .build(); + String testPayload = "test-payload"; + + wrapper.updatePayloadIfNeeded(testPayload); + + verify(mockCache).put(testPayload); + + Field lastUpdateTimeMsField = PayloadCacheWrapper.class.getDeclaredField("lastUpdateTimeMs"); + lastUpdateTimeMsField.setAccessible(true); + long lastUpdateTimeMs = (Long) lastUpdateTimeMsField.get(wrapper); + + assertTrue(System.currentTimeMillis() - lastUpdateTimeMs < 1000, + "lastUpdateTimeMs should be updated to current time"); + } + + @Test + public void test_update_payload_if_needed_respects_update_interval() { + PayloadCache mockCache = mock(PayloadCache.class); + PayloadCacheOptions options = PayloadCacheOptions.builder() + .updateIntervalSeconds(600) + .build(); + PayloadCacheWrapper wrapper = spy(PayloadCacheWrapper.builder() + .payloadCache(mockCache) + .payloadCacheOptions(options) + .build()); + + String testPayload = "test-payload"; + long initialTime = System.currentTimeMillis(); + long updateIntervalMs = options.getUpdateIntervalSeconds() * 1000L; + + doReturn(initialTime).when(wrapper).getCurrentTimeMillis(); + + // First update should succeed + wrapper.updatePayloadIfNeeded(testPayload); + + // Verify the payload was updated + verify(mockCache).put(testPayload); + + // Attempt to update before interval has passed + doReturn(initialTime + updateIntervalMs - 1).when(wrapper).getCurrentTimeMillis(); + wrapper.updatePayloadIfNeeded(testPayload); + + // Verify the payload was not updated again + verify(mockCache, times(1)).put(testPayload); + + // Update after interval has passed + doReturn(initialTime + updateIntervalMs + 1).when(wrapper).getCurrentTimeMillis(); + wrapper.updatePayloadIfNeeded(testPayload); + + // Verify the payload was updated again + verify(mockCache, times(2)).put(testPayload); + } + +} From d201b1fb262e22eb805fce4ce5074d844a4970fa Mon Sep 17 00:00:00 2001 From: liran2000 Date: Mon, 31 Mar 2025 13:23:43 +0300 Subject: [PATCH 05/40] readme update Signed-off-by: liran2000 --- providers/flagd/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/providers/flagd/README.md b/providers/flagd/README.md index d26ef6433..20f6c8fde 100644 --- a/providers/flagd/README.md +++ b/providers/flagd/README.md @@ -56,8 +56,8 @@ If the `in-process` mode is not used, and before the provider is ready, the `get #### Http Connector HttpConnector is responsible for polling data from a specified URL at regular intervals. -It is leveraging Http cache mechanism with 'ETag' header, then when receiving 304 Not Modified response, reducing traffic and -changes updates. Can be enabled via useHttpCache option. +It is leveraging Http cache mechanism with 'ETag' header, then when receiving 304 Not Modified response, +reducing traffic, reducing rate limits effects and changes updates. Can be enabled via useHttpCache option. One of its benefits is to reduce infrastructure/devops work, without additional containers needed. The implementation is using Java HttpClient. From 2091826a506ecf4280c2dae6bba1e61696b05fe8 Mon Sep 17 00:00:00 2001 From: liran2000 Date: Thu, 24 Apr 2025 15:54:11 +0300 Subject: [PATCH 06/40] move to tool - draft Signed-off-by: liran2000 --- providers/flagd/README.md | 5 +- .../sync/http/util/ConcurrentUtils.java | 61 +++ .../sync/http/HttpCacheFetcherTest.java | 309 +++++++++++++ .../http/HttpConnectorIntegrationTest.java | 59 +++ .../sync/http/HttpConnectorOptionsTest.java | 409 +++++++++++++++++ .../sync/http/HttpConnectorTest.java | 416 ++++++++++++++++++ .../sync/http/PayloadCacheWrapperTest.java | 270 ++++++++++++ .../test/resources/simplelogger.properties | 3 + 8 files changed, 1531 insertions(+), 1 deletion(-) create mode 100644 tools/flagd-http-connector/src/main/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/util/ConcurrentUtils.java create mode 100644 tools/flagd-http-connector/src/test/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/HttpCacheFetcherTest.java create mode 100644 tools/flagd-http-connector/src/test/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/HttpConnectorIntegrationTest.java create mode 100644 tools/flagd-http-connector/src/test/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/HttpConnectorOptionsTest.java create mode 100644 tools/flagd-http-connector/src/test/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/HttpConnectorTest.java create mode 100644 tools/flagd-http-connector/src/test/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/PayloadCacheWrapperTest.java create mode 100644 tools/flagd-http-connector/src/test/resources/simplelogger.properties diff --git a/providers/flagd/README.md b/providers/flagd/README.md index 20f6c8fde..f848ca5f6 100644 --- a/providers/flagd/README.md +++ b/providers/flagd/README.md @@ -58,9 +58,12 @@ If the `in-process` mode is not used, and before the provider is ready, the `get HttpConnector is responsible for polling data from a specified URL at regular intervals. It is leveraging Http cache mechanism with 'ETag' header, then when receiving 304 Not Modified response, reducing traffic, reducing rate limits effects and changes updates. Can be enabled via useHttpCache option. -One of its benefits is to reduce infrastructure/devops work, without additional containers needed. The implementation is using Java HttpClient. +##### Use cases and benefits +* Reduce infrastructure/devops work, without additional containers needed. +* Use as an additional provider for fallback / internal backup service via multi-provider. + ##### What happens if the Http source is down when application is starting ? It supports optional fail-safe initialization via cache, such that on initial fetch error following by diff --git a/tools/flagd-http-connector/src/main/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/util/ConcurrentUtils.java b/tools/flagd-http-connector/src/main/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/util/ConcurrentUtils.java new file mode 100644 index 000000000..e7ccbc3b9 --- /dev/null +++ b/tools/flagd-http-connector/src/main/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/util/ConcurrentUtils.java @@ -0,0 +1,61 @@ +package dev.openfeature.contrib.tools.flagd.resolver.process.storage.connector.sync.http.util; + +import lombok.AccessLevel; +import lombok.NoArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.TimeUnit; + +/** + * Concurrent / Concurrency utilities. + * + * @author Liran Mendelovich + */ +@NoArgsConstructor(access = AccessLevel.PRIVATE) +@Slf4j +public class ConcurrentUtils { + + /** + * Graceful shutdown a thread pool.
+ * See + * https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/ExecutorService.html + * + * @param pool thread pool + * @param timeoutSeconds grace period timeout in seconds - timeout can be twice than this value, + * as first it waits for existing tasks to terminate, then waits for cancelled tasks to + * terminate. + */ + public static void shutdownAndAwaitTermination(ExecutorService pool, int timeoutSeconds) { + if (pool == null) { + return; + } + + // Disable new tasks from being submitted + pool.shutdown(); + try { + + // Wait a while for existing tasks to terminate + if (!pool.awaitTermination(timeoutSeconds, TimeUnit.SECONDS)) { + + // Cancel currently executing tasks - best effort, based on interrupt handling + // implementation. + pool.shutdownNow(); + + // Wait a while for tasks to respond to being cancelled + if (!pool.awaitTermination(timeoutSeconds, TimeUnit.SECONDS)) { + log.error("Thread pool did not shutdown all tasks after the timeout: {} seconds.", timeoutSeconds); + } + } + } catch (InterruptedException e) { + + log.info("Current thread interrupted during shutdownAndAwaitTermination, calling shutdownNow."); + + // (Re-)Cancel if current thread also interrupted + pool.shutdownNow(); + + // Preserve interrupt status + Thread.currentThread().interrupt(); + } + } +} diff --git a/tools/flagd-http-connector/src/test/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/HttpCacheFetcherTest.java b/tools/flagd-http-connector/src/test/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/HttpCacheFetcherTest.java new file mode 100644 index 000000000..37e353917 --- /dev/null +++ b/tools/flagd-http-connector/src/test/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/HttpCacheFetcherTest.java @@ -0,0 +1,309 @@ +package dev.openfeature.contrib.tools.flagd.resolver.process.storage.connector.sync.http; + +import static org.junit.Assert.assertNull; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpCacheFetcher; +import java.io.IOException; +import java.lang.reflect.Field; +import java.net.http.HttpClient; +import java.net.http.HttpHeaders; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import lombok.SneakyThrows; +import org.junit.jupiter.api.Test; +public class HttpCacheFetcherTest { + + @Test + public void testFirstRequestSendsNoCacheHeaders() throws Exception { + HttpClient httpClientMock = mock(HttpClient.class); + HttpRequest.Builder requestBuilderMock = mock(HttpRequest.Builder.class); + HttpRequest requestMock = mock(HttpRequest.class); + HttpResponse responseMock = spy(HttpResponse.class); + doReturn(HttpHeaders.of(new HashMap<>(), (a, b) -> true)).when(responseMock).headers(); + + when(requestBuilderMock.build()).thenReturn(requestMock); + when(httpClientMock.send(eq(requestMock), any(HttpResponse.BodyHandler.class))).thenReturn(responseMock); + when(responseMock.statusCode()).thenReturn(200); + + dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpCacheFetcher fetcher = new dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpCacheFetcher(); + fetcher.fetchContent(httpClientMock, requestBuilderMock); + + verify(requestBuilderMock, never()).header(eq("If-None-Match"), anyString()); + verify(requestBuilderMock, never()).header(eq("If-Modified-Since"), anyString()); + } + + @Test + public void testResponseWith200ButNoCacheHeaders() throws Exception { + HttpClient httpClientMock = mock(HttpClient.class); + HttpRequest.Builder requestBuilderMock = mock(HttpRequest.Builder.class); + HttpRequest requestMock = mock(HttpRequest.class); + HttpResponse responseMock = mock(HttpResponse.class); + HttpHeaders headersMock = mock(HttpHeaders.class); + + when(requestBuilderMock.build()).thenReturn(requestMock); + when(httpClientMock.send(eq(requestMock), any(HttpResponse.BodyHandler.class))).thenReturn(responseMock); + when(responseMock.statusCode()).thenReturn(200); + when(responseMock.headers()).thenReturn(headersMock); + when(headersMock.firstValue("ETag")).thenReturn(Optional.empty()); + when(headersMock.firstValue("Last-Modified")).thenReturn(Optional.empty()); + + dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpCacheFetcher fetcher = new dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpCacheFetcher(); + HttpResponse response = fetcher.fetchContent(httpClientMock, requestBuilderMock); + + assertEquals(200, response.statusCode()); + + HttpRequest.Builder secondRequestBuilderMock = mock(HttpRequest.Builder.class); + when(secondRequestBuilderMock.build()).thenReturn(requestMock); + + fetcher.fetchContent(httpClientMock, secondRequestBuilderMock); + + verify(secondRequestBuilderMock, never()).header(eq("If-None-Match"), anyString()); + verify(secondRequestBuilderMock, never()).header(eq("If-Modified-Since"), anyString()); + } + + @Test + public void testFetchContentReturnsHttpResponse() throws Exception { + HttpClient httpClientMock = mock(HttpClient.class); + HttpRequest.Builder requestBuilderMock = mock(HttpRequest.Builder.class); + HttpRequest requestMock = mock(HttpRequest.class); + HttpResponse responseMock = spy(HttpResponse.class); + doReturn(HttpHeaders.of(new HashMap<>(), (a, b) -> true)).when(responseMock).headers(); + + when(requestBuilderMock.build()).thenReturn(requestMock); + when(httpClientMock.send(eq(requestMock), any(HttpResponse.BodyHandler.class))).thenReturn(responseMock); + when(responseMock.statusCode()).thenReturn(404); + + dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpCacheFetcher fetcher = new dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpCacheFetcher(); + HttpResponse result = fetcher.fetchContent(httpClientMock, requestBuilderMock); + + assertEquals(responseMock, result); + } + + @Test + public void test200ResponseNoEtagOrLastModified() throws Exception { + HttpClient httpClientMock = mock(HttpClient.class); + HttpRequest.Builder requestBuilderMock = mock(HttpRequest.Builder.class); + HttpRequest requestMock = mock(HttpRequest.class); + HttpResponse responseMock = spy(HttpResponse.class); + doReturn(HttpHeaders.of(new HashMap<>(), (a, b) -> true)).when(responseMock).headers(); + + when(requestBuilderMock.build()).thenReturn(requestMock); + when(httpClientMock.send(eq(requestMock), any(HttpResponse.BodyHandler.class))).thenReturn(responseMock); + when(responseMock.statusCode()).thenReturn(200); + + dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpCacheFetcher fetcher = new dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpCacheFetcher(); + fetcher.fetchContent(httpClientMock, requestBuilderMock); + + Field cachedETagField = dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpCacheFetcher.class.getDeclaredField("cachedETag"); + cachedETagField.setAccessible(true); + assertNull(cachedETagField.get(fetcher)); + Field cachedLastModifiedField = dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpCacheFetcher.class.getDeclaredField("cachedLastModified"); + cachedLastModifiedField.setAccessible(true); + assertNull(cachedLastModifiedField.get(fetcher)); + } + + @Test + public void testUpdateCacheOn200Response() throws Exception { + HttpClient httpClientMock = mock(HttpClient.class); + HttpRequest.Builder requestBuilderMock = mock(HttpRequest.Builder.class); + HttpRequest requestMock = mock(HttpRequest.class); + HttpResponse responseMock = spy(HttpResponse.class); + doReturn(HttpHeaders.of( + Map.of("Last-Modified", Arrays.asList("Wed, 21 Oct 2015 07:28:00 GMT"), + "ETag", Arrays.asList("etag-value")), + (a, b) -> true)).when(responseMock).headers(); + + when(requestBuilderMock.build()).thenReturn(requestMock); + when(httpClientMock.send(eq(requestMock), any(HttpResponse.BodyHandler.class))).thenReturn(responseMock); + when(responseMock.statusCode()).thenReturn(200); + dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpCacheFetcher fetcher = new dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpCacheFetcher(); + fetcher.fetchContent(httpClientMock, requestBuilderMock); + + Field cachedETagField = dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpCacheFetcher.class.getDeclaredField("cachedETag"); + cachedETagField.setAccessible(true); + assertEquals("etag-value", cachedETagField.get(fetcher)); + Field cachedLastModifiedField = dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpCacheFetcher.class.getDeclaredField("cachedLastModified"); + cachedLastModifiedField.setAccessible(true); + assertEquals("Wed, 21 Oct 2015 07:28:00 GMT", cachedLastModifiedField.get(fetcher)); + } + + @Test + public void testRequestWithCachedEtagIncludesIfNoneMatchHeader() throws Exception { + HttpClient httpClientMock = mock(HttpClient.class); + HttpRequest.Builder requestBuilderMock = mock(HttpRequest.Builder.class); + HttpRequest requestMock = mock(HttpRequest.class); + HttpResponse responseMock = spy(HttpResponse.class); + doReturn(HttpHeaders.of( + Map.of("ETag", Arrays.asList("12345")), + (a, b) -> true)).when(responseMock).headers(); + + when(requestBuilderMock.build()).thenReturn(requestMock); + when(httpClientMock.send(eq(requestMock), any(HttpResponse.BodyHandler.class))).thenReturn(responseMock); + when(responseMock.statusCode()).thenReturn(200); + + dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpCacheFetcher fetcher = new dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpCacheFetcher(); + fetcher.fetchContent(httpClientMock, requestBuilderMock); + fetcher.fetchContent(httpClientMock, requestBuilderMock); + + verify(requestBuilderMock, times(1)).header("If-None-Match", "12345"); + } + + @Test + public void testNullHttpClientOrRequestBuilder() { + dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpCacheFetcher fetcher = new dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpCacheFetcher(); + HttpRequest.Builder requestBuilderMock = mock(HttpRequest.Builder.class); + + assertThrows(NullPointerException.class, () -> { + fetcher.fetchContent(null, requestBuilderMock); + }); + + assertThrows(NullPointerException.class, () -> { + fetcher.fetchContent(mock(HttpClient.class), null); + }); + } + + @Test + public void testResponseWithUnexpectedStatusCode() throws Exception { + HttpClient httpClientMock = mock(HttpClient.class); + HttpRequest.Builder requestBuilderMock = mock(HttpRequest.Builder.class); + HttpRequest requestMock = mock(HttpRequest.class); + HttpResponse responseMock = spy(HttpResponse.class); + doReturn(HttpHeaders.of(new HashMap<>(), (a, b) -> true)).when(responseMock).headers(); + + when(requestBuilderMock.build()).thenReturn(requestMock); + when(httpClientMock.send(eq(requestMock), any(HttpResponse.BodyHandler.class))).thenReturn(responseMock); + when(responseMock.statusCode()).thenReturn(500); + + dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpCacheFetcher fetcher = new dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpCacheFetcher(); + HttpResponse response = fetcher.fetchContent(httpClientMock, requestBuilderMock); + + assertEquals(500, response.statusCode()); + verify(requestBuilderMock, never()).header(eq("If-None-Match"), anyString()); + verify(requestBuilderMock, never()).header(eq("If-Modified-Since"), anyString()); + } + + @Test + public void testRequestIncludesIfModifiedSinceHeaderWhenLastModifiedCached() throws Exception { + HttpClient httpClientMock = mock(HttpClient.class); + HttpRequest.Builder requestBuilderMock = mock(HttpRequest.Builder.class); + HttpRequest requestMock = mock(HttpRequest.class); + HttpResponse responseMock = spy(HttpResponse.class); + doReturn(HttpHeaders.of( + Map.of("Last-Modified", Arrays.asList("Wed, 21 Oct 2015 07:28:00 GMT")), + (a, b) -> true)).when(responseMock).headers(); + + when(requestBuilderMock.build()).thenReturn(requestMock); + when(httpClientMock.send(eq(requestMock), any(HttpResponse.BodyHandler.class))).thenReturn(responseMock); + when(responseMock.statusCode()).thenReturn(200); + + dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpCacheFetcher fetcher = new dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpCacheFetcher(); + fetcher.fetchContent(httpClientMock, requestBuilderMock); + fetcher.fetchContent(httpClientMock, requestBuilderMock); + + verify(requestBuilderMock).header(eq("If-Modified-Since"), eq("Wed, 21 Oct 2015 07:28:00 GMT")); + } + + @Test + public void testCalls200And304Responses() throws Exception { + HttpClient httpClientMock = mock(HttpClient.class); + HttpRequest.Builder requestBuilderMock = mock(HttpRequest.Builder.class); + HttpRequest requestMock = mock(HttpRequest.class); + HttpResponse responseMock200 = mock(HttpResponse.class); + HttpResponse responseMock304 = mock(HttpResponse.class); + + when(requestBuilderMock.build()).thenReturn(requestMock); + when(httpClientMock.send(eq(requestMock), any(HttpResponse.BodyHandler.class))) + .thenReturn(responseMock200) + .thenReturn(responseMock304); + when(responseMock200.statusCode()).thenReturn(200); + when(responseMock304.statusCode()).thenReturn(304); + + dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpCacheFetcher fetcher = new dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpCacheFetcher(); + fetcher.fetchContent(httpClientMock, requestBuilderMock); + fetcher.fetchContent(httpClientMock, requestBuilderMock); + + verify(responseMock200, times(1)).statusCode(); + verify(responseMock304, times(2)).statusCode(); + } + + @Test + public void testRequestIncludesBothEtagAndLastModifiedHeaders() throws Exception { + HttpClient httpClientMock = mock(HttpClient.class); + HttpRequest.Builder requestBuilderMock = mock(HttpRequest.Builder.class); + HttpRequest requestMock = mock(HttpRequest.class); + HttpResponse responseMock = spy(HttpResponse.class); + doReturn(HttpHeaders.of(new HashMap<>(), (a, b) -> true)).when(responseMock).headers(); + + when(requestBuilderMock.build()).thenReturn(requestMock); + when(httpClientMock.send(eq(requestMock), any(HttpResponse.BodyHandler.class))).thenReturn(responseMock); + when(responseMock.statusCode()).thenReturn(200); + + dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpCacheFetcher fetcher = new dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpCacheFetcher(); + Field cachedETagField = dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpCacheFetcher.class.getDeclaredField("cachedETag"); + cachedETagField.setAccessible(true); + cachedETagField.set(fetcher, "test-etag"); + Field cachedLastModifiedField = dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpCacheFetcher.class.getDeclaredField("cachedLastModified"); + cachedLastModifiedField.setAccessible(true); + cachedLastModifiedField.set(fetcher, "test-last-modified"); + + fetcher.fetchContent(httpClientMock, requestBuilderMock); + + verify(requestBuilderMock).header("If-None-Match", "test-etag"); + verify(requestBuilderMock).header("If-Modified-Since", "test-last-modified"); + } + + @SneakyThrows + @Test + public void testHttpClientSendExceptionPropagation() { + HttpClient httpClientMock = mock(HttpClient.class); + HttpRequest.Builder requestBuilderMock = mock(HttpRequest.Builder.class); + HttpRequest requestMock = mock(HttpRequest.class); + + when(requestBuilderMock.build()).thenReturn(requestMock); + when(httpClientMock.send(eq(requestMock), any(HttpResponse.BodyHandler.class))) + .thenThrow(new IOException("Network error")); + + dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpCacheFetcher fetcher = new dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpCacheFetcher(); + assertThrows(IOException.class, () -> { + fetcher.fetchContent(httpClientMock, requestBuilderMock); + }); + } + + @Test + public void testOnlyEtagAndLastModifiedHeadersCached() throws Exception { + HttpClient httpClientMock = mock(HttpClient.class); + HttpRequest.Builder requestBuilderMock = mock(HttpRequest.Builder.class); + HttpRequest requestMock = mock(HttpRequest.class); + HttpResponse responseMock = spy(HttpResponse.class); + doReturn(HttpHeaders.of( + Map.of("Last-Modified", Arrays.asList("last-modified-value"), + "ETag", Arrays.asList("etag-value")), + (a, b) -> true)).when(responseMock).headers(); + + when(requestBuilderMock.build()).thenReturn(requestMock); + when(httpClientMock.send(eq(requestMock), any(HttpResponse.BodyHandler.class))).thenReturn(responseMock); + when(responseMock.statusCode()).thenReturn(200); + + dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpCacheFetcher fetcher = new HttpCacheFetcher(); + fetcher.fetchContent(httpClientMock, requestBuilderMock); + + verify(requestBuilderMock, never()).header(eq("Some-Other-Header"), anyString()); + } + +} diff --git a/tools/flagd-http-connector/src/test/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/HttpConnectorIntegrationTest.java b/tools/flagd-http-connector/src/test/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/HttpConnectorIntegrationTest.java new file mode 100644 index 000000000..75b69721a --- /dev/null +++ b/tools/flagd-http-connector/src/test/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/HttpConnectorIntegrationTest.java @@ -0,0 +1,59 @@ +package dev.openfeature.contrib.tools.flagd.resolver.process.storage.connector.sync.http; + +import static dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpConnectorTest.delay; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assumptions.assumeTrue; + +import dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.QueuePayload; +import dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpConnector; +import dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpConnectorOptions; +import java.util.concurrent.BlockingQueue; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import org.junit.jupiter.api.Test; + +/** + * Integration test for the HttpConnector class, specifically testing the ability to fetch + * raw content from a GitHub URL. This test assumes that integration tests are enabled + * and verifies that the HttpConnector can successfully enqueue data from the specified URL. + * The test initializes the HttpConnector with specific configurations, waits for data + * to be enqueued, and asserts the expected queue size. The connector is shut down + * gracefully after the test execution. + * As this integration test using external request, it is disabled by default, and not part of the CI build. + */ +@Slf4j +class HttpConnectorIntegrationTest { + + @SneakyThrows + @Test + void testGithubRawContent() { + assumeTrue(parseBoolean("integrationTestsEnabled")); + dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpConnector connector = null; + try { + String testUrl = "https://raw.githubusercontent.com/open-feature/java-sdk-contrib/58fe5da7d4e2f6f4ae2c1caf3411a01e84a1dc1a/providers/flagd/version.txt"; + + dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpConnectorOptions httpConnectorOptions = HttpConnectorOptions.builder() + .url(testUrl) + .connectTimeoutSeconds(10) + .requestTimeoutSeconds(10) + .useHttpCache(true) + .pollIntervalSeconds(5) + .build(); + connector = HttpConnector.builder() + .httpConnectorOptions(httpConnectorOptions) + .build(); + BlockingQueue queue = connector.getStreamQueue(); + delay(20000); + assertEquals(1, queue.size()); + } finally { + if (connector != null) { + connector.shutdown(); + } + } + } + + public static boolean parseBoolean(String key) { + return Boolean.parseBoolean(System.getProperty(key, System.getenv(key))); + } + +} diff --git a/tools/flagd-http-connector/src/test/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/HttpConnectorOptionsTest.java b/tools/flagd-http-connector/src/test/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/HttpConnectorOptionsTest.java new file mode 100644 index 000000000..a90bbae38 --- /dev/null +++ b/tools/flagd-http-connector/src/test/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/HttpConnectorOptionsTest.java @@ -0,0 +1,409 @@ +package dev.openfeature.contrib.tools.flagd.resolver.process.storage.connector.sync.http; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; + +import dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpConnectorOptions; +import dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.PayloadCache; +import dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.PayloadCacheOptions; +import java.net.MalformedURLException; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import org.junit.Test; + +public class HttpConnectorOptionsTest { + + + @Test + public void testDefaultValuesInitialization() { + dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpConnectorOptions options = dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpConnectorOptions.builder() + .url("https://example.com") + .build(); + + assertEquals(60, options.getPollIntervalSeconds().intValue()); + assertEquals(10, options.getConnectTimeoutSeconds().intValue()); + assertEquals(10, options.getRequestTimeoutSeconds().intValue()); + assertEquals(100, options.getLinkedBlockingQueueCapacity().intValue()); + assertEquals(2, options.getScheduledThreadPoolSize().intValue()); + assertNotNull(options.getHeaders()); + assertTrue(options.getHeaders().isEmpty()); + assertNotNull(options.getHttpClientExecutor()); + assertNull(options.getProxyHost()); + assertNull(options.getProxyPort()); + assertNull(options.getPayloadCacheOptions()); + assertNull(options.getPayloadCache()); + assertNull(options.getUseHttpCache()); + assertEquals("https://example.com", options.getUrl()); + } + + @Test + public void testInvalidUrlFormat() { + MalformedURLException exception = assertThrows( + MalformedURLException.class, + () -> dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpConnectorOptions.builder() + .url("invalid-url") + .build() + ); + + assertNotNull(exception); + } + + @Test + public void testCustomValuesInitialization() { + dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpConnectorOptions options = dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpConnectorOptions.builder() + .pollIntervalSeconds(120) + .connectTimeoutSeconds(20) + .requestTimeoutSeconds(30) + .linkedBlockingQueueCapacity(200) + .scheduledThreadPoolSize(5) + .url("http://example.com") + .build(); + + assertEquals(120, options.getPollIntervalSeconds().intValue()); + assertEquals(20, options.getConnectTimeoutSeconds().intValue()); + assertEquals(30, options.getRequestTimeoutSeconds().intValue()); + assertEquals(200, options.getLinkedBlockingQueueCapacity().intValue()); + assertEquals(5, options.getScheduledThreadPoolSize().intValue()); + assertEquals("http://example.com", options.getUrl()); + } + + @Test + public void testCustomHeadersMap() { + Map customHeaders = new HashMap<>(); + customHeaders.put("Authorization", "Bearer token"); + customHeaders.put("Content-Type", "application/json"); + + dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpConnectorOptions options = dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpConnectorOptions.builder() + .url("http://example.com") + .headers(customHeaders) + .build(); + + assertEquals("Bearer token", options.getHeaders().get("Authorization")); + assertEquals("application/json", options.getHeaders().get("Content-Type")); + } + + @Test + public void testCustomExecutorService() { + ExecutorService customExecutor = Executors.newFixedThreadPool(5); + dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpConnectorOptions options = dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpConnectorOptions.builder() + .url("https://example.com") + .httpClientExecutor(customExecutor) + .build(); + + assertEquals(customExecutor, options.getHttpClientExecutor()); + } + + @Test + public void testSettingPayloadCacheWithValidOptions() { + dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.PayloadCacheOptions cacheOptions = dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.PayloadCacheOptions.builder() + .updateIntervalSeconds(1800) + .build(); + dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.PayloadCache payloadCache = new dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.PayloadCache() { + private String payload; + + @Override + public void put(String payload) { + this.payload = payload; + } + + @Override + public String get() { + return this.payload; + } + }; + + dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpConnectorOptions options = dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpConnectorOptions.builder() + .url("https://example.com") + .payloadCacheOptions(cacheOptions) + .payloadCache(payloadCache) + .build(); + + assertNotNull(options.getPayloadCacheOptions()); + assertNotNull(options.getPayloadCache()); + assertEquals(1800, options.getPayloadCacheOptions().getUpdateIntervalSeconds()); + } + + @Test + public void testProxyConfigurationWithValidHostAndPort() { + dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpConnectorOptions options = dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpConnectorOptions.builder() + .url("https://example.com") + .proxyHost("proxy.example.com") + .proxyPort(8080) + .build(); + + assertEquals("proxy.example.com", options.getProxyHost()); + assertEquals(8080, options.getProxyPort().intValue()); + } + + @Test + public void testLinkedBlockingQueueCapacityOutOfRange() { + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> { + dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpConnectorOptions.builder() + .url("https://example.com") + .linkedBlockingQueueCapacity(0) + .build(); + }); + assertEquals("linkedBlockingQueueCapacity must be between 1 and 1000", exception.getMessage()); + + exception = assertThrows(IllegalArgumentException.class, () -> { + dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpConnectorOptions.builder() + .url("https://example.com") + .linkedBlockingQueueCapacity(1001) + .build(); + }); + assertEquals("linkedBlockingQueueCapacity must be between 1 and 1000", exception.getMessage()); + } + + @Test + public void testPollIntervalSecondsOutOfRange() { + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> { + dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpConnectorOptions.builder() + .url("https://example.com") + .pollIntervalSeconds(700) + .build(); + }); + assertEquals("pollIntervalSeconds must be between 1 and 600", exception.getMessage()); + } + + @Test + public void testAdditionalCustomValuesInitialization() { + Map headers = new HashMap<>(); + headers.put("Authorization", "Bearer token"); + ExecutorService executorService = Executors.newFixedThreadPool(2); + dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.PayloadCacheOptions cacheOptions = dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.PayloadCacheOptions.builder().build(); + dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.PayloadCache cache = new dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.PayloadCache() { + @Override + public void put(String payload) { + // do nothing + } + @Override + public String get() { return null; } + }; + + dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpConnectorOptions options = dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpConnectorOptions.builder() + .url("https://example.com") + .pollIntervalSeconds(120) + .connectTimeoutSeconds(20) + .requestTimeoutSeconds(30) + .linkedBlockingQueueCapacity(200) + .scheduledThreadPoolSize(4) + .headers(headers) + .httpClientExecutor(executorService) + .proxyHost("proxy.example.com") + .proxyPort(8080) + .payloadCacheOptions(cacheOptions) + .payloadCache(cache) + .useHttpCache(true) + .build(); + + assertEquals(120, options.getPollIntervalSeconds().intValue()); + assertEquals(20, options.getConnectTimeoutSeconds().intValue()); + assertEquals(30, options.getRequestTimeoutSeconds().intValue()); + assertEquals(200, options.getLinkedBlockingQueueCapacity().intValue()); + assertEquals(4, options.getScheduledThreadPoolSize().intValue()); + assertNotNull(options.getHeaders()); + assertEquals("Bearer token", options.getHeaders().get("Authorization")); + assertNotNull(options.getHttpClientExecutor()); + assertEquals("proxy.example.com", options.getProxyHost()); + assertEquals(8080, options.getProxyPort().intValue()); + assertNotNull(options.getPayloadCacheOptions()); + assertNotNull(options.getPayloadCache()); + assertTrue(options.getUseHttpCache()); + assertEquals("https://example.com", options.getUrl()); + } + + @Test + public void testRequestTimeoutSecondsOutOfRange() { + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> { + dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpConnectorOptions.builder() + .url("https://example.com") + .requestTimeoutSeconds(61) + .build(); + }); + assertEquals("requestTimeoutSeconds must be between 1 and 60", exception.getMessage()); + } + + @Test + public void testBuilderInitializesAllFields() { + Map headers = new HashMap<>(); + headers.put("Authorization", "Bearer token"); + ExecutorService executorService = Executors.newFixedThreadPool(2); + dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.PayloadCacheOptions cacheOptions = dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.PayloadCacheOptions.builder().build(); + dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.PayloadCache cache = new dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.PayloadCache() { + @Override + public void put(String payload) { + // do nothing + } + @Override + public String get() { return null; } + }; + + dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpConnectorOptions options = dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpConnectorOptions.builder() + .pollIntervalSeconds(120) + .connectTimeoutSeconds(20) + .requestTimeoutSeconds(30) + .linkedBlockingQueueCapacity(200) + .scheduledThreadPoolSize(4) + .headers(headers) + .httpClientExecutor(executorService) + .proxyHost("proxy.example.com") + .proxyPort(8080) + .payloadCacheOptions(cacheOptions) + .payloadCache(cache) + .useHttpCache(true) + .url("https://example.com") + .build(); + + assertEquals(120, options.getPollIntervalSeconds().intValue()); + assertEquals(20, options.getConnectTimeoutSeconds().intValue()); + assertEquals(30, options.getRequestTimeoutSeconds().intValue()); + assertEquals(200, options.getLinkedBlockingQueueCapacity().intValue()); + assertEquals(4, options.getScheduledThreadPoolSize().intValue()); + assertEquals(headers, options.getHeaders()); + assertEquals(executorService, options.getHttpClientExecutor()); + assertEquals("proxy.example.com", options.getProxyHost()); + assertEquals(8080, options.getProxyPort().intValue()); + assertEquals(cacheOptions, options.getPayloadCacheOptions()); + assertEquals(cache, options.getPayloadCache()); + assertTrue(options.getUseHttpCache()); + assertEquals("https://example.com", options.getUrl()); + } + + @Test + public void testScheduledThreadPoolSizeOutOfRange() { + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> { + dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpConnectorOptions.builder() + .url("https://example.com") + .scheduledThreadPoolSize(11) + .build(); + }); + assertEquals("scheduledThreadPoolSize must be between 1 and 10", exception.getMessage()); + } + + @Test + public void testProxyPortOutOfRange() { + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> { + dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpConnectorOptions.builder() + .url("https://example.com") + .proxyHost("proxy.example.com") + .proxyPort(70000) // Invalid port, out of range + .build(); + }); + assertEquals("proxyPort must be between 1 and 65535", exception.getMessage()); + } + + @Test + public void testConnectTimeoutSecondsOutOfRange() { + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> { + dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpConnectorOptions.builder() + .url("https://example.com") + .connectTimeoutSeconds(0) + .build(); + }); + assertEquals("connectTimeoutSeconds must be between 1 and 60", exception.getMessage()); + + exception = assertThrows(IllegalArgumentException.class, () -> { + dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpConnectorOptions.builder() + .url("https://example.com") + .connectTimeoutSeconds(61) + .build(); + }); + assertEquals("connectTimeoutSeconds must be between 1 and 60", exception.getMessage()); + } + + @Test + public void testProxyPortWithoutProxyHost() { + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> { + dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpConnectorOptions.builder() + .url("https://example.com") + .proxyPort(8080) + .build(); + }); + assertEquals("proxyHost must be set if proxyPort is set", exception.getMessage()); + } + + @Test + public void testDefaultValuesWhenNullParametersProvided() { + dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpConnectorOptions options = dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpConnectorOptions.builder() + .url("https://example.com") + .pollIntervalSeconds(null) + .linkedBlockingQueueCapacity(null) + .scheduledThreadPoolSize(null) + .requestTimeoutSeconds(null) + .connectTimeoutSeconds(null) + .headers(null) + .httpClientExecutor(null) + .proxyHost(null) + .proxyPort(null) + .payloadCacheOptions(null) + .payloadCache(null) + .useHttpCache(null) + .build(); + + assertEquals(60, options.getPollIntervalSeconds().intValue()); + assertEquals(10, options.getConnectTimeoutSeconds().intValue()); + assertEquals(10, options.getRequestTimeoutSeconds().intValue()); + assertEquals(100, options.getLinkedBlockingQueueCapacity().intValue()); + assertEquals(2, options.getScheduledThreadPoolSize().intValue()); + assertNotNull(options.getHeaders()); + assertTrue(options.getHeaders().isEmpty()); + assertNotNull(options.getHttpClientExecutor()); + assertNull(options.getProxyHost()); + assertNull(options.getProxyPort()); + assertNull(options.getPayloadCacheOptions()); + assertNull(options.getPayloadCache()); + assertNull(options.getUseHttpCache()); + assertEquals("https://example.com", options.getUrl()); + } + + @Test + public void testProxyHostWithoutProxyPort() { + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> { + dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpConnectorOptions.builder() + .url("https://example.com") + .proxyHost("proxy.example.com") + .build(); + }); + assertEquals("proxyPort must be set if proxyHost is set", exception.getMessage()); + } + + @Test + public void testSettingPayloadCacheWithoutOptions() { + dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.PayloadCache mockPayloadCache = new PayloadCache() { + @Override + public void put(String payload) { + // Mock implementation + } + + @Override + public String get() { + return "mockPayload"; + } + }; + + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> { + dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpConnectorOptions.builder() + .url("https://example.com") + .payloadCache(mockPayloadCache) + .build(); + }); + + assertEquals("payloadCacheOptions must be set if payloadCache is set", exception.getMessage()); + } + + @Test + public void testPayloadCacheOptionsWithoutPayloadCache() { + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> { + HttpConnectorOptions.builder() + .url("https://example.com") + .payloadCacheOptions(PayloadCacheOptions.builder().build()) + .build(); + }); + assertEquals("payloadCache must be set if payloadCacheOptions is set", exception.getMessage()); + } +} diff --git a/tools/flagd-http-connector/src/test/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/HttpConnectorTest.java b/tools/flagd-http-connector/src/test/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/HttpConnectorTest.java new file mode 100644 index 000000000..0edb440a8 --- /dev/null +++ b/tools/flagd-http-connector/src/test/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/HttpConnectorTest.java @@ -0,0 +1,416 @@ +package dev.openfeature.contrib.tools.flagd.resolver.process.storage.connector.sync.http; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.when; + +import dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.QueuePayload; +import dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.QueuePayloadType; +import dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpConnector; +import dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpConnectorOptions; +import dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.PayloadCache; +import dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.PayloadCacheOptions; +import java.io.IOException; +import java.lang.reflect.Field; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +@Slf4j +class HttpConnectorTest { + + @SneakyThrows + @Test + void testGetStreamQueueInitialAndScheduledPolls() { + String testUrl = "http://example.com"; + HttpClient mockClient = mock(HttpClient.class); + HttpResponse mockResponse = mock(HttpResponse.class); + when(mockResponse.statusCode()).thenReturn(200); + when(mockResponse.body()).thenReturn("test data"); + when(mockClient.send(any(HttpRequest.class), any(HttpResponse.BodyHandler.class))) + .thenReturn(mockResponse); + + dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpConnectorOptions httpConnectorOptions = dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpConnectorOptions.builder() + .url(testUrl) + .httpClientExecutor(Executors.newSingleThreadExecutor()) + .build(); + dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpConnector connector = dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpConnector.builder() + .httpConnectorOptions(httpConnectorOptions) + .build(); + + Field clientField = dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpConnector.class.getDeclaredField("client"); + clientField.setAccessible(true); + clientField.set(connector, mockClient); + + BlockingQueue queue = connector.getStreamQueue(); + + assertFalse(queue.isEmpty()); + QueuePayload payload = queue.poll(); + assertNotNull(payload); + assertEquals(QueuePayloadType.DATA, payload.getType()); + assertEquals("test data", payload.getFlagData()); + + connector.shutdown(); + } + + @SneakyThrows + @Test + void testBuildPollTaskFetchesDataAndAddsToQueue() { + String testUrl = "http://example.com"; + HttpClient mockClient = mock(HttpClient.class); + HttpResponse mockResponse = mock(HttpResponse.class); + when(mockResponse.statusCode()).thenReturn(200); + when(mockResponse.body()).thenReturn("test data"); + when(mockClient.send(any(HttpRequest.class), any(HttpResponse.BodyHandler.class))) + .thenReturn(mockResponse); + + dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.PayloadCache payloadCache = new dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.PayloadCache() { + private String payload; + @Override + public void put(String payload) { + this.payload = payload; + } + + @Override + public String get() { + return payload; + } + }; + dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpConnectorOptions httpConnectorOptions = dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpConnectorOptions.builder() + .url(testUrl) + .proxyHost("proxy-host") + .proxyPort(8080) + .useHttpCache(true) + .payloadCache(payloadCache) + .payloadCacheOptions(dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.PayloadCacheOptions.builder().build()) + .build(); + dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpConnector connector = dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpConnector.builder() + .httpConnectorOptions(httpConnectorOptions) + .build(); + connector.init(); + + Field clientField = dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpConnector.class.getDeclaredField("client"); + clientField.setAccessible(true); + clientField.set(connector, mockClient); + + Runnable pollTask = connector.buildPollTask(); + pollTask.run(); + + Field queueField = dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpConnector.class.getDeclaredField("queue"); + queueField.setAccessible(true); + BlockingQueue queue = (BlockingQueue) queueField.get(connector); + assertFalse(queue.isEmpty()); + QueuePayload payload = queue.poll(); + assertNotNull(payload); + assertEquals(QueuePayloadType.DATA, payload.getType()); + assertEquals("test data", payload.getFlagData()); + } + + @SneakyThrows + @Test + void testHttpRequestIncludesHeaders() { + String testUrl = "http://example.com"; + Map testHeaders = new HashMap<>(); + testHeaders.put("Authorization", "Bearer token"); + testHeaders.put("Content-Type", "application/json"); + + dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpConnectorOptions httpConnectorOptions = dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpConnectorOptions.builder() + .url(testUrl) + .headers(testHeaders) + .build(); + dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpConnector connector = dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpConnector.builder() + .httpConnectorOptions(httpConnectorOptions) + .build(); + + Field headersField = dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpConnector.class.getDeclaredField("headers"); + headersField.setAccessible(true); + Map headers = (Map) headersField.get(connector); + assertNotNull(headers); + assertEquals(2, headers.size()); + assertEquals("Bearer token", headers.get("Authorization")); + assertEquals("application/json", headers.get("Content-Type")); + } + + @SneakyThrows + @Test + void testSuccessfulHttpResponseAddsDataToQueue() { + String testUrl = "http://example.com"; + HttpClient mockClient = mock(HttpClient.class); + HttpResponse mockResponse = mock(HttpResponse.class); + when(mockResponse.statusCode()).thenReturn(200); + when(mockResponse.body()).thenReturn("test data"); + when(mockClient.send(any(HttpRequest.class), any(HttpResponse.BodyHandler.class))) + .thenReturn(mockResponse); + + dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpConnectorOptions httpConnectorOptions = dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpConnectorOptions.builder() + .url(testUrl) + .build(); + dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpConnector connector = dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpConnector.builder() + .httpConnectorOptions(httpConnectorOptions) + .build(); + + Field clientField = dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpConnector.class.getDeclaredField("client"); + clientField.setAccessible(true); + clientField.set(connector, mockClient); + + BlockingQueue queue = connector.getStreamQueue(); + + assertFalse(queue.isEmpty()); + QueuePayload payload = queue.poll(); + assertNotNull(payload); + assertEquals(QueuePayloadType.DATA, payload.getType()); + assertEquals("test data", payload.getFlagData()); + } + + @SneakyThrows + @Test + void testInitFailureUsingCache() { + String testUrl = "http://example.com"; + HttpClient mockClient = mock(HttpClient.class); + HttpResponse mockResponse = mock(HttpResponse.class); + when(mockResponse.statusCode()).thenReturn(200); + when(mockClient.send(any(HttpRequest.class), any(HttpResponse.BodyHandler.class))) + .thenThrow(new IOException("Simulated IO Exception")); + + final String cachedData = "cached data"; + dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.PayloadCache payloadCache = new PayloadCache() { + @Override + public void put(String payload) { + // do nothing + } + + @Override + public String get() { + return cachedData; + } + }; + + dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpConnectorOptions httpConnectorOptions = dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpConnectorOptions.builder() + .url(testUrl) + .payloadCache(payloadCache) + .payloadCacheOptions(PayloadCacheOptions.builder().build()) + .build(); + dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpConnector connector = dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpConnector.builder() + .httpConnectorOptions(httpConnectorOptions) + .build(); + + Field clientField = dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpConnector.class.getDeclaredField("client"); + clientField.setAccessible(true); + clientField.set(connector, mockClient); + + BlockingQueue queue = connector.getStreamQueue(); + + assertFalse(queue.isEmpty()); + QueuePayload payload = queue.poll(); + assertNotNull(payload); + assertEquals(QueuePayloadType.DATA, payload.getType()); + assertEquals(cachedData, payload.getFlagData()); + } + + @SneakyThrows + @Test + void testQueueBecomesFull() { + String testUrl = "http://example.com"; + int queueCapacity = 1; + dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpConnectorOptions httpConnectorOptions = dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpConnectorOptions.builder() + .url(testUrl) + .linkedBlockingQueueCapacity(queueCapacity) + .build(); + dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpConnector connector = dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpConnector.builder() + .httpConnectorOptions(httpConnectorOptions) + .build(); + + BlockingQueue queue = connector.getStreamQueue(); + + queue.offer(new QueuePayload(QueuePayloadType.DATA, "test data 1")); + + boolean wasOffered = queue.offer(new QueuePayload(QueuePayloadType.DATA, "test data 2")); + + assertFalse(wasOffered, "Queue should be full and not accept more items"); + } + + @SneakyThrows + @Test + void testShutdownProperlyTerminatesSchedulerAndHttpClientExecutor() throws InterruptedException { + ExecutorService mockHttpClientExecutor = mock(ExecutorService.class); + ScheduledExecutorService mockScheduler = mock(ScheduledExecutorService.class); + String testUrl = "http://example.com"; + + dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpConnectorOptions httpConnectorOptions = dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpConnectorOptions.builder() + .url(testUrl) + .httpClientExecutor(mockHttpClientExecutor) + .build(); + dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpConnector connector = dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpConnector.builder() + .httpConnectorOptions(httpConnectorOptions) + .build(); + + Field schedulerField = dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpConnector.class.getDeclaredField("scheduler"); + schedulerField.setAccessible(true); + schedulerField.set(connector, mockScheduler); + + connector.shutdown(); + + Mockito.verify(mockScheduler).shutdown(); + Mockito.verify(mockHttpClientExecutor).shutdown(); + } + + @SneakyThrows + @Test + void testHttpResponseNonSuccessStatusCode() { + String testUrl = "http://example.com"; + HttpClient mockClient = mock(HttpClient.class); + HttpResponse mockResponse = mock(HttpResponse.class); + when(mockResponse.statusCode()).thenReturn(404); + when(mockClient.send(any(HttpRequest.class), any(HttpResponse.BodyHandler.class))) + .thenReturn(mockResponse); + + dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpConnectorOptions httpConnectorOptions = dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpConnectorOptions.builder() + .url(testUrl) + .build(); + dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpConnector connector = dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpConnector.builder() + .httpConnectorOptions(httpConnectorOptions) + .build(); + + Field clientField = dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpConnector.class.getDeclaredField("client"); + clientField.setAccessible(true); + clientField.set(connector, mockClient); + + BlockingQueue queue = connector.getStreamQueue(); + + assertTrue(queue.isEmpty(), "Queue should be empty when response status is non-200"); + } + + @SneakyThrows + @Test + void testHttpRequestFailsWithException() { + String testUrl = "http://example.com"; + HttpClient mockClient = mock(HttpClient.class); + dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpConnectorOptions httpConnectorOptions = dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpConnectorOptions.builder() + .url(testUrl) + .build(); + dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpConnector connector = dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpConnector.builder() + .httpConnectorOptions(httpConnectorOptions) + .build(); + + Field clientField = dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpConnector.class.getDeclaredField("client"); + clientField.setAccessible(true); + clientField.set(connector, mockClient); + + when(mockClient.send(any(HttpRequest.class), any(HttpResponse.BodyHandler.class))) + .thenThrow(new RuntimeException("Test exception")); + + BlockingQueue queue = connector.getStreamQueue(); + + assertTrue(queue.isEmpty(), "Queue should be empty when request fails with exception"); + } + + @SneakyThrows + @Test + void testHttpRequestFailsWithIoexception() { + String testUrl = "http://example.com"; + HttpClient mockClient = mock(HttpClient.class); + dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpConnectorOptions httpConnectorOptions = dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpConnectorOptions.builder() + .url(testUrl) + .build(); + dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpConnector connector = dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpConnector.builder() + .httpConnectorOptions(httpConnectorOptions) + .build(); + + Field clientField = dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpConnector.class.getDeclaredField("client"); + clientField.setAccessible(true); + clientField.set(connector, mockClient); + + when(mockClient.send(any(HttpRequest.class), any(HttpResponse.BodyHandler.class))) + .thenThrow(new IOException("Simulated IO Exception")); + + connector.getStreamQueue(); + + Field queueField = dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpConnector.class.getDeclaredField("queue"); + queueField.setAccessible(true); + BlockingQueue queue = (BlockingQueue) queueField.get(connector); + assertTrue(queue.isEmpty(), "Queue should be empty due to IOException"); + } + + @SneakyThrows + @Test + void testScheduledPollingContinuesAtFixedIntervals() { + String testUrl = "http://exampOle.com"; + HttpResponse mockResponse = mock(HttpResponse.class); + when(mockResponse.statusCode()).thenReturn(200); + when(mockResponse.body()).thenReturn("test data"); + + dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpConnectorOptions httpConnectorOptions = dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpConnectorOptions.builder() + .url(testUrl) + .build(); + dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpConnector connector = spy(dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpConnector.builder() + .httpConnectorOptions(httpConnectorOptions) + .build()); + + doReturn(mockResponse).when(connector).execute(any()); + + BlockingQueue queue = connector.getStreamQueue(); + + delay(2000); + assertFalse(queue.isEmpty()); + QueuePayload payload = queue.poll(); + assertNotNull(payload); + assertEquals(QueuePayloadType.DATA, payload.getType()); + assertEquals("test data", payload.getFlagData()); + + connector.shutdown(); + } + + @SneakyThrows + @Test + void testQueuePayloadTypeSetToDataOnSuccess() { + String testUrl = "http://example.com"; + HttpClient mockClient = mock(HttpClient.class); + HttpResponse mockResponse = mock(HttpResponse.class); + + when(mockResponse.statusCode()).thenReturn(200); + when(mockResponse.body()).thenReturn("response body"); + when(mockClient.send(any(HttpRequest.class), any(HttpResponse.BodyHandler.class))) + .thenReturn(mockResponse); + + dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpConnectorOptions httpConnectorOptions = HttpConnectorOptions.builder() + .url(testUrl) + .build(); + dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpConnector connector = dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpConnector.builder() + .httpConnectorOptions(httpConnectorOptions) + .build(); + + Field clientField = HttpConnector.class.getDeclaredField("client"); + clientField.setAccessible(true); + clientField.set(connector, mockClient); + + BlockingQueue queue = connector.getStreamQueue(); + + QueuePayload payload = queue.poll(1, TimeUnit.SECONDS); + assertNotNull(payload); + assertEquals(QueuePayloadType.DATA, payload.getType()); + assertEquals("response body", payload.getFlagData()); + } + + @SneakyThrows + protected static void delay(long ms) { + Thread.sleep(ms); + } + +} diff --git a/tools/flagd-http-connector/src/test/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/PayloadCacheWrapperTest.java b/tools/flagd-http-connector/src/test/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/PayloadCacheWrapperTest.java new file mode 100644 index 000000000..28fba6881 --- /dev/null +++ b/tools/flagd-http-connector/src/test/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/PayloadCacheWrapperTest.java @@ -0,0 +1,270 @@ +package dev.openfeature.contrib.tools.flagd.resolver.process.storage.connector.sync.http; + +import static dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpConnectorTest.delay; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.PayloadCache; +import dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.PayloadCacheOptions; +import dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.PayloadCacheWrapper; +import java.lang.reflect.Field; +import lombok.SneakyThrows; +import org.junit.jupiter.api.Test; + + +public class PayloadCacheWrapperTest { + + @Test + public void testConstructorInitializesWithValidParameters() { + dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.PayloadCache mockCache = mock(dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.PayloadCache.class); + dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.PayloadCacheOptions options = dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.PayloadCacheOptions.builder() + .updateIntervalSeconds(600) + .build(); + + dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.PayloadCacheWrapper wrapper = dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.PayloadCacheWrapper.builder() + .payloadCache(mockCache) + .payloadCacheOptions(options) + .build(); + + assertNotNull(wrapper); + + String testPayload = "test-payload"; + wrapper.updatePayloadIfNeeded(testPayload); + wrapper.get(); + + verify(mockCache).put(testPayload); + verify(mockCache).get(); + } + + @Test + public void testConstructorThrowsExceptionForInvalidInterval() { + dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.PayloadCache mockCache = mock(dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.PayloadCache.class); + dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.PayloadCacheOptions options = dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.PayloadCacheOptions.builder() + .updateIntervalSeconds(0) + .build(); + + dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.PayloadCacheWrapper.PayloadCacheWrapperBuilder payloadCacheWrapperBuilder = dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.PayloadCacheWrapper.builder() + .payloadCache(mockCache) + .payloadCacheOptions(options); + IllegalArgumentException exception = assertThrows( + IllegalArgumentException.class, + payloadCacheWrapperBuilder::build + ); + + assertEquals("pollIntervalSeconds must be larger than 0", exception.getMessage()); + } + + @Test + public void testUpdateSkipsWhenIntervalNotPassed() { + dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.PayloadCache mockCache = mock(dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.PayloadCache.class); + dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.PayloadCacheOptions options = dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.PayloadCacheOptions.builder() + .updateIntervalSeconds(600) + .build(); + dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.PayloadCacheWrapper wrapper = dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.PayloadCacheWrapper.builder() + .payloadCache(mockCache) + .payloadCacheOptions(options) + .build(); + + String initialPayload = "initial-payload"; + wrapper.updatePayloadIfNeeded(initialPayload); + + String newPayload = "new-payload"; + wrapper.updatePayloadIfNeeded(newPayload); + + verify(mockCache, times(1)).put(initialPayload); + verify(mockCache, never()).put(newPayload); + } + + @Test + public void testUpdatePayloadIfNeededHandlesPutException() { + dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.PayloadCache mockCache = mock(dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.PayloadCache.class); + dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.PayloadCacheOptions options = dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.PayloadCacheOptions.builder() + .updateIntervalSeconds(600) + .build(); + dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.PayloadCacheWrapper wrapper = dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.PayloadCacheWrapper.builder() + .payloadCache(mockCache) + .payloadCacheOptions(options) + .build(); + String testPayload = "test-payload"; + + doThrow(new RuntimeException("put exception")).when(mockCache).put(testPayload); + + wrapper.updatePayloadIfNeeded(testPayload); + + verify(mockCache).put(testPayload); + } + + @Test + public void testUpdatePayloadIfNeededUpdatesCacheAfterInterval() { + dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.PayloadCache mockCache = mock(dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.PayloadCache.class); + dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.PayloadCacheOptions options = dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.PayloadCacheOptions.builder() + .updateIntervalSeconds(1) // 1 second interval for quick test + .build(); + dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.PayloadCacheWrapper wrapper = dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.PayloadCacheWrapper.builder() + .payloadCache(mockCache) + .payloadCacheOptions(options) + .build(); + + String initialPayload = "initial-payload"; + String newPayload = "new-payload"; + + wrapper.updatePayloadIfNeeded(initialPayload); + delay(1100); + wrapper.updatePayloadIfNeeded(newPayload); + + verify(mockCache).put(initialPayload); + verify(mockCache).put(newPayload); + } + + @Test + public void testGetReturnsNullWhenCacheGetThrowsException() { + dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.PayloadCache mockCache = mock(dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.PayloadCache.class); + dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.PayloadCacheOptions options = dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.PayloadCacheOptions.builder() + .updateIntervalSeconds(600) + .build(); + dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.PayloadCacheWrapper wrapper = dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.PayloadCacheWrapper.builder() + .payloadCache(mockCache) + .payloadCacheOptions(options) + .build(); + + when(mockCache.get()).thenThrow(new RuntimeException("Cache get failed")); + + String result = wrapper.get(); + + assertNull(result); + + verify(mockCache).get(); + } + + @Test + public void test_get_returns_cached_payload() { + dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.PayloadCache mockCache = mock(dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.PayloadCache.class); + dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.PayloadCacheOptions options = dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.PayloadCacheOptions.builder() + .updateIntervalSeconds(600) + .build(); + dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.PayloadCacheWrapper wrapper = dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.PayloadCacheWrapper.builder() + .payloadCache(mockCache) + .payloadCacheOptions(options) + .build(); + String expectedPayload = "cached-payload"; + when(mockCache.get()).thenReturn(expectedPayload); + + String actualPayload = wrapper.get(); + + assertEquals(expectedPayload, actualPayload); + + verify(mockCache).get(); + } + + @Test + public void test_first_call_updates_cache() { + dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.PayloadCache mockCache = mock(dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.PayloadCache.class); + dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.PayloadCacheOptions options = dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.PayloadCacheOptions.builder() + .updateIntervalSeconds(600) + .build(); + dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.PayloadCacheWrapper wrapper = dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.PayloadCacheWrapper.builder() + .payloadCache(mockCache) + .payloadCacheOptions(options) + .build(); + + String testPayload = "initial-payload"; + + wrapper.updatePayloadIfNeeded(testPayload); + + verify(mockCache).put(testPayload); + } + + @Test + public void test_update_payload_once_within_interval() { + dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.PayloadCache mockCache = mock(dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.PayloadCache.class); + dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.PayloadCacheOptions options = dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.PayloadCacheOptions.builder() + .updateIntervalSeconds(1) // 1 second interval + .build(); + dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.PayloadCacheWrapper wrapper = dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.PayloadCacheWrapper.builder() + .payloadCache(mockCache) + .payloadCacheOptions(options) + .build(); + + String testPayload = "test-payload"; + + wrapper.updatePayloadIfNeeded(testPayload); + wrapper.updatePayloadIfNeeded(testPayload); + + verify(mockCache, times(1)).put(testPayload); + } + + @SneakyThrows + @Test + public void test_last_update_time_ms_updated_after_successful_cache_update() { + dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.PayloadCache mockCache = mock(dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.PayloadCache.class); + dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.PayloadCacheOptions options = dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.PayloadCacheOptions.builder() + .updateIntervalSeconds(600) + .build(); + dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.PayloadCacheWrapper wrapper = dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.PayloadCacheWrapper.builder() + .payloadCache(mockCache) + .payloadCacheOptions(options) + .build(); + String testPayload = "test-payload"; + + wrapper.updatePayloadIfNeeded(testPayload); + + verify(mockCache).put(testPayload); + + Field lastUpdateTimeMsField = dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.PayloadCacheWrapper.class.getDeclaredField("lastUpdateTimeMs"); + lastUpdateTimeMsField.setAccessible(true); + long lastUpdateTimeMs = (Long) lastUpdateTimeMsField.get(wrapper); + + assertTrue(System.currentTimeMillis() - lastUpdateTimeMs < 1000, + "lastUpdateTimeMs should be updated to current time"); + } + + @Test + public void test_update_payload_if_needed_respects_update_interval() { + dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.PayloadCache mockCache = mock(PayloadCache.class); + dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.PayloadCacheOptions options = PayloadCacheOptions.builder() + .updateIntervalSeconds(600) + .build(); + dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.PayloadCacheWrapper wrapper = spy(PayloadCacheWrapper.builder() + .payloadCache(mockCache) + .payloadCacheOptions(options) + .build()); + + String testPayload = "test-payload"; + long initialTime = System.currentTimeMillis(); + long updateIntervalMs = options.getUpdateIntervalSeconds() * 1000L; + + doReturn(initialTime).when(wrapper).getCurrentTimeMillis(); + + // First update should succeed + wrapper.updatePayloadIfNeeded(testPayload); + + // Verify the payload was updated + verify(mockCache).put(testPayload); + + // Attempt to update before interval has passed + doReturn(initialTime + updateIntervalMs - 1).when(wrapper).getCurrentTimeMillis(); + wrapper.updatePayloadIfNeeded(testPayload); + + // Verify the payload was not updated again + verify(mockCache, times(1)).put(testPayload); + + // Update after interval has passed + doReturn(initialTime + updateIntervalMs + 1).when(wrapper).getCurrentTimeMillis(); + wrapper.updatePayloadIfNeeded(testPayload); + + // Verify the payload was updated again + verify(mockCache, times(2)).put(testPayload); + } + +} diff --git a/tools/flagd-http-connector/src/test/resources/simplelogger.properties b/tools/flagd-http-connector/src/test/resources/simplelogger.properties new file mode 100644 index 000000000..d9d489e82 --- /dev/null +++ b/tools/flagd-http-connector/src/test/resources/simplelogger.properties @@ -0,0 +1,3 @@ +org.org.slf4j.simpleLogger.defaultLogLevel=debug + +io.grpc.level=trace From 60386134b97f71f3737f3c232b90039ceb23e874 Mon Sep 17 00:00:00 2001 From: liran2000 Date: Thu, 24 Apr 2025 17:42:39 +0300 Subject: [PATCH 07/40] move to tool - draft - cont. Signed-off-by: liran2000 --- pom.xml | 1 + .../providers/flagd/util/ConcurrentUtils.java | 61 ------ tools/flagd-http-connector/CHANGELOG.md | 35 ++++ tools/flagd-http-connector/README.md | 81 ++++++++ tools/flagd-http-connector/pom.xml | 54 ++++++ .../connector/sync/http/HttpCacheFetcher.java | 49 +++++ .../connector/sync/http/HttpConnector.java | 183 ++++++++++++++++++ .../sync/http/HttpConnectorOptions.java | 125 ++++++++++++ .../connector/sync/http/PayloadCache.java | 6 + .../sync/http/PayloadCacheOptions.java | 24 +++ .../sync/http/PayloadCacheWrapper.java | 59 ++++++ .../sync/http/HttpCacheFetcherTest.java | 41 ++-- .../http/HttpConnectorIntegrationTest.java | 8 +- .../sync/http/HttpConnectorOptionsTest.java | 74 ++++--- .../sync/http/HttpConnectorTest.java | 106 ++++++---- .../sync/http/PayloadCacheWrapperTest.java | 73 ++++--- tools/flagd-http-connector/version.txt | 1 + 17 files changed, 775 insertions(+), 206 deletions(-) delete mode 100644 providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/util/ConcurrentUtils.java create mode 100644 tools/flagd-http-connector/CHANGELOG.md create mode 100644 tools/flagd-http-connector/README.md create mode 100644 tools/flagd-http-connector/pom.xml create mode 100644 tools/flagd-http-connector/src/main/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/HttpCacheFetcher.java create mode 100644 tools/flagd-http-connector/src/main/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/HttpConnector.java create mode 100644 tools/flagd-http-connector/src/main/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/HttpConnectorOptions.java create mode 100644 tools/flagd-http-connector/src/main/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/PayloadCache.java create mode 100644 tools/flagd-http-connector/src/main/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/PayloadCacheOptions.java create mode 100644 tools/flagd-http-connector/src/main/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/PayloadCacheWrapper.java create mode 100644 tools/flagd-http-connector/version.txt diff --git a/pom.xml b/pom.xml index 2d1a97ce2..85bdce3e8 100644 --- a/pom.xml +++ b/pom.xml @@ -40,6 +40,7 @@ providers/configcat providers/statsig providers/multiprovider + tools/flagd-http-connector diff --git a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/util/ConcurrentUtils.java b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/util/ConcurrentUtils.java deleted file mode 100644 index b57faca77..000000000 --- a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/util/ConcurrentUtils.java +++ /dev/null @@ -1,61 +0,0 @@ -package dev.openfeature.contrib.providers.flagd.util; - -import java.util.concurrent.ExecutorService; -import java.util.concurrent.TimeUnit; -import lombok.AccessLevel; -import lombok.NoArgsConstructor; -import lombok.extern.slf4j.Slf4j; - -/** - * Concurrent / Concurrency utilities. - * - * @author Liran Mendelovich - */ -@NoArgsConstructor(access = AccessLevel.PRIVATE) -@Slf4j -public class ConcurrentUtils { - - /** - * Graceful shutdown a thread pool.
- * See - * https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/ExecutorService.html - * - * @param pool thread pool - * @param timeoutSeconds grace period timeout in seconds - timeout can be twice than this value, - * as first it waits for existing tasks to terminate, then waits for cancelled tasks to - * terminate. - */ - public static void shutdownAndAwaitTermination(ExecutorService pool, int timeoutSeconds) { - if (pool == null) { - return; - } - - // Disable new tasks from being submitted - pool.shutdown(); - try { - - // Wait a while for existing tasks to terminate - if (!pool.awaitTermination(timeoutSeconds, TimeUnit.SECONDS)) { - - // Cancel currently executing tasks - best effort, based on interrupt handling - // implementation. - pool.shutdownNow(); - - // Wait a while for tasks to respond to being cancelled - if (!pool.awaitTermination(timeoutSeconds, TimeUnit.SECONDS)) { - log.error("Thread pool did not shutdown all tasks after the timeout: {} seconds.", timeoutSeconds); - } - } - } catch (InterruptedException e) { - - log.info("Current thread interrupted during shutdownAndAwaitTermination, calling shutdownNow."); - - // (Re-)Cancel if current thread also interrupted - pool.shutdownNow(); - - // Preserve interrupt status - Thread.currentThread().interrupt(); - } - } -} diff --git a/tools/flagd-http-connector/CHANGELOG.md b/tools/flagd-http-connector/CHANGELOG.md new file mode 100644 index 000000000..069e8ebdc --- /dev/null +++ b/tools/flagd-http-connector/CHANGELOG.md @@ -0,0 +1,35 @@ +# Changelog + +## [0.1.2](https://github.com/open-feature/java-sdk-contrib/compare/dev.openfeature.contrib.tools.junitopenfeature-v0.1.1...dev.openfeature.contrib.tools.junitopenfeature-v0.1.2) (2024-12-03) + + +### ✨ New Features + +* added interception of parameterized tests to Junit OpenFeature Extension ([#1093](https://github.com/open-feature/java-sdk-contrib/issues/1093)) ([a78c906](https://github.com/open-feature/java-sdk-contrib/commit/a78c906b24b53f7d25eb01aad85ed614eb30ca05)) + +## [0.1.1](https://github.com/open-feature/java-sdk-contrib/compare/dev.openfeature.contrib.tools.junitopenfeature-v0.1.0...dev.openfeature.contrib.tools.junitopenfeature-v0.1.1) (2024-09-27) + + +### 🐛 Bug Fixes + +* race condition causing default when multiple flags are used ([#983](https://github.com/open-feature/java-sdk-contrib/issues/983)) ([356a973](https://github.com/open-feature/java-sdk-contrib/commit/356a973cf2b6ddf82b8311ea200fa30df4f1d048)) + +## [0.1.0](https://github.com/open-feature/java-sdk-contrib/compare/dev.openfeature.contrib.tools.junitopenfeature-v0.0.3...dev.openfeature.contrib.tools.junitopenfeature-v0.1.0) (2024-09-25) + + +### 🐛 Bug Fixes + +* **deps:** update dependency org.apache.commons:commons-lang3 to v3.17.0 ([#932](https://github.com/open-feature/java-sdk-contrib/issues/932)) ([c598d9f](https://github.com/open-feature/java-sdk-contrib/commit/c598d9f0a61f2324fb85d72fdfea34811283c575)) + + +### 🐛 Bug Fixes + +* added missing dependency and installation instruction ([#895](https://github.com/open-feature/java-sdk-contrib/issues/895)) ([6748d02](https://github.com/open-feature/java-sdk-contrib/commit/6748d02403f0ceecb6cb9ecdfb2fecf98423a7db)) +* **deps:** update dependency org.apache.commons:commons-lang3 to v3.16.0 ([#908](https://github.com/open-feature/java-sdk-contrib/issues/908)) ([d21cfe3](https://github.com/open-feature/java-sdk-contrib/commit/d21cfe3ac7da1ff6e1a4dc2ee4b0db5c24ed4847)) + +## [0.0.2](https://github.com/open-feature/java-sdk-contrib/compare/dev.openfeature.contrib.tools.junitopenfeature-v0.0.1...dev.openfeature.contrib.tools.junitopenfeature-v0.0.2) (2024-07-29) + + +### ✨ New Features + +* Add JUnit5 extension for OpenFeature ([#888](https://github.com/open-feature/java-sdk-contrib/issues/888)) ([9fff9db](https://github.com/open-feature/java-sdk-contrib/commit/9fff9db4bcee3c3ae8128a1b2fb040f53df1d5ed)) diff --git a/tools/flagd-http-connector/README.md b/tools/flagd-http-connector/README.md new file mode 100644 index 000000000..f6925f4bf --- /dev/null +++ b/tools/flagd-http-connector/README.md @@ -0,0 +1,81 @@ +# Http Connector + +## Introduction +Http Connector is a tool for [flagd](https://github.com/open-feature/flagd) in-process resolver. + +This mode performs flag evaluations locally (in-process). +Flag configurations for evaluation are obtained via gRPC protocol using +[sync protobuf schema](https://buf.build/open-feature/flagd/file/main:sync/v1/sync_service.proto) service definition. + +## Http Connector functionality + +HttpConnector is responsible for polling data from a specified URL at regular intervals. +It is leveraging Http cache mechanism with 'ETag' header, then when receiving 304 Not Modified response, +reducing traffic, reducing rate limits effects and changes updates. Can be enabled via useHttpCache option. +The implementation is using Java HttpClient. + +## Use cases and benefits +* Reduce infrastructure/devops work, without additional containers needed. +* Use as an additional provider for fallback / internal backup service via multi-provider. + +### What happens if the Http source is down when application is starting ? + +It supports optional fail-safe initialization via cache, such that on initial fetch error following by +source downtime window, initial payload is taken from cache to avoid starting with default values until +the source is back up. Therefore, the cache ttl expected to be higher than the expected source +down-time to recover from during initialization. + +### Sample flow +Sample flow can use: +- Github as the flags payload source. +- Redis cache as a fail-safe initialization cache. + +Sample flow of initialization during Github down-time window, showing that application can still use flags +values as fetched from cache. +```mermaid +sequenceDiagram + participant Provider + participant Github + participant Redis + + break source downtime + Provider->>Github: initialize + Github->>Provider: failure + end + Provider->>Redis: fetch + Redis->>Provider: last payload + +``` + +## Usage + +### Installation + +```xml + + dev.openfeature.contrib.tools + flagd-http-connector + 0.0.1 + +``` + + +### Usage example + +```java + +HttpConnectorOptions httpConnectorOptions = HttpConnectorOptions.builder() + .url("http://example.com/flags") + .build(); +HttpConnector connector = HttpConnector.builder() + .httpConnectorOptions(httpConnectorOptions) + .build(); + +FlagdOptions options = + FlagdOptions.builder() + .resolverType(Config.Resolver.IN_PROCESS) + .customConnector(connector) + .build(); + +FlagdProvider flagdProvider = new FlagdProvider(options); +``` diff --git a/tools/flagd-http-connector/pom.xml b/tools/flagd-http-connector/pom.xml new file mode 100644 index 000000000..8945c7e97 --- /dev/null +++ b/tools/flagd-http-connector/pom.xml @@ -0,0 +1,54 @@ + + + 4.0.0 + + dev.openfeature.contrib + parent + 0.1.0 + ../../pom.xml + + dev.openfeature.contrib.tools + flagd-http-connector + 0.0.1 + + flagd-http-connector + Flagd Http Connector + https://openfeature.dev + + + + liran2000 + Liran Mendelovich + OpenFeature + https://openfeature.dev/ + + + + + + dev.openfeature.contrib.providers + flagd + 0.11.8 + + + + org.apache.commons + commons-lang3 + 3.17.0 + + + + org.junit.jupiter + junit-jupiter-api + test + + + org.junit.jupiter + junit-jupiter-api + test + + + + + diff --git a/tools/flagd-http-connector/src/main/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/HttpCacheFetcher.java b/tools/flagd-http-connector/src/main/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/HttpCacheFetcher.java new file mode 100644 index 000000000..0cc069032 --- /dev/null +++ b/tools/flagd-http-connector/src/main/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/HttpCacheFetcher.java @@ -0,0 +1,49 @@ +package dev.openfeature.contrib.tools.flagd.resolver.process.storage.connector.sync.http; + +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; + +/** + * Fetches content from a given HTTP endpoint using caching headers to optimize network usage. + * If cached ETag or Last-Modified values are available, they are included in the request headers + * to potentially receive a 304 Not Modified response, reducing data transfer. + * Updates the cached ETag and Last-Modified values upon receiving a 200 OK response. + * It does not store the cached response, assuming not needed after first successful fetching. + * Non thread-safe. + * + * @param httpClient the HTTP client used to send the request + * @param httpRequestBuilder the builder for constructing the HTTP request + * @return the HTTP response received from the server + */ +@Slf4j +public class HttpCacheFetcher { + private String cachedETag = null; + private String cachedLastModified = null; + + @SneakyThrows + public HttpResponse fetchContent(HttpClient httpClient, HttpRequest.Builder httpRequestBuilder) { + if (cachedETag != null) { + httpRequestBuilder.header("If-None-Match", cachedETag); + } + if (cachedLastModified != null) { + httpRequestBuilder.header("If-Modified-Since", cachedLastModified); + } + + HttpRequest request = httpRequestBuilder.build(); + HttpResponse httpResponse = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); + + if (httpResponse.statusCode() == 200) { + if (httpResponse.headers() != null) { + cachedETag = httpResponse.headers().firstValue("ETag").orElse(null); + cachedLastModified = httpResponse.headers().firstValue("Last-Modified").orElse(null); + } + log.debug("fetched new content"); + } else if (httpResponse.statusCode() == 304) { + log.debug("got 304 Not Modified"); + } + return httpResponse; + } +} diff --git a/tools/flagd-http-connector/src/main/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/HttpConnector.java b/tools/flagd-http-connector/src/main/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/HttpConnector.java new file mode 100644 index 000000000..3eb8a116d --- /dev/null +++ b/tools/flagd-http-connector/src/main/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/HttpConnector.java @@ -0,0 +1,183 @@ +package dev.openfeature.contrib.tools.flagd.resolver.process.storage.connector.sync.http; + +import static java.net.http.HttpClient.Builder.NO_PROXY; + +import dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.QueuePayload; +import dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.QueuePayloadType; +import dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.QueueSource; +import java.io.IOException; +import java.net.InetSocketAddress; +import java.net.ProxySelector; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.time.Duration; +import java.util.Map; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import dev.openfeature.contrib.tools.flagd.resolver.process.storage.connector.sync.http.util.ConcurrentUtils; +import lombok.Builder; +import lombok.NonNull; +import lombok.extern.slf4j.Slf4j; + +/** + * HttpConnector is responsible for polling data from a specified URL at regular intervals. + * Notice rate limits for polling http sources like Github. + * It implements the QueueSource interface to enqueue and dequeue change messages. + * The class supports configurable parameters such as poll interval, request timeout, and proxy settings. + * It uses a ScheduledExecutorService to schedule polling tasks and an ExecutorService for HTTP client execution. + * The class also provides methods to initialize, retrieve the stream queue, and shutdown the connector gracefully. + * It supports optional fail-safe initialization via cache. + * + * See readme - Http Connector section. + */ +@Slf4j +public class HttpConnector implements QueueSource { + + private Integer pollIntervalSeconds; + private Integer requestTimeoutSeconds; + private BlockingQueue queue; + private HttpClient client; + private ExecutorService httpClientExecutor; + private ScheduledExecutorService scheduler; + private Map headers; + private PayloadCacheWrapper payloadCacheWrapper; + private PayloadCache payloadCache; + private HttpCacheFetcher httpCacheFetcher; + + @NonNull + private String url; + + @Builder + public HttpConnector(HttpConnectorOptions httpConnectorOptions) { + this.pollIntervalSeconds = httpConnectorOptions.getPollIntervalSeconds(); + this.requestTimeoutSeconds = httpConnectorOptions.getRequestTimeoutSeconds(); + ProxySelector proxySelector = NO_PROXY; + if (httpConnectorOptions.getProxyHost() != null && httpConnectorOptions.getProxyPort() != null) { + proxySelector = ProxySelector.of(new InetSocketAddress(httpConnectorOptions.getProxyHost(), + httpConnectorOptions.getProxyPort())); + } + this.url = httpConnectorOptions.getUrl(); + this.headers = httpConnectorOptions.getHeaders(); + this.httpClientExecutor = httpConnectorOptions.getHttpClientExecutor(); + scheduler = Executors.newScheduledThreadPool(httpConnectorOptions.getScheduledThreadPoolSize()); + this.client = HttpClient.newBuilder() + .connectTimeout(Duration.ofSeconds(httpConnectorOptions.getConnectTimeoutSeconds())) + .proxy(proxySelector) + .executor(this.httpClientExecutor) + .build(); + this.queue = new LinkedBlockingQueue<>(httpConnectorOptions.getLinkedBlockingQueueCapacity()); + this.payloadCache = httpConnectorOptions.getPayloadCache(); + if (payloadCache != null) { + this.payloadCacheWrapper = PayloadCacheWrapper.builder() + .payloadCache(payloadCache) + .payloadCacheOptions(httpConnectorOptions.getPayloadCacheOptions()) + .build(); + } + if (Boolean.TRUE.equals(httpConnectorOptions.getUseHttpCache())) { + httpCacheFetcher = new HttpCacheFetcher(); + } + } + + @Override + public void init() throws Exception { + log.info("init Http Connector"); + } + + @Override + public BlockingQueue getStreamQueue() { + boolean success = fetchAndUpdate(); + if (!success) { + log.info("failed initial fetch"); + if (payloadCache != null) { + updateFromCache(); + } + } + Runnable pollTask = buildPollTask(); + scheduler.scheduleWithFixedDelay(pollTask, pollIntervalSeconds, pollIntervalSeconds, TimeUnit.SECONDS); + return queue; + } + + private void updateFromCache() { + log.info("taking initial payload from cache to avoid starting with default values"); + String flagData = payloadCache.get(); + if (flagData == null) { + log.debug("got null from cache"); + return; + } + if (!this.queue.offer(new QueuePayload(QueuePayloadType.DATA, flagData))) { + log.warn("init: Unable to offer file content to queue: queue is full"); + } + } + + protected Runnable buildPollTask() { + return this::fetchAndUpdate; + } + + private boolean fetchAndUpdate() { + HttpRequest.Builder requestBuilder = HttpRequest.newBuilder() + .uri(URI.create(url)) + .timeout(Duration.ofSeconds(requestTimeoutSeconds)) + .GET(); + headers.forEach(requestBuilder::header); + + HttpResponse response; + try { + log.debug("fetching response"); + response = execute(requestBuilder); + } catch (IOException e) { + log.info("could not fetch", e); + return false; + } catch (Exception e) { + log.debug("exception", e); + return false; + } + log.debug("fetched response"); + String payload = response.body(); + if (!isSuccessful(response)) { + log.info("received non-successful status code: {} {}", response.statusCode(), payload); + return false; + } else if (response.statusCode() == 304) { + log.debug("got 304 Not Modified, skipping update"); + return false; + } + if (payload == null) { + log.debug("payload is null"); + return false; + } + log.debug("adding payload to queue"); + if (!this.queue.offer(new QueuePayload(QueuePayloadType.DATA, payload))) { + log.warn("Unable to offer file content to queue: queue is full"); + return false; + } + if (payloadCacheWrapper != null) { + log.debug("scheduling cache update if needed"); + scheduler.execute(() -> + payloadCacheWrapper.updatePayloadIfNeeded(payload) + ); + } + return payload != null; + } + + private static boolean isSuccessful(HttpResponse response) { + return response.statusCode() == 200 || response.statusCode() == 304; + } + + protected HttpResponse execute(HttpRequest.Builder requestBuilder) throws IOException, InterruptedException { + if (httpCacheFetcher != null) { + return httpCacheFetcher.fetchContent(client, requestBuilder); + } + return client.send(requestBuilder.build(), HttpResponse.BodyHandlers.ofString()); + } + + @Override + public void shutdown() throws InterruptedException { + ConcurrentUtils.shutdownAndAwaitTermination(scheduler, 10); + ConcurrentUtils.shutdownAndAwaitTermination(httpClientExecutor, 10); + } +} diff --git a/tools/flagd-http-connector/src/main/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/HttpConnectorOptions.java b/tools/flagd-http-connector/src/main/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/HttpConnectorOptions.java new file mode 100644 index 000000000..740d54ddf --- /dev/null +++ b/tools/flagd-http-connector/src/main/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/HttpConnectorOptions.java @@ -0,0 +1,125 @@ +package dev.openfeature.contrib.tools.flagd.resolver.process.storage.connector.sync.http; + +import java.net.URL; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import lombok.Builder; +import lombok.Getter; +import lombok.NonNull; +import lombok.SneakyThrows; + +@Getter +public class HttpConnectorOptions { + + @Builder.Default + private Integer pollIntervalSeconds = 60; + @Builder.Default + private Integer connectTimeoutSeconds = 10; + @Builder.Default + private Integer requestTimeoutSeconds = 10; + @Builder.Default + private Integer linkedBlockingQueueCapacity = 100; + @Builder.Default + private Integer scheduledThreadPoolSize = 2; + @Builder.Default + private Map headers = new HashMap<>(); + @Builder.Default + private ExecutorService httpClientExecutor = Executors.newFixedThreadPool(1); + @Builder.Default + private String proxyHost; + @Builder.Default + private Integer proxyPort; + @Builder.Default + private PayloadCacheOptions payloadCacheOptions; + @Builder.Default + private PayloadCache payloadCache; + @Builder.Default + private Boolean useHttpCache; + @NonNull + private String url; + + @Builder + public HttpConnectorOptions(Integer pollIntervalSeconds, Integer linkedBlockingQueueCapacity, + Integer scheduledThreadPoolSize, Integer requestTimeoutSeconds, Integer connectTimeoutSeconds, String url, + Map headers, ExecutorService httpClientExecutor, String proxyHost, Integer proxyPort, + PayloadCacheOptions payloadCacheOptions, PayloadCache payloadCache, Boolean useHttpCache) { + validate(url, pollIntervalSeconds, linkedBlockingQueueCapacity, scheduledThreadPoolSize, requestTimeoutSeconds, + connectTimeoutSeconds, proxyHost, proxyPort, payloadCacheOptions, payloadCache); + if (pollIntervalSeconds != null) { + this.pollIntervalSeconds = pollIntervalSeconds; + } + if (linkedBlockingQueueCapacity != null) { + this.linkedBlockingQueueCapacity = linkedBlockingQueueCapacity; + } + if (scheduledThreadPoolSize != null) { + this.scheduledThreadPoolSize = scheduledThreadPoolSize; + } + if (requestTimeoutSeconds != null) { + this.requestTimeoutSeconds = requestTimeoutSeconds; + } + if (connectTimeoutSeconds != null) { + this.connectTimeoutSeconds = connectTimeoutSeconds; + } + this.url = url; + if (headers != null) { + this.headers = headers; + } + if (httpClientExecutor != null) { + this.httpClientExecutor = httpClientExecutor; + } + if (proxyHost != null) { + this.proxyHost = proxyHost; + } + if (proxyPort != null) { + this.proxyPort = proxyPort; + } + if (payloadCache != null) { + this.payloadCache = payloadCache; + } + if (payloadCacheOptions != null) { + this.payloadCacheOptions = payloadCacheOptions; + } + if (useHttpCache != null) { + this.useHttpCache = useHttpCache; + } + } + + @SneakyThrows + private void validate(String url, Integer pollIntervalSeconds, Integer linkedBlockingQueueCapacity, + Integer scheduledThreadPoolSize, Integer requestTimeoutSeconds, Integer connectTimeoutSeconds, + String proxyHost, Integer proxyPort, PayloadCacheOptions payloadCacheOptions, + PayloadCache payloadCache) { + new URL(url).toURI(); + if (linkedBlockingQueueCapacity != null && (linkedBlockingQueueCapacity < 1 || linkedBlockingQueueCapacity > 1000)) { + throw new IllegalArgumentException("linkedBlockingQueueCapacity must be between 1 and 1000"); + } + if (scheduledThreadPoolSize != null && (scheduledThreadPoolSize < 1 || scheduledThreadPoolSize > 10)) { + throw new IllegalArgumentException("scheduledThreadPoolSize must be between 1 and 10"); + } + if (requestTimeoutSeconds != null && (requestTimeoutSeconds < 1 || requestTimeoutSeconds > 60)) { + throw new IllegalArgumentException("requestTimeoutSeconds must be between 1 and 60"); + } + if (connectTimeoutSeconds != null && (connectTimeoutSeconds < 1 || connectTimeoutSeconds > 60)) { + throw new IllegalArgumentException("connectTimeoutSeconds must be between 1 and 60"); + } + if (pollIntervalSeconds != null && (pollIntervalSeconds < 1 || pollIntervalSeconds > 600)) { + throw new IllegalArgumentException("pollIntervalSeconds must be between 1 and 600"); + } + if (proxyPort != null && (proxyPort < 1 || proxyPort > 65535)) { + throw new IllegalArgumentException("proxyPort must be between 1 and 65535"); + } + if (proxyHost != null && proxyPort == null ) { + throw new IllegalArgumentException("proxyPort must be set if proxyHost is set"); + } else if (proxyHost == null && proxyPort != null) { + throw new IllegalArgumentException("proxyHost must be set if proxyPort is set"); + } + if (payloadCacheOptions != null && payloadCache == null) { + throw new IllegalArgumentException("payloadCache must be set if payloadCacheOptions is set"); + } + if (payloadCache != null && payloadCacheOptions == null) { + throw new IllegalArgumentException("payloadCacheOptions must be set if payloadCache is set"); + } + } +} diff --git a/tools/flagd-http-connector/src/main/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/PayloadCache.java b/tools/flagd-http-connector/src/main/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/PayloadCache.java new file mode 100644 index 000000000..4af5f5f1d --- /dev/null +++ b/tools/flagd-http-connector/src/main/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/PayloadCache.java @@ -0,0 +1,6 @@ +package dev.openfeature.contrib.tools.flagd.resolver.process.storage.connector.sync.http; + +public interface PayloadCache { + public void put(String payload); + public String get(); +} diff --git a/tools/flagd-http-connector/src/main/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/PayloadCacheOptions.java b/tools/flagd-http-connector/src/main/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/PayloadCacheOptions.java new file mode 100644 index 000000000..9ca0dabcc --- /dev/null +++ b/tools/flagd-http-connector/src/main/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/PayloadCacheOptions.java @@ -0,0 +1,24 @@ +package dev.openfeature.contrib.tools.flagd.resolver.process.storage.connector.sync.http; + +import lombok.Builder; +import lombok.Getter; + +/** + * Represents configuration options for caching payloads. + *

+ * This class provides options to configure the caching behavior, + * specifically the interval at which the cache should be updated. + *

+ *

+ * The default update interval is set to 30 minutes. + * Change it typically to a value according to cache ttl and tradeoff with not updating it too much for + * corner cases. + *

+ */ +@Builder +@Getter +public class PayloadCacheOptions { + + @Builder.Default + private int updateIntervalSeconds = 60 * 30; // 30 minutes +} diff --git a/tools/flagd-http-connector/src/main/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/PayloadCacheWrapper.java b/tools/flagd-http-connector/src/main/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/PayloadCacheWrapper.java new file mode 100644 index 000000000..be213403e --- /dev/null +++ b/tools/flagd-http-connector/src/main/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/PayloadCacheWrapper.java @@ -0,0 +1,59 @@ +package dev.openfeature.contrib.tools.flagd.resolver.process.storage.connector.sync.http; + +import lombok.Builder; +import lombok.extern.slf4j.Slf4j; + +/** + * A wrapper class for managing a payload cache with a specified update interval. + * This class ensures that the cache is only updated if the specified time interval + * has passed since the last update. It logs debug messages when updates are skipped + * and error messages if the update process fails. + * Not thread-safe. + * + *

Usage involves creating an instance with {@link PayloadCacheOptions} to set + * the update interval, and then using {@link #updatePayloadIfNeeded(String)} to + * conditionally update the cache and {@link #get()} to retrieve the cached payload.

+ */ +@Slf4j +public class PayloadCacheWrapper { + private long lastUpdateTimeMs; + private long updateIntervalMs; + private PayloadCache payloadCache; + + @Builder + public PayloadCacheWrapper(PayloadCache payloadCache, PayloadCacheOptions payloadCacheOptions) { + if (payloadCacheOptions.getUpdateIntervalSeconds() < 1) { + throw new IllegalArgumentException("pollIntervalSeconds must be larger than 0"); + } + this.updateIntervalMs = payloadCacheOptions.getUpdateIntervalSeconds() * 1000L; + this.payloadCache = payloadCache; + } + + public void updatePayloadIfNeeded(String payload) { + if ((getCurrentTimeMillis() - lastUpdateTimeMs) < updateIntervalMs) { + log.debug("not updating payload, updateIntervalMs not reached"); + return; + } + + try { + log.debug("updating payload"); + payloadCache.put(payload); + lastUpdateTimeMs = getCurrentTimeMillis(); + } catch (Exception e) { + log.error("failed updating cache", e); + } + } + + protected long getCurrentTimeMillis() { + return System.currentTimeMillis(); + } + + public String get() { + try { + return payloadCache.get(); + } catch (Exception e) { + log.error("failed getting from cache", e); + return null; + } + } +} diff --git a/tools/flagd-http-connector/src/test/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/HttpCacheFetcherTest.java b/tools/flagd-http-connector/src/test/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/HttpCacheFetcherTest.java index 37e353917..fe49219ec 100644 --- a/tools/flagd-http-connector/src/test/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/HttpCacheFetcherTest.java +++ b/tools/flagd-http-connector/src/test/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/HttpCacheFetcherTest.java @@ -1,6 +1,6 @@ package dev.openfeature.contrib.tools.flagd.resolver.process.storage.connector.sync.http; -import static org.junit.Assert.assertNull; +import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.ArgumentMatchers.any; @@ -14,7 +14,6 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -import dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpCacheFetcher; import java.io.IOException; import java.lang.reflect.Field; import java.net.http.HttpClient; @@ -41,7 +40,7 @@ public void testFirstRequestSendsNoCacheHeaders() throws Exception { when(httpClientMock.send(eq(requestMock), any(HttpResponse.BodyHandler.class))).thenReturn(responseMock); when(responseMock.statusCode()).thenReturn(200); - dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpCacheFetcher fetcher = new dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpCacheFetcher(); + HttpCacheFetcher fetcher = new HttpCacheFetcher(); fetcher.fetchContent(httpClientMock, requestBuilderMock); verify(requestBuilderMock, never()).header(eq("If-None-Match"), anyString()); @@ -63,7 +62,7 @@ public void testResponseWith200ButNoCacheHeaders() throws Exception { when(headersMock.firstValue("ETag")).thenReturn(Optional.empty()); when(headersMock.firstValue("Last-Modified")).thenReturn(Optional.empty()); - dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpCacheFetcher fetcher = new dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpCacheFetcher(); + HttpCacheFetcher fetcher = new HttpCacheFetcher(); HttpResponse response = fetcher.fetchContent(httpClientMock, requestBuilderMock); assertEquals(200, response.statusCode()); @@ -89,7 +88,7 @@ public void testFetchContentReturnsHttpResponse() throws Exception { when(httpClientMock.send(eq(requestMock), any(HttpResponse.BodyHandler.class))).thenReturn(responseMock); when(responseMock.statusCode()).thenReturn(404); - dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpCacheFetcher fetcher = new dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpCacheFetcher(); + HttpCacheFetcher fetcher = new HttpCacheFetcher(); HttpResponse result = fetcher.fetchContent(httpClientMock, requestBuilderMock); assertEquals(responseMock, result); @@ -107,13 +106,13 @@ public void test200ResponseNoEtagOrLastModified() throws Exception { when(httpClientMock.send(eq(requestMock), any(HttpResponse.BodyHandler.class))).thenReturn(responseMock); when(responseMock.statusCode()).thenReturn(200); - dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpCacheFetcher fetcher = new dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpCacheFetcher(); + HttpCacheFetcher fetcher = new HttpCacheFetcher(); fetcher.fetchContent(httpClientMock, requestBuilderMock); - Field cachedETagField = dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpCacheFetcher.class.getDeclaredField("cachedETag"); + Field cachedETagField = HttpCacheFetcher.class.getDeclaredField("cachedETag"); cachedETagField.setAccessible(true); assertNull(cachedETagField.get(fetcher)); - Field cachedLastModifiedField = dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpCacheFetcher.class.getDeclaredField("cachedLastModified"); + Field cachedLastModifiedField = HttpCacheFetcher.class.getDeclaredField("cachedLastModified"); cachedLastModifiedField.setAccessible(true); assertNull(cachedLastModifiedField.get(fetcher)); } @@ -132,13 +131,13 @@ public void testUpdateCacheOn200Response() throws Exception { when(requestBuilderMock.build()).thenReturn(requestMock); when(httpClientMock.send(eq(requestMock), any(HttpResponse.BodyHandler.class))).thenReturn(responseMock); when(responseMock.statusCode()).thenReturn(200); - dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpCacheFetcher fetcher = new dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpCacheFetcher(); + HttpCacheFetcher fetcher = new HttpCacheFetcher(); fetcher.fetchContent(httpClientMock, requestBuilderMock); - Field cachedETagField = dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpCacheFetcher.class.getDeclaredField("cachedETag"); + Field cachedETagField = HttpCacheFetcher.class.getDeclaredField("cachedETag"); cachedETagField.setAccessible(true); assertEquals("etag-value", cachedETagField.get(fetcher)); - Field cachedLastModifiedField = dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpCacheFetcher.class.getDeclaredField("cachedLastModified"); + Field cachedLastModifiedField = HttpCacheFetcher.class.getDeclaredField("cachedLastModified"); cachedLastModifiedField.setAccessible(true); assertEquals("Wed, 21 Oct 2015 07:28:00 GMT", cachedLastModifiedField.get(fetcher)); } @@ -157,7 +156,7 @@ public void testRequestWithCachedEtagIncludesIfNoneMatchHeader() throws Exceptio when(httpClientMock.send(eq(requestMock), any(HttpResponse.BodyHandler.class))).thenReturn(responseMock); when(responseMock.statusCode()).thenReturn(200); - dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpCacheFetcher fetcher = new dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpCacheFetcher(); + HttpCacheFetcher fetcher = new HttpCacheFetcher(); fetcher.fetchContent(httpClientMock, requestBuilderMock); fetcher.fetchContent(httpClientMock, requestBuilderMock); @@ -166,7 +165,7 @@ public void testRequestWithCachedEtagIncludesIfNoneMatchHeader() throws Exceptio @Test public void testNullHttpClientOrRequestBuilder() { - dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpCacheFetcher fetcher = new dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpCacheFetcher(); + HttpCacheFetcher fetcher = new HttpCacheFetcher(); HttpRequest.Builder requestBuilderMock = mock(HttpRequest.Builder.class); assertThrows(NullPointerException.class, () -> { @@ -190,7 +189,7 @@ public void testResponseWithUnexpectedStatusCode() throws Exception { when(httpClientMock.send(eq(requestMock), any(HttpResponse.BodyHandler.class))).thenReturn(responseMock); when(responseMock.statusCode()).thenReturn(500); - dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpCacheFetcher fetcher = new dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpCacheFetcher(); + HttpCacheFetcher fetcher = new HttpCacheFetcher(); HttpResponse response = fetcher.fetchContent(httpClientMock, requestBuilderMock); assertEquals(500, response.statusCode()); @@ -212,7 +211,7 @@ public void testRequestIncludesIfModifiedSinceHeaderWhenLastModifiedCached() thr when(httpClientMock.send(eq(requestMock), any(HttpResponse.BodyHandler.class))).thenReturn(responseMock); when(responseMock.statusCode()).thenReturn(200); - dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpCacheFetcher fetcher = new dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpCacheFetcher(); + HttpCacheFetcher fetcher = new HttpCacheFetcher(); fetcher.fetchContent(httpClientMock, requestBuilderMock); fetcher.fetchContent(httpClientMock, requestBuilderMock); @@ -234,7 +233,7 @@ public void testCalls200And304Responses() throws Exception { when(responseMock200.statusCode()).thenReturn(200); when(responseMock304.statusCode()).thenReturn(304); - dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpCacheFetcher fetcher = new dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpCacheFetcher(); + HttpCacheFetcher fetcher = new HttpCacheFetcher(); fetcher.fetchContent(httpClientMock, requestBuilderMock); fetcher.fetchContent(httpClientMock, requestBuilderMock); @@ -254,11 +253,11 @@ public void testRequestIncludesBothEtagAndLastModifiedHeaders() throws Exception when(httpClientMock.send(eq(requestMock), any(HttpResponse.BodyHandler.class))).thenReturn(responseMock); when(responseMock.statusCode()).thenReturn(200); - dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpCacheFetcher fetcher = new dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpCacheFetcher(); - Field cachedETagField = dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpCacheFetcher.class.getDeclaredField("cachedETag"); + HttpCacheFetcher fetcher = new HttpCacheFetcher(); + Field cachedETagField = HttpCacheFetcher.class.getDeclaredField("cachedETag"); cachedETagField.setAccessible(true); cachedETagField.set(fetcher, "test-etag"); - Field cachedLastModifiedField = dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpCacheFetcher.class.getDeclaredField("cachedLastModified"); + Field cachedLastModifiedField = HttpCacheFetcher.class.getDeclaredField("cachedLastModified"); cachedLastModifiedField.setAccessible(true); cachedLastModifiedField.set(fetcher, "test-last-modified"); @@ -279,7 +278,7 @@ public void testHttpClientSendExceptionPropagation() { when(httpClientMock.send(eq(requestMock), any(HttpResponse.BodyHandler.class))) .thenThrow(new IOException("Network error")); - dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpCacheFetcher fetcher = new dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpCacheFetcher(); + HttpCacheFetcher fetcher = new HttpCacheFetcher(); assertThrows(IOException.class, () -> { fetcher.fetchContent(httpClientMock, requestBuilderMock); }); @@ -300,7 +299,7 @@ public void testOnlyEtagAndLastModifiedHeadersCached() throws Exception { when(httpClientMock.send(eq(requestMock), any(HttpResponse.BodyHandler.class))).thenReturn(responseMock); when(responseMock.statusCode()).thenReturn(200); - dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpCacheFetcher fetcher = new HttpCacheFetcher(); + HttpCacheFetcher fetcher = new HttpCacheFetcher(); fetcher.fetchContent(httpClientMock, requestBuilderMock); verify(requestBuilderMock, never()).header(eq("Some-Other-Header"), anyString()); diff --git a/tools/flagd-http-connector/src/test/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/HttpConnectorIntegrationTest.java b/tools/flagd-http-connector/src/test/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/HttpConnectorIntegrationTest.java index 75b69721a..bd198c1c4 100644 --- a/tools/flagd-http-connector/src/test/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/HttpConnectorIntegrationTest.java +++ b/tools/flagd-http-connector/src/test/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/HttpConnectorIntegrationTest.java @@ -1,12 +1,10 @@ package dev.openfeature.contrib.tools.flagd.resolver.process.storage.connector.sync.http; -import static dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpConnectorTest.delay; +import static dev.openfeature.contrib.tools.flagd.resolver.process.storage.connector.sync.http.HttpConnectorTest.delay; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assumptions.assumeTrue; import dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.QueuePayload; -import dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpConnector; -import dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpConnectorOptions; import java.util.concurrent.BlockingQueue; import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; @@ -28,11 +26,11 @@ class HttpConnectorIntegrationTest { @Test void testGithubRawContent() { assumeTrue(parseBoolean("integrationTestsEnabled")); - dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpConnector connector = null; + HttpConnector connector = null; try { String testUrl = "https://raw.githubusercontent.com/open-feature/java-sdk-contrib/58fe5da7d4e2f6f4ae2c1caf3411a01e84a1dc1a/providers/flagd/version.txt"; - dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpConnectorOptions httpConnectorOptions = HttpConnectorOptions.builder() + HttpConnectorOptions httpConnectorOptions = HttpConnectorOptions.builder() .url(testUrl) .connectTimeoutSeconds(10) .requestTimeoutSeconds(10) diff --git a/tools/flagd-http-connector/src/test/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/HttpConnectorOptionsTest.java b/tools/flagd-http-connector/src/test/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/HttpConnectorOptionsTest.java index a90bbae38..96bd81c4d 100644 --- a/tools/flagd-http-connector/src/test/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/HttpConnectorOptionsTest.java +++ b/tools/flagd-http-connector/src/test/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/HttpConnectorOptionsTest.java @@ -1,27 +1,23 @@ package dev.openfeature.contrib.tools.flagd.resolver.process.storage.connector.sync.http; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertThrows; -import static org.junit.Assert.assertTrue; - -import dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpConnectorOptions; -import dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.PayloadCache; -import dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.PayloadCacheOptions; +import org.junit.jupiter.api.Test; import java.net.MalformedURLException; import java.util.HashMap; import java.util.Map; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; -import org.junit.Test; -public class HttpConnectorOptionsTest { +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +public class HttpConnectorOptionsTest { @Test public void testDefaultValuesInitialization() { - dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpConnectorOptions options = dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpConnectorOptions.builder() + HttpConnectorOptions options = HttpConnectorOptions.builder() .url("https://example.com") .build(); @@ -45,7 +41,7 @@ public void testDefaultValuesInitialization() { public void testInvalidUrlFormat() { MalformedURLException exception = assertThrows( MalformedURLException.class, - () -> dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpConnectorOptions.builder() + () -> HttpConnectorOptions.builder() .url("invalid-url") .build() ); @@ -55,7 +51,7 @@ public void testInvalidUrlFormat() { @Test public void testCustomValuesInitialization() { - dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpConnectorOptions options = dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpConnectorOptions.builder() + HttpConnectorOptions options = HttpConnectorOptions.builder() .pollIntervalSeconds(120) .connectTimeoutSeconds(20) .requestTimeoutSeconds(30) @@ -78,7 +74,7 @@ public void testCustomHeadersMap() { customHeaders.put("Authorization", "Bearer token"); customHeaders.put("Content-Type", "application/json"); - dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpConnectorOptions options = dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpConnectorOptions.builder() + HttpConnectorOptions options = HttpConnectorOptions.builder() .url("http://example.com") .headers(customHeaders) .build(); @@ -90,7 +86,7 @@ public void testCustomHeadersMap() { @Test public void testCustomExecutorService() { ExecutorService customExecutor = Executors.newFixedThreadPool(5); - dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpConnectorOptions options = dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpConnectorOptions.builder() + HttpConnectorOptions options = HttpConnectorOptions.builder() .url("https://example.com") .httpClientExecutor(customExecutor) .build(); @@ -100,10 +96,10 @@ public void testCustomExecutorService() { @Test public void testSettingPayloadCacheWithValidOptions() { - dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.PayloadCacheOptions cacheOptions = dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.PayloadCacheOptions.builder() + PayloadCacheOptions cacheOptions = PayloadCacheOptions.builder() .updateIntervalSeconds(1800) .build(); - dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.PayloadCache payloadCache = new dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.PayloadCache() { + PayloadCache payloadCache = new PayloadCache() { private String payload; @Override @@ -117,7 +113,7 @@ public String get() { } }; - dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpConnectorOptions options = dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpConnectorOptions.builder() + HttpConnectorOptions options = HttpConnectorOptions.builder() .url("https://example.com") .payloadCacheOptions(cacheOptions) .payloadCache(payloadCache) @@ -130,7 +126,7 @@ public String get() { @Test public void testProxyConfigurationWithValidHostAndPort() { - dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpConnectorOptions options = dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpConnectorOptions.builder() + HttpConnectorOptions options = HttpConnectorOptions.builder() .url("https://example.com") .proxyHost("proxy.example.com") .proxyPort(8080) @@ -143,7 +139,7 @@ public void testProxyConfigurationWithValidHostAndPort() { @Test public void testLinkedBlockingQueueCapacityOutOfRange() { IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> { - dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpConnectorOptions.builder() + HttpConnectorOptions.builder() .url("https://example.com") .linkedBlockingQueueCapacity(0) .build(); @@ -151,7 +147,7 @@ public void testLinkedBlockingQueueCapacityOutOfRange() { assertEquals("linkedBlockingQueueCapacity must be between 1 and 1000", exception.getMessage()); exception = assertThrows(IllegalArgumentException.class, () -> { - dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpConnectorOptions.builder() + HttpConnectorOptions.builder() .url("https://example.com") .linkedBlockingQueueCapacity(1001) .build(); @@ -162,7 +158,7 @@ public void testLinkedBlockingQueueCapacityOutOfRange() { @Test public void testPollIntervalSecondsOutOfRange() { IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> { - dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpConnectorOptions.builder() + HttpConnectorOptions.builder() .url("https://example.com") .pollIntervalSeconds(700) .build(); @@ -175,8 +171,8 @@ public void testAdditionalCustomValuesInitialization() { Map headers = new HashMap<>(); headers.put("Authorization", "Bearer token"); ExecutorService executorService = Executors.newFixedThreadPool(2); - dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.PayloadCacheOptions cacheOptions = dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.PayloadCacheOptions.builder().build(); - dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.PayloadCache cache = new dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.PayloadCache() { + PayloadCacheOptions cacheOptions = PayloadCacheOptions.builder().build(); + PayloadCache cache = new PayloadCache() { @Override public void put(String payload) { // do nothing @@ -185,7 +181,7 @@ public void put(String payload) { public String get() { return null; } }; - dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpConnectorOptions options = dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpConnectorOptions.builder() + HttpConnectorOptions options = HttpConnectorOptions.builder() .url("https://example.com") .pollIntervalSeconds(120) .connectTimeoutSeconds(20) @@ -220,7 +216,7 @@ public void put(String payload) { @Test public void testRequestTimeoutSecondsOutOfRange() { IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> { - dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpConnectorOptions.builder() + HttpConnectorOptions.builder() .url("https://example.com") .requestTimeoutSeconds(61) .build(); @@ -233,8 +229,8 @@ public void testBuilderInitializesAllFields() { Map headers = new HashMap<>(); headers.put("Authorization", "Bearer token"); ExecutorService executorService = Executors.newFixedThreadPool(2); - dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.PayloadCacheOptions cacheOptions = dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.PayloadCacheOptions.builder().build(); - dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.PayloadCache cache = new dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.PayloadCache() { + PayloadCacheOptions cacheOptions = PayloadCacheOptions.builder().build(); + PayloadCache cache = new PayloadCache() { @Override public void put(String payload) { // do nothing @@ -243,7 +239,7 @@ public void put(String payload) { public String get() { return null; } }; - dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpConnectorOptions options = dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpConnectorOptions.builder() + HttpConnectorOptions options = HttpConnectorOptions.builder() .pollIntervalSeconds(120) .connectTimeoutSeconds(20) .requestTimeoutSeconds(30) @@ -277,7 +273,7 @@ public void put(String payload) { @Test public void testScheduledThreadPoolSizeOutOfRange() { IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> { - dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpConnectorOptions.builder() + HttpConnectorOptions.builder() .url("https://example.com") .scheduledThreadPoolSize(11) .build(); @@ -288,7 +284,7 @@ public void testScheduledThreadPoolSizeOutOfRange() { @Test public void testProxyPortOutOfRange() { IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> { - dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpConnectorOptions.builder() + HttpConnectorOptions.builder() .url("https://example.com") .proxyHost("proxy.example.com") .proxyPort(70000) // Invalid port, out of range @@ -300,7 +296,7 @@ public void testProxyPortOutOfRange() { @Test public void testConnectTimeoutSecondsOutOfRange() { IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> { - dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpConnectorOptions.builder() + HttpConnectorOptions.builder() .url("https://example.com") .connectTimeoutSeconds(0) .build(); @@ -308,7 +304,7 @@ public void testConnectTimeoutSecondsOutOfRange() { assertEquals("connectTimeoutSeconds must be between 1 and 60", exception.getMessage()); exception = assertThrows(IllegalArgumentException.class, () -> { - dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpConnectorOptions.builder() + HttpConnectorOptions.builder() .url("https://example.com") .connectTimeoutSeconds(61) .build(); @@ -319,7 +315,7 @@ public void testConnectTimeoutSecondsOutOfRange() { @Test public void testProxyPortWithoutProxyHost() { IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> { - dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpConnectorOptions.builder() + HttpConnectorOptions.builder() .url("https://example.com") .proxyPort(8080) .build(); @@ -329,7 +325,7 @@ public void testProxyPortWithoutProxyHost() { @Test public void testDefaultValuesWhenNullParametersProvided() { - dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpConnectorOptions options = dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpConnectorOptions.builder() + HttpConnectorOptions options = HttpConnectorOptions.builder() .url("https://example.com") .pollIntervalSeconds(null) .linkedBlockingQueueCapacity(null) @@ -364,7 +360,7 @@ public void testDefaultValuesWhenNullParametersProvided() { @Test public void testProxyHostWithoutProxyPort() { IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> { - dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpConnectorOptions.builder() + HttpConnectorOptions.builder() .url("https://example.com") .proxyHost("proxy.example.com") .build(); @@ -374,7 +370,7 @@ public void testProxyHostWithoutProxyPort() { @Test public void testSettingPayloadCacheWithoutOptions() { - dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.PayloadCache mockPayloadCache = new PayloadCache() { + PayloadCache mockPayloadCache = new PayloadCache() { @Override public void put(String payload) { // Mock implementation @@ -387,7 +383,7 @@ public String get() { }; IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> { - dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpConnectorOptions.builder() + HttpConnectorOptions.builder() .url("https://example.com") .payloadCache(mockPayloadCache) .build(); diff --git a/tools/flagd-http-connector/src/test/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/HttpConnectorTest.java b/tools/flagd-http-connector/src/test/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/HttpConnectorTest.java index 0edb440a8..5298d98f6 100644 --- a/tools/flagd-http-connector/src/test/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/HttpConnectorTest.java +++ b/tools/flagd-http-connector/src/test/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/HttpConnectorTest.java @@ -1,5 +1,6 @@ package dev.openfeature.contrib.tools.flagd.resolver.process.storage.connector.sync.http; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; @@ -10,12 +11,11 @@ import static org.mockito.Mockito.spy; import static org.mockito.Mockito.when; +import dev.openfeature.contrib.providers.flagd.Config; +import dev.openfeature.contrib.providers.flagd.FlagdOptions; +import dev.openfeature.contrib.providers.flagd.FlagdProvider; import dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.QueuePayload; import dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.QueuePayloadType; -import dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpConnector; -import dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpConnectorOptions; -import dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.PayloadCache; -import dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.PayloadCacheOptions; import java.io.IOException; import java.lang.reflect.Field; import java.net.http.HttpClient; @@ -28,6 +28,8 @@ import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; +import dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.QueueSource; +import dev.openfeature.sdk.EvaluationContext; import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; import org.junit.jupiter.api.Test; @@ -47,15 +49,15 @@ void testGetStreamQueueInitialAndScheduledPolls() { when(mockClient.send(any(HttpRequest.class), any(HttpResponse.BodyHandler.class))) .thenReturn(mockResponse); - dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpConnectorOptions httpConnectorOptions = dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpConnectorOptions.builder() + HttpConnectorOptions httpConnectorOptions = HttpConnectorOptions.builder() .url(testUrl) .httpClientExecutor(Executors.newSingleThreadExecutor()) .build(); - dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpConnector connector = dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpConnector.builder() + HttpConnector connector = HttpConnector.builder() .httpConnectorOptions(httpConnectorOptions) .build(); - Field clientField = dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpConnector.class.getDeclaredField("client"); + Field clientField = HttpConnector.class.getDeclaredField("client"); clientField.setAccessible(true); clientField.set(connector, mockClient); @@ -81,7 +83,7 @@ void testBuildPollTaskFetchesDataAndAddsToQueue() { when(mockClient.send(any(HttpRequest.class), any(HttpResponse.BodyHandler.class))) .thenReturn(mockResponse); - dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.PayloadCache payloadCache = new dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.PayloadCache() { + PayloadCache payloadCache = new PayloadCache() { private String payload; @Override public void put(String payload) { @@ -93,27 +95,27 @@ public String get() { return payload; } }; - dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpConnectorOptions httpConnectorOptions = dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpConnectorOptions.builder() + HttpConnectorOptions httpConnectorOptions = HttpConnectorOptions.builder() .url(testUrl) .proxyHost("proxy-host") .proxyPort(8080) .useHttpCache(true) .payloadCache(payloadCache) - .payloadCacheOptions(dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.PayloadCacheOptions.builder().build()) + .payloadCacheOptions(PayloadCacheOptions.builder().build()) .build(); - dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpConnector connector = dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpConnector.builder() + HttpConnector connector = HttpConnector.builder() .httpConnectorOptions(httpConnectorOptions) .build(); connector.init(); - Field clientField = dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpConnector.class.getDeclaredField("client"); + Field clientField = HttpConnector.class.getDeclaredField("client"); clientField.setAccessible(true); clientField.set(connector, mockClient); Runnable pollTask = connector.buildPollTask(); pollTask.run(); - Field queueField = dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpConnector.class.getDeclaredField("queue"); + Field queueField = HttpConnector.class.getDeclaredField("queue"); queueField.setAccessible(true); BlockingQueue queue = (BlockingQueue) queueField.get(connector); assertFalse(queue.isEmpty()); @@ -131,15 +133,15 @@ void testHttpRequestIncludesHeaders() { testHeaders.put("Authorization", "Bearer token"); testHeaders.put("Content-Type", "application/json"); - dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpConnectorOptions httpConnectorOptions = dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpConnectorOptions.builder() + HttpConnectorOptions httpConnectorOptions = HttpConnectorOptions.builder() .url(testUrl) .headers(testHeaders) .build(); - dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpConnector connector = dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpConnector.builder() + HttpConnector connector = HttpConnector.builder() .httpConnectorOptions(httpConnectorOptions) .build(); - Field headersField = dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpConnector.class.getDeclaredField("headers"); + Field headersField = HttpConnector.class.getDeclaredField("headers"); headersField.setAccessible(true); Map headers = (Map) headersField.get(connector); assertNotNull(headers); @@ -159,14 +161,14 @@ void testSuccessfulHttpResponseAddsDataToQueue() { when(mockClient.send(any(HttpRequest.class), any(HttpResponse.BodyHandler.class))) .thenReturn(mockResponse); - dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpConnectorOptions httpConnectorOptions = dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpConnectorOptions.builder() + HttpConnectorOptions httpConnectorOptions = HttpConnectorOptions.builder() .url(testUrl) .build(); - dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpConnector connector = dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpConnector.builder() + HttpConnector connector = HttpConnector.builder() .httpConnectorOptions(httpConnectorOptions) .build(); - Field clientField = dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpConnector.class.getDeclaredField("client"); + Field clientField = HttpConnector.class.getDeclaredField("client"); clientField.setAccessible(true); clientField.set(connector, mockClient); @@ -190,7 +192,7 @@ void testInitFailureUsingCache() { .thenThrow(new IOException("Simulated IO Exception")); final String cachedData = "cached data"; - dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.PayloadCache payloadCache = new PayloadCache() { + PayloadCache payloadCache = new PayloadCache() { @Override public void put(String payload) { // do nothing @@ -202,16 +204,16 @@ public String get() { } }; - dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpConnectorOptions httpConnectorOptions = dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpConnectorOptions.builder() + HttpConnectorOptions httpConnectorOptions = HttpConnectorOptions.builder() .url(testUrl) .payloadCache(payloadCache) .payloadCacheOptions(PayloadCacheOptions.builder().build()) .build(); - dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpConnector connector = dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpConnector.builder() + HttpConnector connector = HttpConnector.builder() .httpConnectorOptions(httpConnectorOptions) .build(); - Field clientField = dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpConnector.class.getDeclaredField("client"); + Field clientField = HttpConnector.class.getDeclaredField("client"); clientField.setAccessible(true); clientField.set(connector, mockClient); @@ -229,11 +231,11 @@ public String get() { void testQueueBecomesFull() { String testUrl = "http://example.com"; int queueCapacity = 1; - dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpConnectorOptions httpConnectorOptions = dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpConnectorOptions.builder() + HttpConnectorOptions httpConnectorOptions = HttpConnectorOptions.builder() .url(testUrl) .linkedBlockingQueueCapacity(queueCapacity) .build(); - dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpConnector connector = dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpConnector.builder() + HttpConnector connector = HttpConnector.builder() .httpConnectorOptions(httpConnectorOptions) .build(); @@ -253,15 +255,15 @@ void testShutdownProperlyTerminatesSchedulerAndHttpClientExecutor() throws Inter ScheduledExecutorService mockScheduler = mock(ScheduledExecutorService.class); String testUrl = "http://example.com"; - dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpConnectorOptions httpConnectorOptions = dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpConnectorOptions.builder() + HttpConnectorOptions httpConnectorOptions = HttpConnectorOptions.builder() .url(testUrl) .httpClientExecutor(mockHttpClientExecutor) .build(); - dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpConnector connector = dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpConnector.builder() + HttpConnector connector = HttpConnector.builder() .httpConnectorOptions(httpConnectorOptions) .build(); - Field schedulerField = dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpConnector.class.getDeclaredField("scheduler"); + Field schedulerField = HttpConnector.class.getDeclaredField("scheduler"); schedulerField.setAccessible(true); schedulerField.set(connector, mockScheduler); @@ -281,14 +283,14 @@ void testHttpResponseNonSuccessStatusCode() { when(mockClient.send(any(HttpRequest.class), any(HttpResponse.BodyHandler.class))) .thenReturn(mockResponse); - dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpConnectorOptions httpConnectorOptions = dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpConnectorOptions.builder() + HttpConnectorOptions httpConnectorOptions = HttpConnectorOptions.builder() .url(testUrl) .build(); - dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpConnector connector = dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpConnector.builder() + HttpConnector connector = HttpConnector.builder() .httpConnectorOptions(httpConnectorOptions) .build(); - Field clientField = dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpConnector.class.getDeclaredField("client"); + Field clientField = HttpConnector.class.getDeclaredField("client"); clientField.setAccessible(true); clientField.set(connector, mockClient); @@ -302,14 +304,14 @@ void testHttpResponseNonSuccessStatusCode() { void testHttpRequestFailsWithException() { String testUrl = "http://example.com"; HttpClient mockClient = mock(HttpClient.class); - dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpConnectorOptions httpConnectorOptions = dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpConnectorOptions.builder() + HttpConnectorOptions httpConnectorOptions = HttpConnectorOptions.builder() .url(testUrl) .build(); - dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpConnector connector = dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpConnector.builder() + HttpConnector connector = HttpConnector.builder() .httpConnectorOptions(httpConnectorOptions) .build(); - Field clientField = dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpConnector.class.getDeclaredField("client"); + Field clientField = HttpConnector.class.getDeclaredField("client"); clientField.setAccessible(true); clientField.set(connector, mockClient); @@ -326,14 +328,14 @@ void testHttpRequestFailsWithException() { void testHttpRequestFailsWithIoexception() { String testUrl = "http://example.com"; HttpClient mockClient = mock(HttpClient.class); - dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpConnectorOptions httpConnectorOptions = dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpConnectorOptions.builder() + HttpConnectorOptions httpConnectorOptions = HttpConnectorOptions.builder() .url(testUrl) .build(); - dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpConnector connector = dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpConnector.builder() + HttpConnector connector = HttpConnector.builder() .httpConnectorOptions(httpConnectorOptions) .build(); - Field clientField = dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpConnector.class.getDeclaredField("client"); + Field clientField = HttpConnector.class.getDeclaredField("client"); clientField.setAccessible(true); clientField.set(connector, mockClient); @@ -342,7 +344,7 @@ void testHttpRequestFailsWithIoexception() { connector.getStreamQueue(); - Field queueField = dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpConnector.class.getDeclaredField("queue"); + Field queueField = HttpConnector.class.getDeclaredField("queue"); queueField.setAccessible(true); BlockingQueue queue = (BlockingQueue) queueField.get(connector); assertTrue(queue.isEmpty(), "Queue should be empty due to IOException"); @@ -356,10 +358,10 @@ void testScheduledPollingContinuesAtFixedIntervals() { when(mockResponse.statusCode()).thenReturn(200); when(mockResponse.body()).thenReturn("test data"); - dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpConnectorOptions httpConnectorOptions = dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpConnectorOptions.builder() + HttpConnectorOptions httpConnectorOptions = HttpConnectorOptions.builder() .url(testUrl) .build(); - dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpConnector connector = spy(dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpConnector.builder() + HttpConnector connector = spy(HttpConnector.builder() .httpConnectorOptions(httpConnectorOptions) .build()); @@ -389,10 +391,10 @@ void testQueuePayloadTypeSetToDataOnSuccess() { when(mockClient.send(any(HttpRequest.class), any(HttpResponse.BodyHandler.class))) .thenReturn(mockResponse); - dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpConnectorOptions httpConnectorOptions = HttpConnectorOptions.builder() + HttpConnectorOptions httpConnectorOptions = HttpConnectorOptions.builder() .url(testUrl) .build(); - dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpConnector connector = dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpConnector.builder() + HttpConnector connector = HttpConnector.builder() .httpConnectorOptions(httpConnectorOptions) .build(); @@ -408,6 +410,26 @@ void testQueuePayloadTypeSetToDataOnSuccess() { assertEquals("response body", payload.getFlagData()); } + @Test + public void providerTest() { + HttpConnectorOptions httpConnectorOptions = HttpConnectorOptions.builder() + .url("http://example.com") + .build(); + HttpConnector connector = HttpConnector.builder() + .httpConnectorOptions(httpConnectorOptions) + .build(); + + FlagdOptions options = + FlagdOptions.builder() + .resolverType(Config.Resolver.IN_PROCESS) + .customConnector(connector) + .build(); + + FlagdProvider flagdProvider = new FlagdProvider(options); + + assertDoesNotThrow(() -> flagdProvider.getMetadata()); + } + @SneakyThrows protected static void delay(long ms) { Thread.sleep(ms); diff --git a/tools/flagd-http-connector/src/test/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/PayloadCacheWrapperTest.java b/tools/flagd-http-connector/src/test/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/PayloadCacheWrapperTest.java index 28fba6881..3ff3ff679 100644 --- a/tools/flagd-http-connector/src/test/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/PayloadCacheWrapperTest.java +++ b/tools/flagd-http-connector/src/test/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/PayloadCacheWrapperTest.java @@ -1,6 +1,6 @@ package dev.openfeature.contrib.tools.flagd.resolver.process.storage.connector.sync.http; -import static dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpConnectorTest.delay; +import static dev.openfeature.contrib.tools.flagd.resolver.process.storage.connector.sync.http.HttpConnectorTest.delay; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; @@ -15,9 +15,6 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -import dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.PayloadCache; -import dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.PayloadCacheOptions; -import dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.PayloadCacheWrapper; import java.lang.reflect.Field; import lombok.SneakyThrows; import org.junit.jupiter.api.Test; @@ -27,12 +24,12 @@ public class PayloadCacheWrapperTest { @Test public void testConstructorInitializesWithValidParameters() { - dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.PayloadCache mockCache = mock(dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.PayloadCache.class); - dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.PayloadCacheOptions options = dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.PayloadCacheOptions.builder() + PayloadCache mockCache = mock(PayloadCache.class); + PayloadCacheOptions options = PayloadCacheOptions.builder() .updateIntervalSeconds(600) .build(); - dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.PayloadCacheWrapper wrapper = dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.PayloadCacheWrapper.builder() + PayloadCacheWrapper wrapper = PayloadCacheWrapper.builder() .payloadCache(mockCache) .payloadCacheOptions(options) .build(); @@ -49,12 +46,12 @@ public void testConstructorInitializesWithValidParameters() { @Test public void testConstructorThrowsExceptionForInvalidInterval() { - dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.PayloadCache mockCache = mock(dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.PayloadCache.class); - dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.PayloadCacheOptions options = dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.PayloadCacheOptions.builder() + PayloadCache mockCache = mock(PayloadCache.class); + PayloadCacheOptions options = PayloadCacheOptions.builder() .updateIntervalSeconds(0) .build(); - dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.PayloadCacheWrapper.PayloadCacheWrapperBuilder payloadCacheWrapperBuilder = dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.PayloadCacheWrapper.builder() + PayloadCacheWrapper.PayloadCacheWrapperBuilder payloadCacheWrapperBuilder = PayloadCacheWrapper.builder() .payloadCache(mockCache) .payloadCacheOptions(options); IllegalArgumentException exception = assertThrows( @@ -67,11 +64,11 @@ public void testConstructorThrowsExceptionForInvalidInterval() { @Test public void testUpdateSkipsWhenIntervalNotPassed() { - dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.PayloadCache mockCache = mock(dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.PayloadCache.class); - dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.PayloadCacheOptions options = dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.PayloadCacheOptions.builder() + PayloadCache mockCache = mock(PayloadCache.class); + PayloadCacheOptions options = PayloadCacheOptions.builder() .updateIntervalSeconds(600) .build(); - dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.PayloadCacheWrapper wrapper = dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.PayloadCacheWrapper.builder() + PayloadCacheWrapper wrapper = PayloadCacheWrapper.builder() .payloadCache(mockCache) .payloadCacheOptions(options) .build(); @@ -88,11 +85,11 @@ public void testUpdateSkipsWhenIntervalNotPassed() { @Test public void testUpdatePayloadIfNeededHandlesPutException() { - dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.PayloadCache mockCache = mock(dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.PayloadCache.class); - dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.PayloadCacheOptions options = dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.PayloadCacheOptions.builder() + PayloadCache mockCache = mock(PayloadCache.class); + PayloadCacheOptions options = PayloadCacheOptions.builder() .updateIntervalSeconds(600) .build(); - dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.PayloadCacheWrapper wrapper = dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.PayloadCacheWrapper.builder() + PayloadCacheWrapper wrapper = PayloadCacheWrapper.builder() .payloadCache(mockCache) .payloadCacheOptions(options) .build(); @@ -107,11 +104,11 @@ public void testUpdatePayloadIfNeededHandlesPutException() { @Test public void testUpdatePayloadIfNeededUpdatesCacheAfterInterval() { - dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.PayloadCache mockCache = mock(dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.PayloadCache.class); - dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.PayloadCacheOptions options = dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.PayloadCacheOptions.builder() + PayloadCache mockCache = mock(PayloadCache.class); + PayloadCacheOptions options = PayloadCacheOptions.builder() .updateIntervalSeconds(1) // 1 second interval for quick test .build(); - dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.PayloadCacheWrapper wrapper = dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.PayloadCacheWrapper.builder() + PayloadCacheWrapper wrapper = PayloadCacheWrapper.builder() .payloadCache(mockCache) .payloadCacheOptions(options) .build(); @@ -129,11 +126,11 @@ public void testUpdatePayloadIfNeededUpdatesCacheAfterInterval() { @Test public void testGetReturnsNullWhenCacheGetThrowsException() { - dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.PayloadCache mockCache = mock(dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.PayloadCache.class); - dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.PayloadCacheOptions options = dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.PayloadCacheOptions.builder() + PayloadCache mockCache = mock(PayloadCache.class); + PayloadCacheOptions options = PayloadCacheOptions.builder() .updateIntervalSeconds(600) .build(); - dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.PayloadCacheWrapper wrapper = dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.PayloadCacheWrapper.builder() + PayloadCacheWrapper wrapper = PayloadCacheWrapper.builder() .payloadCache(mockCache) .payloadCacheOptions(options) .build(); @@ -149,11 +146,11 @@ public void testGetReturnsNullWhenCacheGetThrowsException() { @Test public void test_get_returns_cached_payload() { - dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.PayloadCache mockCache = mock(dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.PayloadCache.class); - dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.PayloadCacheOptions options = dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.PayloadCacheOptions.builder() + PayloadCache mockCache = mock(PayloadCache.class); + PayloadCacheOptions options = PayloadCacheOptions.builder() .updateIntervalSeconds(600) .build(); - dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.PayloadCacheWrapper wrapper = dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.PayloadCacheWrapper.builder() + PayloadCacheWrapper wrapper = PayloadCacheWrapper.builder() .payloadCache(mockCache) .payloadCacheOptions(options) .build(); @@ -169,11 +166,11 @@ public void test_get_returns_cached_payload() { @Test public void test_first_call_updates_cache() { - dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.PayloadCache mockCache = mock(dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.PayloadCache.class); - dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.PayloadCacheOptions options = dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.PayloadCacheOptions.builder() + PayloadCache mockCache = mock(PayloadCache.class); + PayloadCacheOptions options = PayloadCacheOptions.builder() .updateIntervalSeconds(600) .build(); - dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.PayloadCacheWrapper wrapper = dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.PayloadCacheWrapper.builder() + PayloadCacheWrapper wrapper = PayloadCacheWrapper.builder() .payloadCache(mockCache) .payloadCacheOptions(options) .build(); @@ -187,11 +184,11 @@ public void test_first_call_updates_cache() { @Test public void test_update_payload_once_within_interval() { - dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.PayloadCache mockCache = mock(dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.PayloadCache.class); - dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.PayloadCacheOptions options = dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.PayloadCacheOptions.builder() + PayloadCache mockCache = mock(PayloadCache.class); + PayloadCacheOptions options = PayloadCacheOptions.builder() .updateIntervalSeconds(1) // 1 second interval .build(); - dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.PayloadCacheWrapper wrapper = dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.PayloadCacheWrapper.builder() + PayloadCacheWrapper wrapper = PayloadCacheWrapper.builder() .payloadCache(mockCache) .payloadCacheOptions(options) .build(); @@ -207,11 +204,11 @@ public void test_update_payload_once_within_interval() { @SneakyThrows @Test public void test_last_update_time_ms_updated_after_successful_cache_update() { - dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.PayloadCache mockCache = mock(dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.PayloadCache.class); - dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.PayloadCacheOptions options = dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.PayloadCacheOptions.builder() + PayloadCache mockCache = mock(PayloadCache.class); + PayloadCacheOptions options = PayloadCacheOptions.builder() .updateIntervalSeconds(600) .build(); - dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.PayloadCacheWrapper wrapper = dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.PayloadCacheWrapper.builder() + PayloadCacheWrapper wrapper = PayloadCacheWrapper.builder() .payloadCache(mockCache) .payloadCacheOptions(options) .build(); @@ -221,7 +218,7 @@ public void test_last_update_time_ms_updated_after_successful_cache_update() { verify(mockCache).put(testPayload); - Field lastUpdateTimeMsField = dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.PayloadCacheWrapper.class.getDeclaredField("lastUpdateTimeMs"); + Field lastUpdateTimeMsField = PayloadCacheWrapper.class.getDeclaredField("lastUpdateTimeMs"); lastUpdateTimeMsField.setAccessible(true); long lastUpdateTimeMs = (Long) lastUpdateTimeMsField.get(wrapper); @@ -231,11 +228,11 @@ public void test_last_update_time_ms_updated_after_successful_cache_update() { @Test public void test_update_payload_if_needed_respects_update_interval() { - dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.PayloadCache mockCache = mock(PayloadCache.class); - dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.PayloadCacheOptions options = PayloadCacheOptions.builder() + PayloadCache mockCache = mock(PayloadCache.class); + PayloadCacheOptions options = PayloadCacheOptions.builder() .updateIntervalSeconds(600) .build(); - dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.PayloadCacheWrapper wrapper = spy(PayloadCacheWrapper.builder() + PayloadCacheWrapper wrapper = spy(PayloadCacheWrapper.builder() .payloadCache(mockCache) .payloadCacheOptions(options) .build()); diff --git a/tools/flagd-http-connector/version.txt b/tools/flagd-http-connector/version.txt new file mode 100644 index 000000000..8acdd82b7 --- /dev/null +++ b/tools/flagd-http-connector/version.txt @@ -0,0 +1 @@ +0.0.1 From 8f03143ba466aae5fcd290bf11cdac2fb8af18b9 Mon Sep 17 00:00:00 2001 From: liran2000 Date: Thu, 24 Apr 2025 17:45:51 +0300 Subject: [PATCH 08/40] move to tool - draft - cont. Signed-off-by: liran2000 --- .../connector/sync/http/HttpCacheFetcher.java | 49 --- .../connector/sync/http/HttpConnector.java | 184 -------- .../sync/http/HttpConnectorOptions.java | 125 ------ .../connector/sync/http/PayloadCache.java | 6 - .../sync/http/PayloadCacheOptions.java | 24 - .../sync/http/PayloadCacheWrapper.java | 59 --- .../main/resources/simplelogger.properties | 2 +- .../sync/http/HttpCacheFetcherTest.java | 308 ------------- .../http/HttpConnectorIntegrationTest.java | 57 --- .../sync/http/HttpConnectorOptionsTest.java | 406 ----------------- .../sync/http/HttpConnectorTest.java | 412 ------------------ .../sync/http/PayloadCacheWrapperTest.java | 267 ------------ 12 files changed, 1 insertion(+), 1898 deletions(-) delete mode 100644 providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/connector/sync/http/HttpCacheFetcher.java delete mode 100644 providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/connector/sync/http/HttpConnector.java delete mode 100644 providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/connector/sync/http/HttpConnectorOptions.java delete mode 100644 providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/connector/sync/http/PayloadCache.java delete mode 100644 providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/connector/sync/http/PayloadCacheOptions.java delete mode 100644 providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/connector/sync/http/PayloadCacheWrapper.java delete mode 100644 providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/connector/sync/http/HttpCacheFetcherTest.java delete mode 100644 providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/connector/sync/http/HttpConnectorIntegrationTest.java delete mode 100644 providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/connector/sync/http/HttpConnectorOptionsTest.java delete mode 100644 providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/connector/sync/http/HttpConnectorTest.java delete mode 100644 providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/connector/sync/http/PayloadCacheWrapperTest.java diff --git a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/connector/sync/http/HttpCacheFetcher.java b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/connector/sync/http/HttpCacheFetcher.java deleted file mode 100644 index e2a59a9fc..000000000 --- a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/connector/sync/http/HttpCacheFetcher.java +++ /dev/null @@ -1,49 +0,0 @@ -package dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http; - -import java.net.http.HttpClient; -import java.net.http.HttpRequest; -import java.net.http.HttpResponse; -import lombok.SneakyThrows; -import lombok.extern.slf4j.Slf4j; - -/** - * Fetches content from a given HTTP endpoint using caching headers to optimize network usage. - * If cached ETag or Last-Modified values are available, they are included in the request headers - * to potentially receive a 304 Not Modified response, reducing data transfer. - * Updates the cached ETag and Last-Modified values upon receiving a 200 OK response. - * It does not store the cached response, assuming not needed after first successful fetching. - * Non thread-safe. - * - * @param httpClient the HTTP client used to send the request - * @param httpRequestBuilder the builder for constructing the HTTP request - * @return the HTTP response received from the server - */ -@Slf4j -public class HttpCacheFetcher { - private String cachedETag = null; - private String cachedLastModified = null; - - @SneakyThrows - public HttpResponse fetchContent(HttpClient httpClient, HttpRequest.Builder httpRequestBuilder) { - if (cachedETag != null) { - httpRequestBuilder.header("If-None-Match", cachedETag); - } - if (cachedLastModified != null) { - httpRequestBuilder.header("If-Modified-Since", cachedLastModified); - } - - HttpRequest request = httpRequestBuilder.build(); - HttpResponse httpResponse = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); - - if (httpResponse.statusCode() == 200) { - if (httpResponse.headers() != null) { - cachedETag = httpResponse.headers().firstValue("ETag").orElse(null); - cachedLastModified = httpResponse.headers().firstValue("Last-Modified").orElse(null); - } - log.debug("fetched new content"); - } else if (httpResponse.statusCode() == 304) { - log.debug("got 304 Not Modified"); - } - return httpResponse; - } -} diff --git a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/connector/sync/http/HttpConnector.java b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/connector/sync/http/HttpConnector.java deleted file mode 100644 index b72b5c035..000000000 --- a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/connector/sync/http/HttpConnector.java +++ /dev/null @@ -1,184 +0,0 @@ -package dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http; - -import dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.QueuePayload; -import dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.QueuePayloadType; -import dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.QueueSource; -import dev.openfeature.contrib.providers.flagd.util.ConcurrentUtils; -import lombok.Builder; -import lombok.NonNull; -import lombok.extern.slf4j.Slf4j; - -import java.io.IOException; -import java.net.InetSocketAddress; -import java.net.ProxySelector; -import java.net.URI; -import java.net.http.HttpClient; -import java.net.http.HttpRequest; -import java.net.http.HttpResponse; -import java.time.Duration; -import java.util.Map; -import java.util.concurrent.BlockingQueue; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.LinkedBlockingQueue; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.TimeUnit; - -import static java.net.http.HttpClient.Builder.NO_PROXY; - -/** - * HttpConnector is responsible for polling data from a specified URL at regular intervals. - * Notice rate limits for polling http sources like Github. - * It implements the QueueSource interface to enqueue and dequeue change messages. - * The class supports configurable parameters such as poll interval, request timeout, and proxy settings. - * It uses a ScheduledExecutorService to schedule polling tasks and an ExecutorService for HTTP client execution. - * The class also provides methods to initialize, retrieve the stream queue, and shutdown the connector gracefully. - * It supports optional fail-safe initialization via cache. - * - * See readme - Http Connector section. - */ -@Slf4j -public class HttpConnector implements QueueSource { - - private Integer pollIntervalSeconds; - private Integer requestTimeoutSeconds; - private BlockingQueue queue; - private HttpClient client; - private ExecutorService httpClientExecutor; - private ScheduledExecutorService scheduler; - private Map headers; - private PayloadCacheWrapper payloadCacheWrapper; - private PayloadCache payloadCache; - private HttpCacheFetcher httpCacheFetcher; - - @NonNull - private String url; - - @Builder - public HttpConnector(HttpConnectorOptions httpConnectorOptions) { - this.pollIntervalSeconds = httpConnectorOptions.getPollIntervalSeconds(); - this.requestTimeoutSeconds = httpConnectorOptions.getRequestTimeoutSeconds(); - ProxySelector proxySelector = NO_PROXY; - if (httpConnectorOptions.getProxyHost() != null && httpConnectorOptions.getProxyPort() != null) { - proxySelector = ProxySelector.of(new InetSocketAddress(httpConnectorOptions.getProxyHost(), - httpConnectorOptions.getProxyPort())); - } - this.url = httpConnectorOptions.getUrl(); - this.headers = httpConnectorOptions.getHeaders(); - this.httpClientExecutor = httpConnectorOptions.getHttpClientExecutor(); - scheduler = Executors.newScheduledThreadPool(httpConnectorOptions.getScheduledThreadPoolSize()); - this.client = HttpClient.newBuilder() - .connectTimeout(Duration.ofSeconds(httpConnectorOptions.getConnectTimeoutSeconds())) - .proxy(proxySelector) - .executor(this.httpClientExecutor) - .build(); - this.queue = new LinkedBlockingQueue<>(httpConnectorOptions.getLinkedBlockingQueueCapacity()); - this.payloadCache = httpConnectorOptions.getPayloadCache(); - if (payloadCache != null) { - this.payloadCacheWrapper = PayloadCacheWrapper.builder() - .payloadCache(payloadCache) - .payloadCacheOptions(httpConnectorOptions.getPayloadCacheOptions()) - .build(); - } - if (Boolean.TRUE.equals(httpConnectorOptions.getUseHttpCache())) { - httpCacheFetcher = new HttpCacheFetcher(); - } - } - - @Override - public void init() throws Exception { - log.info("init Http Connector"); - } - - @Override - public BlockingQueue getStreamQueue() { - boolean success = fetchAndUpdate(); - if (!success) { - log.info("failed initial fetch"); - if (payloadCache != null) { - updateFromCache(); - } - } - Runnable pollTask = buildPollTask(); - scheduler.scheduleWithFixedDelay(pollTask, pollIntervalSeconds, pollIntervalSeconds, TimeUnit.SECONDS); - return queue; - } - - private void updateFromCache() { - log.info("taking initial payload from cache to avoid starting with default values"); - String flagData = payloadCache.get(); - if (flagData == null) { - log.debug("got null from cache"); - return; - } - if (!this.queue.offer(new QueuePayload(QueuePayloadType.DATA, flagData))) { - log.warn("init: Unable to offer file content to queue: queue is full"); - } - } - - protected Runnable buildPollTask() { - return this::fetchAndUpdate; - } - - private boolean fetchAndUpdate() { - HttpRequest.Builder requestBuilder = HttpRequest.newBuilder() - .uri(URI.create(url)) - .timeout(Duration.ofSeconds(requestTimeoutSeconds)) - .GET(); - headers.forEach(requestBuilder::header); - - HttpResponse response; - try { - log.debug("fetching response"); - response = execute(requestBuilder); - } catch (IOException e) { - log.info("could not fetch", e); - return false; - } catch (Exception e) { - log.debug("exception", e); - return false; - } - log.debug("fetched response"); - String payload = response.body(); - if (!isSuccessful(response)) { - log.info("received non-successful status code: {} {}", response.statusCode(), payload); - return false; - } else if (response.statusCode() == 304) { - log.debug("got 304 Not Modified, skipping update"); - return false; - } - if (payload == null) { - log.debug("payload is null"); - return false; - } - log.debug("adding payload to queue"); - if (!this.queue.offer(new QueuePayload(QueuePayloadType.DATA, payload))) { - log.warn("Unable to offer file content to queue: queue is full"); - return false; - } - if (payloadCacheWrapper != null) { - log.debug("scheduling cache update if needed"); - scheduler.execute(() -> - payloadCacheWrapper.updatePayloadIfNeeded(payload) - ); - } - return payload != null; - } - - private static boolean isSuccessful(HttpResponse response) { - return response.statusCode() == 200 || response.statusCode() == 304; - } - - protected HttpResponse execute(HttpRequest.Builder requestBuilder) throws IOException, InterruptedException { - if (httpCacheFetcher != null) { - return httpCacheFetcher.fetchContent(client, requestBuilder); - } - return client.send(requestBuilder.build(), HttpResponse.BodyHandlers.ofString()); - } - - @Override - public void shutdown() throws InterruptedException { - ConcurrentUtils.shutdownAndAwaitTermination(scheduler, 10); - ConcurrentUtils.shutdownAndAwaitTermination(httpClientExecutor, 10); - } -} diff --git a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/connector/sync/http/HttpConnectorOptions.java b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/connector/sync/http/HttpConnectorOptions.java deleted file mode 100644 index 0f8ff0186..000000000 --- a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/connector/sync/http/HttpConnectorOptions.java +++ /dev/null @@ -1,125 +0,0 @@ -package dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http; - -import java.net.URL; -import java.util.HashMap; -import java.util.Map; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import lombok.Builder; -import lombok.Getter; -import lombok.NonNull; -import lombok.SneakyThrows; - -@Getter -public class HttpConnectorOptions { - - @Builder.Default - private Integer pollIntervalSeconds = 60; - @Builder.Default - private Integer connectTimeoutSeconds = 10; - @Builder.Default - private Integer requestTimeoutSeconds = 10; - @Builder.Default - private Integer linkedBlockingQueueCapacity = 100; - @Builder.Default - private Integer scheduledThreadPoolSize = 2; - @Builder.Default - private Map headers = new HashMap<>(); - @Builder.Default - private ExecutorService httpClientExecutor = Executors.newFixedThreadPool(1); - @Builder.Default - private String proxyHost; - @Builder.Default - private Integer proxyPort; - @Builder.Default - private PayloadCacheOptions payloadCacheOptions; - @Builder.Default - private PayloadCache payloadCache; - @Builder.Default - private Boolean useHttpCache; - @NonNull - private String url; - - @Builder - public HttpConnectorOptions(Integer pollIntervalSeconds, Integer linkedBlockingQueueCapacity, - Integer scheduledThreadPoolSize, Integer requestTimeoutSeconds, Integer connectTimeoutSeconds, String url, - Map headers, ExecutorService httpClientExecutor, String proxyHost, Integer proxyPort, - PayloadCacheOptions payloadCacheOptions, PayloadCache payloadCache, Boolean useHttpCache) { - validate(url, pollIntervalSeconds, linkedBlockingQueueCapacity, scheduledThreadPoolSize, requestTimeoutSeconds, - connectTimeoutSeconds, proxyHost, proxyPort, payloadCacheOptions, payloadCache); - if (pollIntervalSeconds != null) { - this.pollIntervalSeconds = pollIntervalSeconds; - } - if (linkedBlockingQueueCapacity != null) { - this.linkedBlockingQueueCapacity = linkedBlockingQueueCapacity; - } - if (scheduledThreadPoolSize != null) { - this.scheduledThreadPoolSize = scheduledThreadPoolSize; - } - if (requestTimeoutSeconds != null) { - this.requestTimeoutSeconds = requestTimeoutSeconds; - } - if (connectTimeoutSeconds != null) { - this.connectTimeoutSeconds = connectTimeoutSeconds; - } - this.url = url; - if (headers != null) { - this.headers = headers; - } - if (httpClientExecutor != null) { - this.httpClientExecutor = httpClientExecutor; - } - if (proxyHost != null) { - this.proxyHost = proxyHost; - } - if (proxyPort != null) { - this.proxyPort = proxyPort; - } - if (payloadCache != null) { - this.payloadCache = payloadCache; - } - if (payloadCacheOptions != null) { - this.payloadCacheOptions = payloadCacheOptions; - } - if (useHttpCache != null) { - this.useHttpCache = useHttpCache; - } - } - - @SneakyThrows - private void validate(String url, Integer pollIntervalSeconds, Integer linkedBlockingQueueCapacity, - Integer scheduledThreadPoolSize, Integer requestTimeoutSeconds, Integer connectTimeoutSeconds, - String proxyHost, Integer proxyPort, PayloadCacheOptions payloadCacheOptions, - PayloadCache payloadCache) { - new URL(url).toURI(); - if (linkedBlockingQueueCapacity != null && (linkedBlockingQueueCapacity < 1 || linkedBlockingQueueCapacity > 1000)) { - throw new IllegalArgumentException("linkedBlockingQueueCapacity must be between 1 and 1000"); - } - if (scheduledThreadPoolSize != null && (scheduledThreadPoolSize < 1 || scheduledThreadPoolSize > 10)) { - throw new IllegalArgumentException("scheduledThreadPoolSize must be between 1 and 10"); - } - if (requestTimeoutSeconds != null && (requestTimeoutSeconds < 1 || requestTimeoutSeconds > 60)) { - throw new IllegalArgumentException("requestTimeoutSeconds must be between 1 and 60"); - } - if (connectTimeoutSeconds != null && (connectTimeoutSeconds < 1 || connectTimeoutSeconds > 60)) { - throw new IllegalArgumentException("connectTimeoutSeconds must be between 1 and 60"); - } - if (pollIntervalSeconds != null && (pollIntervalSeconds < 1 || pollIntervalSeconds > 600)) { - throw new IllegalArgumentException("pollIntervalSeconds must be between 1 and 600"); - } - if (proxyPort != null && (proxyPort < 1 || proxyPort > 65535)) { - throw new IllegalArgumentException("proxyPort must be between 1 and 65535"); - } - if (proxyHost != null && proxyPort == null ) { - throw new IllegalArgumentException("proxyPort must be set if proxyHost is set"); - } else if (proxyHost == null && proxyPort != null) { - throw new IllegalArgumentException("proxyHost must be set if proxyPort is set"); - } - if (payloadCacheOptions != null && payloadCache == null) { - throw new IllegalArgumentException("payloadCache must be set if payloadCacheOptions is set"); - } - if (payloadCache != null && payloadCacheOptions == null) { - throw new IllegalArgumentException("payloadCacheOptions must be set if payloadCache is set"); - } - } -} diff --git a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/connector/sync/http/PayloadCache.java b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/connector/sync/http/PayloadCache.java deleted file mode 100644 index 31416af1e..000000000 --- a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/connector/sync/http/PayloadCache.java +++ /dev/null @@ -1,6 +0,0 @@ -package dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http; - -public interface PayloadCache { - public void put(String payload); - public String get(); -} diff --git a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/connector/sync/http/PayloadCacheOptions.java b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/connector/sync/http/PayloadCacheOptions.java deleted file mode 100644 index d29ed115d..000000000 --- a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/connector/sync/http/PayloadCacheOptions.java +++ /dev/null @@ -1,24 +0,0 @@ -package dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http; - -import lombok.Builder; -import lombok.Getter; - -/** - * Represents configuration options for caching payloads. - *

- * This class provides options to configure the caching behavior, - * specifically the interval at which the cache should be updated. - *

- *

- * The default update interval is set to 30 minutes. - * Change it typically to a value according to cache ttl and tradeoff with not updating it too much for - * corner cases. - *

- */ -@Builder -@Getter -public class PayloadCacheOptions { - - @Builder.Default - private int updateIntervalSeconds = 60 * 30; // 30 minutes -} diff --git a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/connector/sync/http/PayloadCacheWrapper.java b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/connector/sync/http/PayloadCacheWrapper.java deleted file mode 100644 index 449cf1969..000000000 --- a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/connector/sync/http/PayloadCacheWrapper.java +++ /dev/null @@ -1,59 +0,0 @@ -package dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http; - -import lombok.Builder; -import lombok.extern.slf4j.Slf4j; - -/** - * A wrapper class for managing a payload cache with a specified update interval. - * This class ensures that the cache is only updated if the specified time interval - * has passed since the last update. It logs debug messages when updates are skipped - * and error messages if the update process fails. - * Not thread-safe. - * - *

Usage involves creating an instance with {@link PayloadCacheOptions} to set - * the update interval, and then using {@link #updatePayloadIfNeeded(String)} to - * conditionally update the cache and {@link #get()} to retrieve the cached payload.

- */ -@Slf4j -public class PayloadCacheWrapper { - private long lastUpdateTimeMs; - private long updateIntervalMs; - private PayloadCache payloadCache; - - @Builder - public PayloadCacheWrapper(PayloadCache payloadCache, PayloadCacheOptions payloadCacheOptions) { - if (payloadCacheOptions.getUpdateIntervalSeconds() < 1) { - throw new IllegalArgumentException("pollIntervalSeconds must be larger than 0"); - } - this.updateIntervalMs = payloadCacheOptions.getUpdateIntervalSeconds() * 1000L; - this.payloadCache = payloadCache; - } - - public void updatePayloadIfNeeded(String payload) { - if ((getCurrentTimeMillis() - lastUpdateTimeMs) < updateIntervalMs) { - log.debug("not updating payload, updateIntervalMs not reached"); - return; - } - - try { - log.debug("updating payload"); - payloadCache.put(payload); - lastUpdateTimeMs = getCurrentTimeMillis(); - } catch (Exception e) { - log.error("failed updating cache", e); - } - } - - protected long getCurrentTimeMillis() { - return System.currentTimeMillis(); - } - - public String get() { - try { - return payloadCache.get(); - } catch (Exception e) { - log.error("failed getting from cache", e); - return null; - } - } -} diff --git a/providers/flagd/src/main/resources/simplelogger.properties b/providers/flagd/src/main/resources/simplelogger.properties index d9d489e82..80c478930 100644 --- a/providers/flagd/src/main/resources/simplelogger.properties +++ b/providers/flagd/src/main/resources/simplelogger.properties @@ -1,3 +1,3 @@ -org.org.slf4j.simpleLogger.defaultLogLevel=debug +org.slf4j.simpleLogger.defaultLogLevel=debug io.grpc.level=trace diff --git a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/connector/sync/http/HttpCacheFetcherTest.java b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/connector/sync/http/HttpCacheFetcherTest.java deleted file mode 100644 index a6d0b859e..000000000 --- a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/connector/sync/http/HttpCacheFetcherTest.java +++ /dev/null @@ -1,308 +0,0 @@ -package dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http; - -import static org.junit.Assert.assertNull; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.doReturn; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import java.io.IOException; -import java.lang.reflect.Field; -import java.net.http.HttpClient; -import java.net.http.HttpHeaders; -import java.net.http.HttpRequest; -import java.net.http.HttpResponse; -import java.util.Arrays; -import java.util.HashMap; -import java.util.Map; -import java.util.Optional; -import lombok.SneakyThrows; -import org.junit.jupiter.api.Test; -public class HttpCacheFetcherTest { - - @Test - public void testFirstRequestSendsNoCacheHeaders() throws Exception { - HttpClient httpClientMock = mock(HttpClient.class); - HttpRequest.Builder requestBuilderMock = mock(HttpRequest.Builder.class); - HttpRequest requestMock = mock(HttpRequest.class); - HttpResponse responseMock = spy(HttpResponse.class); - doReturn(HttpHeaders.of(new HashMap<>(), (a, b) -> true)).when(responseMock).headers(); - - when(requestBuilderMock.build()).thenReturn(requestMock); - when(httpClientMock.send(eq(requestMock), any(HttpResponse.BodyHandler.class))).thenReturn(responseMock); - when(responseMock.statusCode()).thenReturn(200); - - HttpCacheFetcher fetcher = new HttpCacheFetcher(); - fetcher.fetchContent(httpClientMock, requestBuilderMock); - - verify(requestBuilderMock, never()).header(eq("If-None-Match"), anyString()); - verify(requestBuilderMock, never()).header(eq("If-Modified-Since"), anyString()); - } - - @Test - public void testResponseWith200ButNoCacheHeaders() throws Exception { - HttpClient httpClientMock = mock(HttpClient.class); - HttpRequest.Builder requestBuilderMock = mock(HttpRequest.Builder.class); - HttpRequest requestMock = mock(HttpRequest.class); - HttpResponse responseMock = mock(HttpResponse.class); - HttpHeaders headersMock = mock(HttpHeaders.class); - - when(requestBuilderMock.build()).thenReturn(requestMock); - when(httpClientMock.send(eq(requestMock), any(HttpResponse.BodyHandler.class))).thenReturn(responseMock); - when(responseMock.statusCode()).thenReturn(200); - when(responseMock.headers()).thenReturn(headersMock); - when(headersMock.firstValue("ETag")).thenReturn(Optional.empty()); - when(headersMock.firstValue("Last-Modified")).thenReturn(Optional.empty()); - - HttpCacheFetcher fetcher = new HttpCacheFetcher(); - HttpResponse response = fetcher.fetchContent(httpClientMock, requestBuilderMock); - - assertEquals(200, response.statusCode()); - - HttpRequest.Builder secondRequestBuilderMock = mock(HttpRequest.Builder.class); - when(secondRequestBuilderMock.build()).thenReturn(requestMock); - - fetcher.fetchContent(httpClientMock, secondRequestBuilderMock); - - verify(secondRequestBuilderMock, never()).header(eq("If-None-Match"), anyString()); - verify(secondRequestBuilderMock, never()).header(eq("If-Modified-Since"), anyString()); - } - - @Test - public void testFetchContentReturnsHttpResponse() throws Exception { - HttpClient httpClientMock = mock(HttpClient.class); - HttpRequest.Builder requestBuilderMock = mock(HttpRequest.Builder.class); - HttpRequest requestMock = mock(HttpRequest.class); - HttpResponse responseMock = spy(HttpResponse.class); - doReturn(HttpHeaders.of(new HashMap<>(), (a, b) -> true)).when(responseMock).headers(); - - when(requestBuilderMock.build()).thenReturn(requestMock); - when(httpClientMock.send(eq(requestMock), any(HttpResponse.BodyHandler.class))).thenReturn(responseMock); - when(responseMock.statusCode()).thenReturn(404); - - HttpCacheFetcher fetcher = new HttpCacheFetcher(); - HttpResponse result = fetcher.fetchContent(httpClientMock, requestBuilderMock); - - assertEquals(responseMock, result); - } - - @Test - public void test200ResponseNoEtagOrLastModified() throws Exception { - HttpClient httpClientMock = mock(HttpClient.class); - HttpRequest.Builder requestBuilderMock = mock(HttpRequest.Builder.class); - HttpRequest requestMock = mock(HttpRequest.class); - HttpResponse responseMock = spy(HttpResponse.class); - doReturn(HttpHeaders.of(new HashMap<>(), (a, b) -> true)).when(responseMock).headers(); - - when(requestBuilderMock.build()).thenReturn(requestMock); - when(httpClientMock.send(eq(requestMock), any(HttpResponse.BodyHandler.class))).thenReturn(responseMock); - when(responseMock.statusCode()).thenReturn(200); - - HttpCacheFetcher fetcher = new HttpCacheFetcher(); - fetcher.fetchContent(httpClientMock, requestBuilderMock); - - Field cachedETagField = HttpCacheFetcher.class.getDeclaredField("cachedETag"); - cachedETagField.setAccessible(true); - assertNull(cachedETagField.get(fetcher)); - Field cachedLastModifiedField = HttpCacheFetcher.class.getDeclaredField("cachedLastModified"); - cachedLastModifiedField.setAccessible(true); - assertNull(cachedLastModifiedField.get(fetcher)); - } - - @Test - public void testUpdateCacheOn200Response() throws Exception { - HttpClient httpClientMock = mock(HttpClient.class); - HttpRequest.Builder requestBuilderMock = mock(HttpRequest.Builder.class); - HttpRequest requestMock = mock(HttpRequest.class); - HttpResponse responseMock = spy(HttpResponse.class); - doReturn(HttpHeaders.of( - Map.of("Last-Modified", Arrays.asList("Wed, 21 Oct 2015 07:28:00 GMT"), - "ETag", Arrays.asList("etag-value")), - (a, b) -> true)).when(responseMock).headers(); - - when(requestBuilderMock.build()).thenReturn(requestMock); - when(httpClientMock.send(eq(requestMock), any(HttpResponse.BodyHandler.class))).thenReturn(responseMock); - when(responseMock.statusCode()).thenReturn(200); - HttpCacheFetcher fetcher = new HttpCacheFetcher(); - fetcher.fetchContent(httpClientMock, requestBuilderMock); - - Field cachedETagField = HttpCacheFetcher.class.getDeclaredField("cachedETag"); - cachedETagField.setAccessible(true); - assertEquals("etag-value", cachedETagField.get(fetcher)); - Field cachedLastModifiedField = HttpCacheFetcher.class.getDeclaredField("cachedLastModified"); - cachedLastModifiedField.setAccessible(true); - assertEquals("Wed, 21 Oct 2015 07:28:00 GMT", cachedLastModifiedField.get(fetcher)); - } - - @Test - public void testRequestWithCachedEtagIncludesIfNoneMatchHeader() throws Exception { - HttpClient httpClientMock = mock(HttpClient.class); - HttpRequest.Builder requestBuilderMock = mock(HttpRequest.Builder.class); - HttpRequest requestMock = mock(HttpRequest.class); - HttpResponse responseMock = spy(HttpResponse.class); - doReturn(HttpHeaders.of( - Map.of("ETag", Arrays.asList("12345")), - (a, b) -> true)).when(responseMock).headers(); - - when(requestBuilderMock.build()).thenReturn(requestMock); - when(httpClientMock.send(eq(requestMock), any(HttpResponse.BodyHandler.class))).thenReturn(responseMock); - when(responseMock.statusCode()).thenReturn(200); - - HttpCacheFetcher fetcher = new HttpCacheFetcher(); - fetcher.fetchContent(httpClientMock, requestBuilderMock); - fetcher.fetchContent(httpClientMock, requestBuilderMock); - - verify(requestBuilderMock, times(1)).header("If-None-Match", "12345"); - } - - @Test - public void testNullHttpClientOrRequestBuilder() { - HttpCacheFetcher fetcher = new HttpCacheFetcher(); - HttpRequest.Builder requestBuilderMock = mock(HttpRequest.Builder.class); - - assertThrows(NullPointerException.class, () -> { - fetcher.fetchContent(null, requestBuilderMock); - }); - - assertThrows(NullPointerException.class, () -> { - fetcher.fetchContent(mock(HttpClient.class), null); - }); - } - - @Test - public void testResponseWithUnexpectedStatusCode() throws Exception { - HttpClient httpClientMock = mock(HttpClient.class); - HttpRequest.Builder requestBuilderMock = mock(HttpRequest.Builder.class); - HttpRequest requestMock = mock(HttpRequest.class); - HttpResponse responseMock = spy(HttpResponse.class); - doReturn(HttpHeaders.of(new HashMap<>(), (a, b) -> true)).when(responseMock).headers(); - - when(requestBuilderMock.build()).thenReturn(requestMock); - when(httpClientMock.send(eq(requestMock), any(HttpResponse.BodyHandler.class))).thenReturn(responseMock); - when(responseMock.statusCode()).thenReturn(500); - - HttpCacheFetcher fetcher = new HttpCacheFetcher(); - HttpResponse response = fetcher.fetchContent(httpClientMock, requestBuilderMock); - - assertEquals(500, response.statusCode()); - verify(requestBuilderMock, never()).header(eq("If-None-Match"), anyString()); - verify(requestBuilderMock, never()).header(eq("If-Modified-Since"), anyString()); - } - - @Test - public void testRequestIncludesIfModifiedSinceHeaderWhenLastModifiedCached() throws Exception { - HttpClient httpClientMock = mock(HttpClient.class); - HttpRequest.Builder requestBuilderMock = mock(HttpRequest.Builder.class); - HttpRequest requestMock = mock(HttpRequest.class); - HttpResponse responseMock = spy(HttpResponse.class); - doReturn(HttpHeaders.of( - Map.of("Last-Modified", Arrays.asList("Wed, 21 Oct 2015 07:28:00 GMT")), - (a, b) -> true)).when(responseMock).headers(); - - when(requestBuilderMock.build()).thenReturn(requestMock); - when(httpClientMock.send(eq(requestMock), any(HttpResponse.BodyHandler.class))).thenReturn(responseMock); - when(responseMock.statusCode()).thenReturn(200); - - HttpCacheFetcher fetcher = new HttpCacheFetcher(); - fetcher.fetchContent(httpClientMock, requestBuilderMock); - fetcher.fetchContent(httpClientMock, requestBuilderMock); - - verify(requestBuilderMock).header(eq("If-Modified-Since"), eq("Wed, 21 Oct 2015 07:28:00 GMT")); - } - - @Test - public void testCalls200And304Responses() throws Exception { - HttpClient httpClientMock = mock(HttpClient.class); - HttpRequest.Builder requestBuilderMock = mock(HttpRequest.Builder.class); - HttpRequest requestMock = mock(HttpRequest.class); - HttpResponse responseMock200 = mock(HttpResponse.class); - HttpResponse responseMock304 = mock(HttpResponse.class); - - when(requestBuilderMock.build()).thenReturn(requestMock); - when(httpClientMock.send(eq(requestMock), any(HttpResponse.BodyHandler.class))) - .thenReturn(responseMock200) - .thenReturn(responseMock304); - when(responseMock200.statusCode()).thenReturn(200); - when(responseMock304.statusCode()).thenReturn(304); - - HttpCacheFetcher fetcher = new HttpCacheFetcher(); - fetcher.fetchContent(httpClientMock, requestBuilderMock); - fetcher.fetchContent(httpClientMock, requestBuilderMock); - - verify(responseMock200, times(1)).statusCode(); - verify(responseMock304, times(2)).statusCode(); - } - - @Test - public void testRequestIncludesBothEtagAndLastModifiedHeaders() throws Exception { - HttpClient httpClientMock = mock(HttpClient.class); - HttpRequest.Builder requestBuilderMock = mock(HttpRequest.Builder.class); - HttpRequest requestMock = mock(HttpRequest.class); - HttpResponse responseMock = spy(HttpResponse.class); - doReturn(HttpHeaders.of(new HashMap<>(), (a, b) -> true)).when(responseMock).headers(); - - when(requestBuilderMock.build()).thenReturn(requestMock); - when(httpClientMock.send(eq(requestMock), any(HttpResponse.BodyHandler.class))).thenReturn(responseMock); - when(responseMock.statusCode()).thenReturn(200); - - HttpCacheFetcher fetcher = new HttpCacheFetcher(); - Field cachedETagField = HttpCacheFetcher.class.getDeclaredField("cachedETag"); - cachedETagField.setAccessible(true); - cachedETagField.set(fetcher, "test-etag"); - Field cachedLastModifiedField = HttpCacheFetcher.class.getDeclaredField("cachedLastModified"); - cachedLastModifiedField.setAccessible(true); - cachedLastModifiedField.set(fetcher, "test-last-modified"); - - fetcher.fetchContent(httpClientMock, requestBuilderMock); - - verify(requestBuilderMock).header("If-None-Match", "test-etag"); - verify(requestBuilderMock).header("If-Modified-Since", "test-last-modified"); - } - - @SneakyThrows - @Test - public void testHttpClientSendExceptionPropagation() { - HttpClient httpClientMock = mock(HttpClient.class); - HttpRequest.Builder requestBuilderMock = mock(HttpRequest.Builder.class); - HttpRequest requestMock = mock(HttpRequest.class); - - when(requestBuilderMock.build()).thenReturn(requestMock); - when(httpClientMock.send(eq(requestMock), any(HttpResponse.BodyHandler.class))) - .thenThrow(new IOException("Network error")); - - HttpCacheFetcher fetcher = new HttpCacheFetcher(); - assertThrows(IOException.class, () -> { - fetcher.fetchContent(httpClientMock, requestBuilderMock); - }); - } - - @Test - public void testOnlyEtagAndLastModifiedHeadersCached() throws Exception { - HttpClient httpClientMock = mock(HttpClient.class); - HttpRequest.Builder requestBuilderMock = mock(HttpRequest.Builder.class); - HttpRequest requestMock = mock(HttpRequest.class); - HttpResponse responseMock = spy(HttpResponse.class); - doReturn(HttpHeaders.of( - Map.of("Last-Modified", Arrays.asList("last-modified-value"), - "ETag", Arrays.asList("etag-value")), - (a, b) -> true)).when(responseMock).headers(); - - when(requestBuilderMock.build()).thenReturn(requestMock); - when(httpClientMock.send(eq(requestMock), any(HttpResponse.BodyHandler.class))).thenReturn(responseMock); - when(responseMock.statusCode()).thenReturn(200); - - HttpCacheFetcher fetcher = new HttpCacheFetcher(); - fetcher.fetchContent(httpClientMock, requestBuilderMock); - - verify(requestBuilderMock, never()).header(eq("Some-Other-Header"), anyString()); - } - -} diff --git a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/connector/sync/http/HttpConnectorIntegrationTest.java b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/connector/sync/http/HttpConnectorIntegrationTest.java deleted file mode 100644 index e27a37b0c..000000000 --- a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/connector/sync/http/HttpConnectorIntegrationTest.java +++ /dev/null @@ -1,57 +0,0 @@ -package dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http; - -import static dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpConnectorTest.delay; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assumptions.assumeTrue; - -import dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.QueuePayload; -import java.util.concurrent.BlockingQueue; -import lombok.SneakyThrows; -import lombok.extern.slf4j.Slf4j; -import org.junit.jupiter.api.Test; - -/** - * Integration test for the HttpConnector class, specifically testing the ability to fetch - * raw content from a GitHub URL. This test assumes that integration tests are enabled - * and verifies that the HttpConnector can successfully enqueue data from the specified URL. - * The test initializes the HttpConnector with specific configurations, waits for data - * to be enqueued, and asserts the expected queue size. The connector is shut down - * gracefully after the test execution. - * As this integration test using external request, it is disabled by default, and not part of the CI build. - */ -@Slf4j -class HttpConnectorIntegrationTest { - - @SneakyThrows - @Test - void testGithubRawContent() { - assumeTrue(parseBoolean("integrationTestsEnabled")); - HttpConnector connector = null; - try { - String testUrl = "https://raw.githubusercontent.com/open-feature/java-sdk-contrib/58fe5da7d4e2f6f4ae2c1caf3411a01e84a1dc1a/providers/flagd/version.txt"; - - HttpConnectorOptions httpConnectorOptions = HttpConnectorOptions.builder() - .url(testUrl) - .connectTimeoutSeconds(10) - .requestTimeoutSeconds(10) - .useHttpCache(true) - .pollIntervalSeconds(5) - .build(); - connector = HttpConnector.builder() - .httpConnectorOptions(httpConnectorOptions) - .build(); - BlockingQueue queue = connector.getStreamQueue(); - delay(20000); - assertEquals(1, queue.size()); - } finally { - if (connector != null) { - connector.shutdown(); - } - } - } - - public static boolean parseBoolean(String key) { - return Boolean.parseBoolean(System.getProperty(key, System.getenv(key))); - } - -} diff --git a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/connector/sync/http/HttpConnectorOptionsTest.java b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/connector/sync/http/HttpConnectorOptionsTest.java deleted file mode 100644 index 8b6f87b77..000000000 --- a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/connector/sync/http/HttpConnectorOptionsTest.java +++ /dev/null @@ -1,406 +0,0 @@ -package dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertThrows; -import static org.junit.Assert.assertTrue; - -import java.net.MalformedURLException; -import java.util.HashMap; -import java.util.Map; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import org.junit.Test; - -public class HttpConnectorOptionsTest { - - - @Test - public void testDefaultValuesInitialization() { - HttpConnectorOptions options = HttpConnectorOptions.builder() - .url("https://example.com") - .build(); - - assertEquals(60, options.getPollIntervalSeconds().intValue()); - assertEquals(10, options.getConnectTimeoutSeconds().intValue()); - assertEquals(10, options.getRequestTimeoutSeconds().intValue()); - assertEquals(100, options.getLinkedBlockingQueueCapacity().intValue()); - assertEquals(2, options.getScheduledThreadPoolSize().intValue()); - assertNotNull(options.getHeaders()); - assertTrue(options.getHeaders().isEmpty()); - assertNotNull(options.getHttpClientExecutor()); - assertNull(options.getProxyHost()); - assertNull(options.getProxyPort()); - assertNull(options.getPayloadCacheOptions()); - assertNull(options.getPayloadCache()); - assertNull(options.getUseHttpCache()); - assertEquals("https://example.com", options.getUrl()); - } - - @Test - public void testInvalidUrlFormat() { - MalformedURLException exception = assertThrows( - MalformedURLException.class, - () -> HttpConnectorOptions.builder() - .url("invalid-url") - .build() - ); - - assertNotNull(exception); - } - - @Test - public void testCustomValuesInitialization() { - HttpConnectorOptions options = HttpConnectorOptions.builder() - .pollIntervalSeconds(120) - .connectTimeoutSeconds(20) - .requestTimeoutSeconds(30) - .linkedBlockingQueueCapacity(200) - .scheduledThreadPoolSize(5) - .url("http://example.com") - .build(); - - assertEquals(120, options.getPollIntervalSeconds().intValue()); - assertEquals(20, options.getConnectTimeoutSeconds().intValue()); - assertEquals(30, options.getRequestTimeoutSeconds().intValue()); - assertEquals(200, options.getLinkedBlockingQueueCapacity().intValue()); - assertEquals(5, options.getScheduledThreadPoolSize().intValue()); - assertEquals("http://example.com", options.getUrl()); - } - - @Test - public void testCustomHeadersMap() { - Map customHeaders = new HashMap<>(); - customHeaders.put("Authorization", "Bearer token"); - customHeaders.put("Content-Type", "application/json"); - - HttpConnectorOptions options = HttpConnectorOptions.builder() - .url("http://example.com") - .headers(customHeaders) - .build(); - - assertEquals("Bearer token", options.getHeaders().get("Authorization")); - assertEquals("application/json", options.getHeaders().get("Content-Type")); - } - - @Test - public void testCustomExecutorService() { - ExecutorService customExecutor = Executors.newFixedThreadPool(5); - HttpConnectorOptions options = HttpConnectorOptions.builder() - .url("https://example.com") - .httpClientExecutor(customExecutor) - .build(); - - assertEquals(customExecutor, options.getHttpClientExecutor()); - } - - @Test - public void testSettingPayloadCacheWithValidOptions() { - PayloadCacheOptions cacheOptions = PayloadCacheOptions.builder() - .updateIntervalSeconds(1800) - .build(); - PayloadCache payloadCache = new PayloadCache() { - private String payload; - - @Override - public void put(String payload) { - this.payload = payload; - } - - @Override - public String get() { - return this.payload; - } - }; - - HttpConnectorOptions options = HttpConnectorOptions.builder() - .url("https://example.com") - .payloadCacheOptions(cacheOptions) - .payloadCache(payloadCache) - .build(); - - assertNotNull(options.getPayloadCacheOptions()); - assertNotNull(options.getPayloadCache()); - assertEquals(1800, options.getPayloadCacheOptions().getUpdateIntervalSeconds()); - } - - @Test - public void testProxyConfigurationWithValidHostAndPort() { - HttpConnectorOptions options = HttpConnectorOptions.builder() - .url("https://example.com") - .proxyHost("proxy.example.com") - .proxyPort(8080) - .build(); - - assertEquals("proxy.example.com", options.getProxyHost()); - assertEquals(8080, options.getProxyPort().intValue()); - } - - @Test - public void testLinkedBlockingQueueCapacityOutOfRange() { - IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> { - HttpConnectorOptions.builder() - .url("https://example.com") - .linkedBlockingQueueCapacity(0) - .build(); - }); - assertEquals("linkedBlockingQueueCapacity must be between 1 and 1000", exception.getMessage()); - - exception = assertThrows(IllegalArgumentException.class, () -> { - HttpConnectorOptions.builder() - .url("https://example.com") - .linkedBlockingQueueCapacity(1001) - .build(); - }); - assertEquals("linkedBlockingQueueCapacity must be between 1 and 1000", exception.getMessage()); - } - - @Test - public void testPollIntervalSecondsOutOfRange() { - IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> { - HttpConnectorOptions.builder() - .url("https://example.com") - .pollIntervalSeconds(700) - .build(); - }); - assertEquals("pollIntervalSeconds must be between 1 and 600", exception.getMessage()); - } - - @Test - public void testAdditionalCustomValuesInitialization() { - Map headers = new HashMap<>(); - headers.put("Authorization", "Bearer token"); - ExecutorService executorService = Executors.newFixedThreadPool(2); - PayloadCacheOptions cacheOptions = PayloadCacheOptions.builder().build(); - PayloadCache cache = new PayloadCache() { - @Override - public void put(String payload) { - // do nothing - } - @Override - public String get() { return null; } - }; - - HttpConnectorOptions options = HttpConnectorOptions.builder() - .url("https://example.com") - .pollIntervalSeconds(120) - .connectTimeoutSeconds(20) - .requestTimeoutSeconds(30) - .linkedBlockingQueueCapacity(200) - .scheduledThreadPoolSize(4) - .headers(headers) - .httpClientExecutor(executorService) - .proxyHost("proxy.example.com") - .proxyPort(8080) - .payloadCacheOptions(cacheOptions) - .payloadCache(cache) - .useHttpCache(true) - .build(); - - assertEquals(120, options.getPollIntervalSeconds().intValue()); - assertEquals(20, options.getConnectTimeoutSeconds().intValue()); - assertEquals(30, options.getRequestTimeoutSeconds().intValue()); - assertEquals(200, options.getLinkedBlockingQueueCapacity().intValue()); - assertEquals(4, options.getScheduledThreadPoolSize().intValue()); - assertNotNull(options.getHeaders()); - assertEquals("Bearer token", options.getHeaders().get("Authorization")); - assertNotNull(options.getHttpClientExecutor()); - assertEquals("proxy.example.com", options.getProxyHost()); - assertEquals(8080, options.getProxyPort().intValue()); - assertNotNull(options.getPayloadCacheOptions()); - assertNotNull(options.getPayloadCache()); - assertTrue(options.getUseHttpCache()); - assertEquals("https://example.com", options.getUrl()); - } - - @Test - public void testRequestTimeoutSecondsOutOfRange() { - IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> { - HttpConnectorOptions.builder() - .url("https://example.com") - .requestTimeoutSeconds(61) - .build(); - }); - assertEquals("requestTimeoutSeconds must be between 1 and 60", exception.getMessage()); - } - - @Test - public void testBuilderInitializesAllFields() { - Map headers = new HashMap<>(); - headers.put("Authorization", "Bearer token"); - ExecutorService executorService = Executors.newFixedThreadPool(2); - PayloadCacheOptions cacheOptions = PayloadCacheOptions.builder().build(); - PayloadCache cache = new PayloadCache() { - @Override - public void put(String payload) { - // do nothing - } - @Override - public String get() { return null; } - }; - - HttpConnectorOptions options = HttpConnectorOptions.builder() - .pollIntervalSeconds(120) - .connectTimeoutSeconds(20) - .requestTimeoutSeconds(30) - .linkedBlockingQueueCapacity(200) - .scheduledThreadPoolSize(4) - .headers(headers) - .httpClientExecutor(executorService) - .proxyHost("proxy.example.com") - .proxyPort(8080) - .payloadCacheOptions(cacheOptions) - .payloadCache(cache) - .useHttpCache(true) - .url("https://example.com") - .build(); - - assertEquals(120, options.getPollIntervalSeconds().intValue()); - assertEquals(20, options.getConnectTimeoutSeconds().intValue()); - assertEquals(30, options.getRequestTimeoutSeconds().intValue()); - assertEquals(200, options.getLinkedBlockingQueueCapacity().intValue()); - assertEquals(4, options.getScheduledThreadPoolSize().intValue()); - assertEquals(headers, options.getHeaders()); - assertEquals(executorService, options.getHttpClientExecutor()); - assertEquals("proxy.example.com", options.getProxyHost()); - assertEquals(8080, options.getProxyPort().intValue()); - assertEquals(cacheOptions, options.getPayloadCacheOptions()); - assertEquals(cache, options.getPayloadCache()); - assertTrue(options.getUseHttpCache()); - assertEquals("https://example.com", options.getUrl()); - } - - @Test - public void testScheduledThreadPoolSizeOutOfRange() { - IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> { - HttpConnectorOptions.builder() - .url("https://example.com") - .scheduledThreadPoolSize(11) - .build(); - }); - assertEquals("scheduledThreadPoolSize must be between 1 and 10", exception.getMessage()); - } - - @Test - public void testProxyPortOutOfRange() { - IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> { - HttpConnectorOptions.builder() - .url("https://example.com") - .proxyHost("proxy.example.com") - .proxyPort(70000) // Invalid port, out of range - .build(); - }); - assertEquals("proxyPort must be between 1 and 65535", exception.getMessage()); - } - - @Test - public void testConnectTimeoutSecondsOutOfRange() { - IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> { - HttpConnectorOptions.builder() - .url("https://example.com") - .connectTimeoutSeconds(0) - .build(); - }); - assertEquals("connectTimeoutSeconds must be between 1 and 60", exception.getMessage()); - - exception = assertThrows(IllegalArgumentException.class, () -> { - HttpConnectorOptions.builder() - .url("https://example.com") - .connectTimeoutSeconds(61) - .build(); - }); - assertEquals("connectTimeoutSeconds must be between 1 and 60", exception.getMessage()); - } - - @Test - public void testProxyPortWithoutProxyHost() { - IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> { - HttpConnectorOptions.builder() - .url("https://example.com") - .proxyPort(8080) - .build(); - }); - assertEquals("proxyHost must be set if proxyPort is set", exception.getMessage()); - } - - @Test - public void testDefaultValuesWhenNullParametersProvided() { - HttpConnectorOptions options = HttpConnectorOptions.builder() - .url("https://example.com") - .pollIntervalSeconds(null) - .linkedBlockingQueueCapacity(null) - .scheduledThreadPoolSize(null) - .requestTimeoutSeconds(null) - .connectTimeoutSeconds(null) - .headers(null) - .httpClientExecutor(null) - .proxyHost(null) - .proxyPort(null) - .payloadCacheOptions(null) - .payloadCache(null) - .useHttpCache(null) - .build(); - - assertEquals(60, options.getPollIntervalSeconds().intValue()); - assertEquals(10, options.getConnectTimeoutSeconds().intValue()); - assertEquals(10, options.getRequestTimeoutSeconds().intValue()); - assertEquals(100, options.getLinkedBlockingQueueCapacity().intValue()); - assertEquals(2, options.getScheduledThreadPoolSize().intValue()); - assertNotNull(options.getHeaders()); - assertTrue(options.getHeaders().isEmpty()); - assertNotNull(options.getHttpClientExecutor()); - assertNull(options.getProxyHost()); - assertNull(options.getProxyPort()); - assertNull(options.getPayloadCacheOptions()); - assertNull(options.getPayloadCache()); - assertNull(options.getUseHttpCache()); - assertEquals("https://example.com", options.getUrl()); - } - - @Test - public void testProxyHostWithoutProxyPort() { - IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> { - HttpConnectorOptions.builder() - .url("https://example.com") - .proxyHost("proxy.example.com") - .build(); - }); - assertEquals("proxyPort must be set if proxyHost is set", exception.getMessage()); - } - - @Test - public void testSettingPayloadCacheWithoutOptions() { - PayloadCache mockPayloadCache = new PayloadCache() { - @Override - public void put(String payload) { - // Mock implementation - } - - @Override - public String get() { - return "mockPayload"; - } - }; - - IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> { - HttpConnectorOptions.builder() - .url("https://example.com") - .payloadCache(mockPayloadCache) - .build(); - }); - - assertEquals("payloadCacheOptions must be set if payloadCache is set", exception.getMessage()); - } - - @Test - public void testPayloadCacheOptionsWithoutPayloadCache() { - IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> { - HttpConnectorOptions.builder() - .url("https://example.com") - .payloadCacheOptions(PayloadCacheOptions.builder().build()) - .build(); - }); - assertEquals("payloadCache must be set if payloadCacheOptions is set", exception.getMessage()); - } -} diff --git a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/connector/sync/http/HttpConnectorTest.java b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/connector/sync/http/HttpConnectorTest.java deleted file mode 100644 index a62bc8ecd..000000000 --- a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/connector/sync/http/HttpConnectorTest.java +++ /dev/null @@ -1,412 +0,0 @@ -package dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.doReturn; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.when; - -import dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.QueuePayload; -import dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.QueuePayloadType; -import java.io.IOException; -import java.lang.reflect.Field; -import java.net.http.HttpClient; -import java.net.http.HttpRequest; -import java.net.http.HttpResponse; -import java.util.HashMap; -import java.util.Map; -import java.util.concurrent.BlockingQueue; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.TimeUnit; -import lombok.SneakyThrows; -import lombok.extern.slf4j.Slf4j; -import org.junit.jupiter.api.Test; -import org.mockito.Mockito; - -@Slf4j -class HttpConnectorTest { - - @SneakyThrows - @Test - void testGetStreamQueueInitialAndScheduledPolls() { - String testUrl = "http://example.com"; - HttpClient mockClient = mock(HttpClient.class); - HttpResponse mockResponse = mock(HttpResponse.class); - when(mockResponse.statusCode()).thenReturn(200); - when(mockResponse.body()).thenReturn("test data"); - when(mockClient.send(any(HttpRequest.class), any(HttpResponse.BodyHandler.class))) - .thenReturn(mockResponse); - - HttpConnectorOptions httpConnectorOptions = HttpConnectorOptions.builder() - .url(testUrl) - .httpClientExecutor(Executors.newSingleThreadExecutor()) - .build(); - HttpConnector connector = HttpConnector.builder() - .httpConnectorOptions(httpConnectorOptions) - .build(); - - Field clientField = HttpConnector.class.getDeclaredField("client"); - clientField.setAccessible(true); - clientField.set(connector, mockClient); - - BlockingQueue queue = connector.getStreamQueue(); - - assertFalse(queue.isEmpty()); - QueuePayload payload = queue.poll(); - assertNotNull(payload); - assertEquals(QueuePayloadType.DATA, payload.getType()); - assertEquals("test data", payload.getFlagData()); - - connector.shutdown(); - } - - @SneakyThrows - @Test - void testBuildPollTaskFetchesDataAndAddsToQueue() { - String testUrl = "http://example.com"; - HttpClient mockClient = mock(HttpClient.class); - HttpResponse mockResponse = mock(HttpResponse.class); - when(mockResponse.statusCode()).thenReturn(200); - when(mockResponse.body()).thenReturn("test data"); - when(mockClient.send(any(HttpRequest.class), any(HttpResponse.BodyHandler.class))) - .thenReturn(mockResponse); - - PayloadCache payloadCache = new PayloadCache() { - private String payload; - @Override - public void put(String payload) { - this.payload = payload; - } - - @Override - public String get() { - return payload; - } - }; - HttpConnectorOptions httpConnectorOptions = HttpConnectorOptions.builder() - .url(testUrl) - .proxyHost("proxy-host") - .proxyPort(8080) - .useHttpCache(true) - .payloadCache(payloadCache) - .payloadCacheOptions(PayloadCacheOptions.builder().build()) - .build(); - HttpConnector connector = HttpConnector.builder() - .httpConnectorOptions(httpConnectorOptions) - .build(); - connector.init(); - - Field clientField = HttpConnector.class.getDeclaredField("client"); - clientField.setAccessible(true); - clientField.set(connector, mockClient); - - Runnable pollTask = connector.buildPollTask(); - pollTask.run(); - - Field queueField = HttpConnector.class.getDeclaredField("queue"); - queueField.setAccessible(true); - BlockingQueue queue = (BlockingQueue) queueField.get(connector); - assertFalse(queue.isEmpty()); - QueuePayload payload = queue.poll(); - assertNotNull(payload); - assertEquals(QueuePayloadType.DATA, payload.getType()); - assertEquals("test data", payload.getFlagData()); - } - - @SneakyThrows - @Test - void testHttpRequestIncludesHeaders() { - String testUrl = "http://example.com"; - Map testHeaders = new HashMap<>(); - testHeaders.put("Authorization", "Bearer token"); - testHeaders.put("Content-Type", "application/json"); - - HttpConnectorOptions httpConnectorOptions = HttpConnectorOptions.builder() - .url(testUrl) - .headers(testHeaders) - .build(); - HttpConnector connector = HttpConnector.builder() - .httpConnectorOptions(httpConnectorOptions) - .build(); - - Field headersField = HttpConnector.class.getDeclaredField("headers"); - headersField.setAccessible(true); - Map headers = (Map) headersField.get(connector); - assertNotNull(headers); - assertEquals(2, headers.size()); - assertEquals("Bearer token", headers.get("Authorization")); - assertEquals("application/json", headers.get("Content-Type")); - } - - @SneakyThrows - @Test - void testSuccessfulHttpResponseAddsDataToQueue() { - String testUrl = "http://example.com"; - HttpClient mockClient = mock(HttpClient.class); - HttpResponse mockResponse = mock(HttpResponse.class); - when(mockResponse.statusCode()).thenReturn(200); - when(mockResponse.body()).thenReturn("test data"); - when(mockClient.send(any(HttpRequest.class), any(HttpResponse.BodyHandler.class))) - .thenReturn(mockResponse); - - HttpConnectorOptions httpConnectorOptions = HttpConnectorOptions.builder() - .url(testUrl) - .build(); - HttpConnector connector = HttpConnector.builder() - .httpConnectorOptions(httpConnectorOptions) - .build(); - - Field clientField = HttpConnector.class.getDeclaredField("client"); - clientField.setAccessible(true); - clientField.set(connector, mockClient); - - BlockingQueue queue = connector.getStreamQueue(); - - assertFalse(queue.isEmpty()); - QueuePayload payload = queue.poll(); - assertNotNull(payload); - assertEquals(QueuePayloadType.DATA, payload.getType()); - assertEquals("test data", payload.getFlagData()); - } - - @SneakyThrows - @Test - void testInitFailureUsingCache() { - String testUrl = "http://example.com"; - HttpClient mockClient = mock(HttpClient.class); - HttpResponse mockResponse = mock(HttpResponse.class); - when(mockResponse.statusCode()).thenReturn(200); - when(mockClient.send(any(HttpRequest.class), any(HttpResponse.BodyHandler.class))) - .thenThrow(new IOException("Simulated IO Exception")); - - final String cachedData = "cached data"; - PayloadCache payloadCache = new PayloadCache() { - @Override - public void put(String payload) { - // do nothing - } - - @Override - public String get() { - return cachedData; - } - }; - - HttpConnectorOptions httpConnectorOptions = HttpConnectorOptions.builder() - .url(testUrl) - .payloadCache(payloadCache) - .payloadCacheOptions(PayloadCacheOptions.builder().build()) - .build(); - HttpConnector connector = HttpConnector.builder() - .httpConnectorOptions(httpConnectorOptions) - .build(); - - Field clientField = HttpConnector.class.getDeclaredField("client"); - clientField.setAccessible(true); - clientField.set(connector, mockClient); - - BlockingQueue queue = connector.getStreamQueue(); - - assertFalse(queue.isEmpty()); - QueuePayload payload = queue.poll(); - assertNotNull(payload); - assertEquals(QueuePayloadType.DATA, payload.getType()); - assertEquals(cachedData, payload.getFlagData()); - } - - @SneakyThrows - @Test - void testQueueBecomesFull() { - String testUrl = "http://example.com"; - int queueCapacity = 1; - HttpConnectorOptions httpConnectorOptions = HttpConnectorOptions.builder() - .url(testUrl) - .linkedBlockingQueueCapacity(queueCapacity) - .build(); - HttpConnector connector = HttpConnector.builder() - .httpConnectorOptions(httpConnectorOptions) - .build(); - - BlockingQueue queue = connector.getStreamQueue(); - - queue.offer(new QueuePayload(QueuePayloadType.DATA, "test data 1")); - - boolean wasOffered = queue.offer(new QueuePayload(QueuePayloadType.DATA, "test data 2")); - - assertFalse(wasOffered, "Queue should be full and not accept more items"); - } - - @SneakyThrows - @Test - void testShutdownProperlyTerminatesSchedulerAndHttpClientExecutor() throws InterruptedException { - ExecutorService mockHttpClientExecutor = mock(ExecutorService.class); - ScheduledExecutorService mockScheduler = mock(ScheduledExecutorService.class); - String testUrl = "http://example.com"; - - HttpConnectorOptions httpConnectorOptions = HttpConnectorOptions.builder() - .url(testUrl) - .httpClientExecutor(mockHttpClientExecutor) - .build(); - HttpConnector connector = HttpConnector.builder() - .httpConnectorOptions(httpConnectorOptions) - .build(); - - Field schedulerField = HttpConnector.class.getDeclaredField("scheduler"); - schedulerField.setAccessible(true); - schedulerField.set(connector, mockScheduler); - - connector.shutdown(); - - Mockito.verify(mockScheduler).shutdown(); - Mockito.verify(mockHttpClientExecutor).shutdown(); - } - - @SneakyThrows - @Test - void testHttpResponseNonSuccessStatusCode() { - String testUrl = "http://example.com"; - HttpClient mockClient = mock(HttpClient.class); - HttpResponse mockResponse = mock(HttpResponse.class); - when(mockResponse.statusCode()).thenReturn(404); - when(mockClient.send(any(HttpRequest.class), any(HttpResponse.BodyHandler.class))) - .thenReturn(mockResponse); - - HttpConnectorOptions httpConnectorOptions = HttpConnectorOptions.builder() - .url(testUrl) - .build(); - HttpConnector connector = HttpConnector.builder() - .httpConnectorOptions(httpConnectorOptions) - .build(); - - Field clientField = HttpConnector.class.getDeclaredField("client"); - clientField.setAccessible(true); - clientField.set(connector, mockClient); - - BlockingQueue queue = connector.getStreamQueue(); - - assertTrue(queue.isEmpty(), "Queue should be empty when response status is non-200"); - } - - @SneakyThrows - @Test - void testHttpRequestFailsWithException() { - String testUrl = "http://example.com"; - HttpClient mockClient = mock(HttpClient.class); - HttpConnectorOptions httpConnectorOptions = HttpConnectorOptions.builder() - .url(testUrl) - .build(); - HttpConnector connector = HttpConnector.builder() - .httpConnectorOptions(httpConnectorOptions) - .build(); - - Field clientField = HttpConnector.class.getDeclaredField("client"); - clientField.setAccessible(true); - clientField.set(connector, mockClient); - - when(mockClient.send(any(HttpRequest.class), any(HttpResponse.BodyHandler.class))) - .thenThrow(new RuntimeException("Test exception")); - - BlockingQueue queue = connector.getStreamQueue(); - - assertTrue(queue.isEmpty(), "Queue should be empty when request fails with exception"); - } - - @SneakyThrows - @Test - void testHttpRequestFailsWithIoexception() { - String testUrl = "http://example.com"; - HttpClient mockClient = mock(HttpClient.class); - HttpConnectorOptions httpConnectorOptions = HttpConnectorOptions.builder() - .url(testUrl) - .build(); - HttpConnector connector = HttpConnector.builder() - .httpConnectorOptions(httpConnectorOptions) - .build(); - - Field clientField = HttpConnector.class.getDeclaredField("client"); - clientField.setAccessible(true); - clientField.set(connector, mockClient); - - when(mockClient.send(any(HttpRequest.class), any(HttpResponse.BodyHandler.class))) - .thenThrow(new IOException("Simulated IO Exception")); - - connector.getStreamQueue(); - - Field queueField = HttpConnector.class.getDeclaredField("queue"); - queueField.setAccessible(true); - BlockingQueue queue = (BlockingQueue) queueField.get(connector); - assertTrue(queue.isEmpty(), "Queue should be empty due to IOException"); - } - - @SneakyThrows - @Test - void testScheduledPollingContinuesAtFixedIntervals() { - String testUrl = "http://exampOle.com"; - HttpResponse mockResponse = mock(HttpResponse.class); - when(mockResponse.statusCode()).thenReturn(200); - when(mockResponse.body()).thenReturn("test data"); - - HttpConnectorOptions httpConnectorOptions = HttpConnectorOptions.builder() - .url(testUrl) - .build(); - HttpConnector connector = spy(HttpConnector.builder() - .httpConnectorOptions(httpConnectorOptions) - .build()); - - doReturn(mockResponse).when(connector).execute(any()); - - BlockingQueue queue = connector.getStreamQueue(); - - delay(2000); - assertFalse(queue.isEmpty()); - QueuePayload payload = queue.poll(); - assertNotNull(payload); - assertEquals(QueuePayloadType.DATA, payload.getType()); - assertEquals("test data", payload.getFlagData()); - - connector.shutdown(); - } - - @SneakyThrows - @Test - void testQueuePayloadTypeSetToDataOnSuccess() { - String testUrl = "http://example.com"; - HttpClient mockClient = mock(HttpClient.class); - HttpResponse mockResponse = mock(HttpResponse.class); - - when(mockResponse.statusCode()).thenReturn(200); - when(mockResponse.body()).thenReturn("response body"); - when(mockClient.send(any(HttpRequest.class), any(HttpResponse.BodyHandler.class))) - .thenReturn(mockResponse); - - HttpConnectorOptions httpConnectorOptions = HttpConnectorOptions.builder() - .url(testUrl) - .build(); - HttpConnector connector = HttpConnector.builder() - .httpConnectorOptions(httpConnectorOptions) - .build(); - - Field clientField = HttpConnector.class.getDeclaredField("client"); - clientField.setAccessible(true); - clientField.set(connector, mockClient); - - BlockingQueue queue = connector.getStreamQueue(); - - QueuePayload payload = queue.poll(1, TimeUnit.SECONDS); - assertNotNull(payload); - assertEquals(QueuePayloadType.DATA, payload.getType()); - assertEquals("response body", payload.getFlagData()); - } - - @SneakyThrows - protected static void delay(long ms) { - Thread.sleep(ms); - } - -} diff --git a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/connector/sync/http/PayloadCacheWrapperTest.java b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/connector/sync/http/PayloadCacheWrapperTest.java deleted file mode 100644 index 65f92e0ae..000000000 --- a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/connector/sync/http/PayloadCacheWrapperTest.java +++ /dev/null @@ -1,267 +0,0 @@ -package dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http; - -import static dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.http.HttpConnectorTest.delay; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.mockito.Mockito.doReturn; -import static org.mockito.Mockito.doThrow; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import java.lang.reflect.Field; -import lombok.SneakyThrows; -import org.junit.jupiter.api.Test; - - -public class PayloadCacheWrapperTest { - - @Test - public void testConstructorInitializesWithValidParameters() { - PayloadCache mockCache = mock(PayloadCache.class); - PayloadCacheOptions options = PayloadCacheOptions.builder() - .updateIntervalSeconds(600) - .build(); - - PayloadCacheWrapper wrapper = PayloadCacheWrapper.builder() - .payloadCache(mockCache) - .payloadCacheOptions(options) - .build(); - - assertNotNull(wrapper); - - String testPayload = "test-payload"; - wrapper.updatePayloadIfNeeded(testPayload); - wrapper.get(); - - verify(mockCache).put(testPayload); - verify(mockCache).get(); - } - - @Test - public void testConstructorThrowsExceptionForInvalidInterval() { - PayloadCache mockCache = mock(PayloadCache.class); - PayloadCacheOptions options = PayloadCacheOptions.builder() - .updateIntervalSeconds(0) - .build(); - - PayloadCacheWrapper.PayloadCacheWrapperBuilder payloadCacheWrapperBuilder = PayloadCacheWrapper.builder() - .payloadCache(mockCache) - .payloadCacheOptions(options); - IllegalArgumentException exception = assertThrows( - IllegalArgumentException.class, - payloadCacheWrapperBuilder::build - ); - - assertEquals("pollIntervalSeconds must be larger than 0", exception.getMessage()); - } - - @Test - public void testUpdateSkipsWhenIntervalNotPassed() { - PayloadCache mockCache = mock(PayloadCache.class); - PayloadCacheOptions options = PayloadCacheOptions.builder() - .updateIntervalSeconds(600) - .build(); - PayloadCacheWrapper wrapper = PayloadCacheWrapper.builder() - .payloadCache(mockCache) - .payloadCacheOptions(options) - .build(); - - String initialPayload = "initial-payload"; - wrapper.updatePayloadIfNeeded(initialPayload); - - String newPayload = "new-payload"; - wrapper.updatePayloadIfNeeded(newPayload); - - verify(mockCache, times(1)).put(initialPayload); - verify(mockCache, never()).put(newPayload); - } - - @Test - public void testUpdatePayloadIfNeededHandlesPutException() { - PayloadCache mockCache = mock(PayloadCache.class); - PayloadCacheOptions options = PayloadCacheOptions.builder() - .updateIntervalSeconds(600) - .build(); - PayloadCacheWrapper wrapper = PayloadCacheWrapper.builder() - .payloadCache(mockCache) - .payloadCacheOptions(options) - .build(); - String testPayload = "test-payload"; - - doThrow(new RuntimeException("put exception")).when(mockCache).put(testPayload); - - wrapper.updatePayloadIfNeeded(testPayload); - - verify(mockCache).put(testPayload); - } - - @Test - public void testUpdatePayloadIfNeededUpdatesCacheAfterInterval() { - PayloadCache mockCache = mock(PayloadCache.class); - PayloadCacheOptions options = PayloadCacheOptions.builder() - .updateIntervalSeconds(1) // 1 second interval for quick test - .build(); - PayloadCacheWrapper wrapper = PayloadCacheWrapper.builder() - .payloadCache(mockCache) - .payloadCacheOptions(options) - .build(); - - String initialPayload = "initial-payload"; - String newPayload = "new-payload"; - - wrapper.updatePayloadIfNeeded(initialPayload); - delay(1100); - wrapper.updatePayloadIfNeeded(newPayload); - - verify(mockCache).put(initialPayload); - verify(mockCache).put(newPayload); - } - - @Test - public void testGetReturnsNullWhenCacheGetThrowsException() { - PayloadCache mockCache = mock(PayloadCache.class); - PayloadCacheOptions options = PayloadCacheOptions.builder() - .updateIntervalSeconds(600) - .build(); - PayloadCacheWrapper wrapper = PayloadCacheWrapper.builder() - .payloadCache(mockCache) - .payloadCacheOptions(options) - .build(); - - when(mockCache.get()).thenThrow(new RuntimeException("Cache get failed")); - - String result = wrapper.get(); - - assertNull(result); - - verify(mockCache).get(); - } - - @Test - public void test_get_returns_cached_payload() { - PayloadCache mockCache = mock(PayloadCache.class); - PayloadCacheOptions options = PayloadCacheOptions.builder() - .updateIntervalSeconds(600) - .build(); - PayloadCacheWrapper wrapper = PayloadCacheWrapper.builder() - .payloadCache(mockCache) - .payloadCacheOptions(options) - .build(); - String expectedPayload = "cached-payload"; - when(mockCache.get()).thenReturn(expectedPayload); - - String actualPayload = wrapper.get(); - - assertEquals(expectedPayload, actualPayload); - - verify(mockCache).get(); - } - - @Test - public void test_first_call_updates_cache() { - PayloadCache mockCache = mock(PayloadCache.class); - PayloadCacheOptions options = PayloadCacheOptions.builder() - .updateIntervalSeconds(600) - .build(); - PayloadCacheWrapper wrapper = PayloadCacheWrapper.builder() - .payloadCache(mockCache) - .payloadCacheOptions(options) - .build(); - - String testPayload = "initial-payload"; - - wrapper.updatePayloadIfNeeded(testPayload); - - verify(mockCache).put(testPayload); - } - - @Test - public void test_update_payload_once_within_interval() { - PayloadCache mockCache = mock(PayloadCache.class); - PayloadCacheOptions options = PayloadCacheOptions.builder() - .updateIntervalSeconds(1) // 1 second interval - .build(); - PayloadCacheWrapper wrapper = PayloadCacheWrapper.builder() - .payloadCache(mockCache) - .payloadCacheOptions(options) - .build(); - - String testPayload = "test-payload"; - - wrapper.updatePayloadIfNeeded(testPayload); - wrapper.updatePayloadIfNeeded(testPayload); - - verify(mockCache, times(1)).put(testPayload); - } - - @SneakyThrows - @Test - public void test_last_update_time_ms_updated_after_successful_cache_update() { - PayloadCache mockCache = mock(PayloadCache.class); - PayloadCacheOptions options = PayloadCacheOptions.builder() - .updateIntervalSeconds(600) - .build(); - PayloadCacheWrapper wrapper = PayloadCacheWrapper.builder() - .payloadCache(mockCache) - .payloadCacheOptions(options) - .build(); - String testPayload = "test-payload"; - - wrapper.updatePayloadIfNeeded(testPayload); - - verify(mockCache).put(testPayload); - - Field lastUpdateTimeMsField = PayloadCacheWrapper.class.getDeclaredField("lastUpdateTimeMs"); - lastUpdateTimeMsField.setAccessible(true); - long lastUpdateTimeMs = (Long) lastUpdateTimeMsField.get(wrapper); - - assertTrue(System.currentTimeMillis() - lastUpdateTimeMs < 1000, - "lastUpdateTimeMs should be updated to current time"); - } - - @Test - public void test_update_payload_if_needed_respects_update_interval() { - PayloadCache mockCache = mock(PayloadCache.class); - PayloadCacheOptions options = PayloadCacheOptions.builder() - .updateIntervalSeconds(600) - .build(); - PayloadCacheWrapper wrapper = spy(PayloadCacheWrapper.builder() - .payloadCache(mockCache) - .payloadCacheOptions(options) - .build()); - - String testPayload = "test-payload"; - long initialTime = System.currentTimeMillis(); - long updateIntervalMs = options.getUpdateIntervalSeconds() * 1000L; - - doReturn(initialTime).when(wrapper).getCurrentTimeMillis(); - - // First update should succeed - wrapper.updatePayloadIfNeeded(testPayload); - - // Verify the payload was updated - verify(mockCache).put(testPayload); - - // Attempt to update before interval has passed - doReturn(initialTime + updateIntervalMs - 1).when(wrapper).getCurrentTimeMillis(); - wrapper.updatePayloadIfNeeded(testPayload); - - // Verify the payload was not updated again - verify(mockCache, times(1)).put(testPayload); - - // Update after interval has passed - doReturn(initialTime + updateIntervalMs + 1).when(wrapper).getCurrentTimeMillis(); - wrapper.updatePayloadIfNeeded(testPayload); - - // Verify the payload was updated again - verify(mockCache, times(2)).put(testPayload); - } - -} From 59ffacb41273a3eaa4926df28a7d0fa8b6e37f21 Mon Sep 17 00:00:00 2001 From: liran2000 Date: Thu, 24 Apr 2025 17:47:28 +0300 Subject: [PATCH 09/40] move to tool - draft - cont. Signed-off-by: liran2000 --- providers/flagd/README.md | 53 +++---------------- .../test/resources/simplelogger.properties | 2 +- 2 files changed, 7 insertions(+), 48 deletions(-) diff --git a/providers/flagd/README.md b/providers/flagd/README.md index f848ca5f6..de0d8a091 100644 --- a/providers/flagd/README.md +++ b/providers/flagd/README.md @@ -54,45 +54,6 @@ The value is updated with every (re)connection to the sync implementation. This can be used to enrich evaluations with such data. If the `in-process` mode is not used, and before the provider is ready, the `getSyncMetadata` returns an empty map. -#### Http Connector -HttpConnector is responsible for polling data from a specified URL at regular intervals. -It is leveraging Http cache mechanism with 'ETag' header, then when receiving 304 Not Modified response, -reducing traffic, reducing rate limits effects and changes updates. Can be enabled via useHttpCache option. -The implementation is using Java HttpClient. - -##### Use cases and benefits -* Reduce infrastructure/devops work, without additional containers needed. -* Use as an additional provider for fallback / internal backup service via multi-provider. - -##### What happens if the Http source is down when application is starting ? - -It supports optional fail-safe initialization via cache, such that on initial fetch error following by -source downtime window, initial payload is taken from cache to avoid starting with default values until -the source is back up. Therefore, the cache ttl expected to be higher than the expected source -down-time to recover from during initialization. - -##### Sample flow -Sample flow can use: -- Github as the flags payload source. -- Redis cache as a fail-safe initialization cache. - -Sample flow of initialization during Github down-time window, showing that application can still use flags -values as fetched from cache. -```mermaid -sequenceDiagram - participant Provider - participant Github - participant Redis - - break source downtime - Provider->>Github: initialize - Github->>Provider: failure - end - Provider->>Redis: fetch - Redis->>Provider: last payload - -``` - ### Offline mode (File resolver) In-process resolvers can also work in an offline mode. @@ -113,17 +74,15 @@ This mode is useful for local development, tests and offline applications. #### Custom Connector You can include a custom connector as a configuration option to customize how the in-process resolver fetches flags. -The custom connector must implement the [QueueSource interface](https://github.com/open-feature/java-sdk-contrib/blob/main/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/connector/QueueSource.java). +The custom connector must implement the [Connector interface](https://github.com/open-feature/java-sdk-contrib/blob/main/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/connector/Connector.java). ```java -QueueSource connector = HttpConnector.builder() - .url(testUrl) - .build(); +Connector myCustomConnector = new MyCustomConnector(); FlagdOptions options = - FlagdOptions.builder() - .resolverType(Config.Resolver.IN_PROCESS) - .customConnector(myCustomConnector) - .build(); + FlagdOptions.builder() + .resolverType(Config.Resolver.IN_PROCESS) + .customConnector(myCustomConnector) + .build(); FlagdProvider flagdProvider = new FlagdProvider(options); ``` diff --git a/tools/flagd-http-connector/src/test/resources/simplelogger.properties b/tools/flagd-http-connector/src/test/resources/simplelogger.properties index d9d489e82..80c478930 100644 --- a/tools/flagd-http-connector/src/test/resources/simplelogger.properties +++ b/tools/flagd-http-connector/src/test/resources/simplelogger.properties @@ -1,3 +1,3 @@ -org.org.slf4j.simpleLogger.defaultLogLevel=debug +org.slf4j.simpleLogger.defaultLogLevel=debug io.grpc.level=trace From 64054725bf84418d1724cd42b24adb8e9cd0ea43 Mon Sep 17 00:00:00 2001 From: liran2000 Date: Sat, 26 Apr 2025 16:34:57 +0300 Subject: [PATCH 10/40] add Configuration section to readme Signed-off-by: liran2000 --- tools/flagd-http-connector/README.md | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/tools/flagd-http-connector/README.md b/tools/flagd-http-connector/README.md index f6925f4bf..78b459af8 100644 --- a/tools/flagd-http-connector/README.md +++ b/tools/flagd-http-connector/README.md @@ -79,3 +79,26 @@ FlagdOptions options = FlagdProvider flagdProvider = new FlagdProvider(options); ``` + +### Configuration +The Http Connector can be configured using the following properties in the `HttpConnectorOptions` class.: + +| Property Name | Type | Description | +|-------------------------------------------|---------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| url | String | The URL to poll for updates. This is a required field. | +| pollIntervalSeconds | Integer | The interval in seconds at which the connector will poll the URL for updates. Default is 60 seconds. | +| connectTimeoutSeconds | Integer | The timeout in seconds for establishing a connection to the URL. Default is 10 seconds. | +| requestTimeoutSeconds | Integer | The timeout in seconds for the request to complete. Default is 10 seconds. | +| linkedBlockingQueueCapacity | Integer | The capacity of the linked blocking queue used for processing requests. Default is 100. | +| scheduledThreadPoolSize | Integer | The size of the scheduled thread pool used for processing requests. Default is 2. | +| headers | Map | A map of headers to be included in the request. Default is an empty map. | +| httpClientExecutor | ExecutorService | The executor service used for making HTTP requests. Default is a fixed thread pool with 1 thread. | +| proxyHost | String | The host of the proxy server to use for requests. Default is null. | +| proxyPort | Integer | The port of the proxy server to use for requests. Default is null. | +| payloadCacheOptions | PayloadCacheOptions | Options for configuring the payload cache. Default is null. | +| payloadCache | PayloadCache | The payload cache to use for caching responses. Default is null. | +| useHttpCache | Boolean | Whether to use HTTP caching for the requests. Default is false. | +| PayloadCacheOptions.updateIntervalSeconds | Integer | The interval, in seconds, at which the cache is updated. By default, this is set to 30 minutes. The goal is to avoid overloading fallback cache writes, since the cache serves only as a fallback mechanism. Typically, this value can be tuned to be shorter than the cache's TTL, balancing the need to minimize unnecessary updates while still handling edge cases effectively. | + + + From 512d3a2dc4f5e0b4368b1bb3c82219425fa5e504 Mon Sep 17 00:00:00 2001 From: liran2000 Date: Sat, 26 Apr 2025 16:50:12 +0300 Subject: [PATCH 11/40] readme update Signed-off-by: liran2000 --- tools/flagd-http-connector/README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tools/flagd-http-connector/README.md b/tools/flagd-http-connector/README.md index 78b459af8..39a3f3336 100644 --- a/tools/flagd-http-connector/README.md +++ b/tools/flagd-http-connector/README.md @@ -4,8 +4,7 @@ Http Connector is a tool for [flagd](https://github.com/open-feature/flagd) in-process resolver. This mode performs flag evaluations locally (in-process). -Flag configurations for evaluation are obtained via gRPC protocol using -[sync protobuf schema](https://buf.build/open-feature/flagd/file/main:sync/v1/sync_service.proto) service definition. +Flag configurations for evaluation are obtained via Http. ## Http Connector functionality From d615b0c85f3299bdc3e85050e3600e358c61432f Mon Sep 17 00:00:00 2001 From: liran2000 Date: Sun, 27 Apr 2025 15:03:36 +0300 Subject: [PATCH 12/40] updates Signed-off-by: liran2000 --- tools/flagd-http-connector/pom.xml | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/tools/flagd-http-connector/pom.xml b/tools/flagd-http-connector/pom.xml index 8945c7e97..0f979b14d 100644 --- a/tools/flagd-http-connector/pom.xml +++ b/tools/flagd-http-connector/pom.xml @@ -9,7 +9,7 @@ ../../pom.xml dev.openfeature.contrib.tools - flagd-http-connector + flagdhttpconnector 0.0.1 flagd-http-connector @@ -38,17 +38,6 @@ 3.17.0 - - org.junit.jupiter - junit-jupiter-api - test - - - org.junit.jupiter - junit-jupiter-api - test - - From 36f5bc92a767c6d0637ea8a13c86ac56568b6f7e Mon Sep 17 00:00:00 2001 From: liran2000 Date: Sun, 27 Apr 2025 15:09:37 +0300 Subject: [PATCH 13/40] updates Signed-off-by: liran2000 --- .../process/storage/connector/sync/http/HttpCacheFetcher.java | 4 ---- .../process/storage/connector/sync/http/HttpConnector.java | 2 +- .../process/storage/connector/sync/http/PayloadCache.java | 4 ++-- 3 files changed, 3 insertions(+), 7 deletions(-) diff --git a/tools/flagd-http-connector/src/main/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/HttpCacheFetcher.java b/tools/flagd-http-connector/src/main/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/HttpCacheFetcher.java index 0cc069032..b457b4bd2 100644 --- a/tools/flagd-http-connector/src/main/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/HttpCacheFetcher.java +++ b/tools/flagd-http-connector/src/main/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/HttpCacheFetcher.java @@ -13,10 +13,6 @@ * Updates the cached ETag and Last-Modified values upon receiving a 200 OK response. * It does not store the cached response, assuming not needed after first successful fetching. * Non thread-safe. - * - * @param httpClient the HTTP client used to send the request - * @param httpRequestBuilder the builder for constructing the HTTP request - * @return the HTTP response received from the server */ @Slf4j public class HttpCacheFetcher { diff --git a/tools/flagd-http-connector/src/main/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/HttpConnector.java b/tools/flagd-http-connector/src/main/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/HttpConnector.java index 3eb8a116d..a2346d492 100644 --- a/tools/flagd-http-connector/src/main/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/HttpConnector.java +++ b/tools/flagd-http-connector/src/main/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/HttpConnector.java @@ -161,7 +161,7 @@ private boolean fetchAndUpdate() { payloadCacheWrapper.updatePayloadIfNeeded(payload) ); } - return payload != null; + return true; } private static boolean isSuccessful(HttpResponse response) { diff --git a/tools/flagd-http-connector/src/main/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/PayloadCache.java b/tools/flagd-http-connector/src/main/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/PayloadCache.java index 4af5f5f1d..da67b23e6 100644 --- a/tools/flagd-http-connector/src/main/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/PayloadCache.java +++ b/tools/flagd-http-connector/src/main/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/PayloadCache.java @@ -1,6 +1,6 @@ package dev.openfeature.contrib.tools.flagd.resolver.process.storage.connector.sync.http; public interface PayloadCache { - public void put(String payload); - public String get(); + void put(String payload); + String get(); } From ec5f1581a0e5e2679052e76fe1c9420d9cb653ef Mon Sep 17 00:00:00 2001 From: liran2000 Date: Sun, 27 Apr 2025 15:33:27 +0300 Subject: [PATCH 14/40] updates Signed-off-by: liran2000 --- .../checkstyle-suppressions.xml | 8 + tools/flagd-http-connector/checkstyle.xml | 442 ++++++++++++++++++ .../test/resources/simplelogger.properties | 5 +- 3 files changed, 452 insertions(+), 3 deletions(-) create mode 100644 tools/flagd-http-connector/checkstyle-suppressions.xml create mode 100644 tools/flagd-http-connector/checkstyle.xml diff --git a/tools/flagd-http-connector/checkstyle-suppressions.xml b/tools/flagd-http-connector/checkstyle-suppressions.xml new file mode 100644 index 000000000..3d374f555 --- /dev/null +++ b/tools/flagd-http-connector/checkstyle-suppressions.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/tools/flagd-http-connector/checkstyle.xml b/tools/flagd-http-connector/checkstyle.xml new file mode 100644 index 000000000..f8ca27ad6 --- /dev/null +++ b/tools/flagd-http-connector/checkstyle.xml @@ -0,0 +1,442 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tools/flagd-http-connector/src/test/resources/simplelogger.properties b/tools/flagd-http-connector/src/test/resources/simplelogger.properties index 80c478930..50b5bd1bd 100644 --- a/tools/flagd-http-connector/src/test/resources/simplelogger.properties +++ b/tools/flagd-http-connector/src/test/resources/simplelogger.properties @@ -1,3 +1,2 @@ -org.slf4j.simpleLogger.defaultLogLevel=debug - -io.grpc.level=trace +org.slf4j.simpleLogger.defaultLogLevel=info +io.grpc.level=info From 17d5d0ba7d8a24914612446daaa182c238ccd4d5 Mon Sep 17 00:00:00 2001 From: liran2000 Date: Sun, 27 Apr 2025 15:55:42 +0300 Subject: [PATCH 15/40] updates Signed-off-by: liran2000 --- .../connector/sync/http/HttpCacheFetcher.java | 6 ++++++ .../storage/connector/sync/http/HttpConnector.java | 11 ++++++++--- .../connector/sync/http/HttpConnectorOptions.java | 11 +++++++++-- .../storage/connector/sync/http/PayloadCache.java | 4 ++++ .../connector/sync/http/PayloadCacheOptions.java | 8 ++++---- .../connector/sync/http/PayloadCacheWrapper.java | 13 +++++++++++++ .../connector/sync/http/util/ConcurrentUtils.java | 4 ++-- 7 files changed, 46 insertions(+), 11 deletions(-) diff --git a/tools/flagd-http-connector/src/main/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/HttpCacheFetcher.java b/tools/flagd-http-connector/src/main/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/HttpCacheFetcher.java index b457b4bd2..fbf158ebf 100644 --- a/tools/flagd-http-connector/src/main/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/HttpCacheFetcher.java +++ b/tools/flagd-http-connector/src/main/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/HttpCacheFetcher.java @@ -19,6 +19,12 @@ public class HttpCacheFetcher { private String cachedETag = null; private String cachedLastModified = null; + /** + * Fetches content from the given HTTP endpoint using the provided HttpClient and HttpRequest.Builder. + * @param httpClient the HttpClient to use for the request + * @param httpRequestBuilder the HttpRequest.Builder to build the request + * @return the HttpResponse containing the response body as a String + */ @SneakyThrows public HttpResponse fetchContent(HttpClient httpClient, HttpRequest.Builder httpRequestBuilder) { if (cachedETag != null) { diff --git a/tools/flagd-http-connector/src/main/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/HttpConnector.java b/tools/flagd-http-connector/src/main/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/HttpConnector.java index a2346d492..baeb68acf 100644 --- a/tools/flagd-http-connector/src/main/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/HttpConnector.java +++ b/tools/flagd-http-connector/src/main/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/HttpConnector.java @@ -5,6 +5,7 @@ import dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.QueuePayload; import dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.QueuePayloadType; import dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.QueueSource; +import dev.openfeature.contrib.tools.flagd.resolver.process.storage.connector.sync.http.util.ConcurrentUtils; import java.io.IOException; import java.net.InetSocketAddress; import java.net.ProxySelector; @@ -20,7 +21,6 @@ import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; -import dev.openfeature.contrib.tools.flagd.resolver.process.storage.connector.sync.http.util.ConcurrentUtils; import lombok.Builder; import lombok.NonNull; import lombok.extern.slf4j.Slf4j; @@ -33,7 +33,7 @@ * It uses a ScheduledExecutorService to schedule polling tasks and an ExecutorService for HTTP client execution. * The class also provides methods to initialize, retrieve the stream queue, and shutdown the connector gracefully. * It supports optional fail-safe initialization via cache. - * + *

* See readme - Http Connector section. */ @Slf4j @@ -53,6 +53,10 @@ public class HttpConnector implements QueueSource { @NonNull private String url; + /** + * HttpConnector constructor. + * @param httpConnectorOptions options for configuring the HttpConnector. + */ @Builder public HttpConnector(HttpConnectorOptions httpConnectorOptions) { this.pollIntervalSeconds = httpConnectorOptions.getPollIntervalSeconds(); @@ -168,7 +172,8 @@ private static boolean isSuccessful(HttpResponse response) { return response.statusCode() == 200 || response.statusCode() == 304; } - protected HttpResponse execute(HttpRequest.Builder requestBuilder) throws IOException, InterruptedException { + protected HttpResponse execute(HttpRequest.Builder requestBuilder) + throws IOException, InterruptedException { if (httpCacheFetcher != null) { return httpCacheFetcher.fetchContent(client, requestBuilder); } diff --git a/tools/flagd-http-connector/src/main/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/HttpConnectorOptions.java b/tools/flagd-http-connector/src/main/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/HttpConnectorOptions.java index 740d54ddf..a3494e724 100644 --- a/tools/flagd-http-connector/src/main/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/HttpConnectorOptions.java +++ b/tools/flagd-http-connector/src/main/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/HttpConnectorOptions.java @@ -10,6 +10,9 @@ import lombok.NonNull; import lombok.SneakyThrows; +/** + * Represents configuration options for the HTTP connector. + */ @Getter public class HttpConnectorOptions { @@ -40,6 +43,9 @@ public class HttpConnectorOptions { @NonNull private String url; + /** + * HttpConnectorOptions constructor. + */ @Builder public HttpConnectorOptions(Integer pollIntervalSeconds, Integer linkedBlockingQueueCapacity, Integer scheduledThreadPoolSize, Integer requestTimeoutSeconds, Integer connectTimeoutSeconds, String url, @@ -92,7 +98,8 @@ private void validate(String url, Integer pollIntervalSeconds, Integer linkedBlo String proxyHost, Integer proxyPort, PayloadCacheOptions payloadCacheOptions, PayloadCache payloadCache) { new URL(url).toURI(); - if (linkedBlockingQueueCapacity != null && (linkedBlockingQueueCapacity < 1 || linkedBlockingQueueCapacity > 1000)) { + if (linkedBlockingQueueCapacity != null && + (linkedBlockingQueueCapacity < 1 || linkedBlockingQueueCapacity > 1000)) { throw new IllegalArgumentException("linkedBlockingQueueCapacity must be between 1 and 1000"); } if (scheduledThreadPoolSize != null && (scheduledThreadPoolSize < 1 || scheduledThreadPoolSize > 10)) { @@ -110,7 +117,7 @@ private void validate(String url, Integer pollIntervalSeconds, Integer linkedBlo if (proxyPort != null && (proxyPort < 1 || proxyPort > 65535)) { throw new IllegalArgumentException("proxyPort must be between 1 and 65535"); } - if (proxyHost != null && proxyPort == null ) { + if (proxyHost != null && proxyPort == null) { throw new IllegalArgumentException("proxyPort must be set if proxyHost is set"); } else if (proxyHost == null && proxyPort != null) { throw new IllegalArgumentException("proxyHost must be set if proxyPort is set"); diff --git a/tools/flagd-http-connector/src/main/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/PayloadCache.java b/tools/flagd-http-connector/src/main/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/PayloadCache.java index da67b23e6..25149a938 100644 --- a/tools/flagd-http-connector/src/main/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/PayloadCache.java +++ b/tools/flagd-http-connector/src/main/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/PayloadCache.java @@ -1,6 +1,10 @@ package dev.openfeature.contrib.tools.flagd.resolver.process.storage.connector.sync.http; +/** + * Interface for a simple payload cache. + */ public interface PayloadCache { + void put(String payload); String get(); } diff --git a/tools/flagd-http-connector/src/main/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/PayloadCacheOptions.java b/tools/flagd-http-connector/src/main/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/PayloadCacheOptions.java index 9ca0dabcc..cc5edd15c 100644 --- a/tools/flagd-http-connector/src/main/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/PayloadCacheOptions.java +++ b/tools/flagd-http-connector/src/main/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/PayloadCacheOptions.java @@ -5,12 +5,12 @@ /** * Represents configuration options for caching payloads. - *

- * This class provides options to configure the caching behavior, + * + *

This class provides options to configure the caching behavior, * specifically the interval at which the cache should be updated. *

- *

- * The default update interval is set to 30 minutes. + * + *

The default update interval is set to 30 minutes. * Change it typically to a value according to cache ttl and tradeoff with not updating it too much for * corner cases. *

diff --git a/tools/flagd-http-connector/src/main/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/PayloadCacheWrapper.java b/tools/flagd-http-connector/src/main/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/PayloadCacheWrapper.java index be213403e..080493539 100644 --- a/tools/flagd-http-connector/src/main/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/PayloadCacheWrapper.java +++ b/tools/flagd-http-connector/src/main/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/PayloadCacheWrapper.java @@ -20,6 +20,11 @@ public class PayloadCacheWrapper { private long updateIntervalMs; private PayloadCache payloadCache; + /** + * Constructor for PayloadCacheWrapper. + * @param payloadCache the payload cache to be used + * @param payloadCacheOptions the options for configuring the cache + */ @Builder public PayloadCacheWrapper(PayloadCache payloadCache, PayloadCacheOptions payloadCacheOptions) { if (payloadCacheOptions.getUpdateIntervalSeconds() < 1) { @@ -29,6 +34,10 @@ public PayloadCacheWrapper(PayloadCache payloadCache, PayloadCacheOptions payloa this.payloadCache = payloadCache; } + /** + * Updates the payload in the cache if the specified update interval has passed + * @param payload the payload to be cached + */ public void updatePayloadIfNeeded(String payload) { if ((getCurrentTimeMillis() - lastUpdateTimeMs) < updateIntervalMs) { log.debug("not updating payload, updateIntervalMs not reached"); @@ -48,6 +57,10 @@ protected long getCurrentTimeMillis() { return System.currentTimeMillis(); } + /** + * Retrieves the cached payload. + * @return the cached payload + */ public String get() { try { return payloadCache.get(); diff --git a/tools/flagd-http-connector/src/main/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/util/ConcurrentUtils.java b/tools/flagd-http-connector/src/main/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/util/ConcurrentUtils.java index e7ccbc3b9..29aefd46f 100644 --- a/tools/flagd-http-connector/src/main/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/util/ConcurrentUtils.java +++ b/tools/flagd-http-connector/src/main/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/util/ConcurrentUtils.java @@ -1,10 +1,10 @@ package dev.openfeature.contrib.tools.flagd.resolver.process.storage.connector.sync.http.util; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.TimeUnit; import lombok.AccessLevel; import lombok.NoArgsConstructor; import lombok.extern.slf4j.Slf4j; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.TimeUnit; /** * Concurrent / Concurrency utilities. From 1a48989d7520cc0f902dc8aa7af190962d5bd282 Mon Sep 17 00:00:00 2001 From: liran2000 Date: Sun, 27 Apr 2025 16:00:33 +0300 Subject: [PATCH 16/40] updates Signed-off-by: liran2000 --- .../process/storage/connector/sync/http/HttpConnector.java | 1 - 1 file changed, 1 deletion(-) diff --git a/tools/flagd-http-connector/src/main/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/HttpConnector.java b/tools/flagd-http-connector/src/main/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/HttpConnector.java index baeb68acf..262bf872c 100644 --- a/tools/flagd-http-connector/src/main/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/HttpConnector.java +++ b/tools/flagd-http-connector/src/main/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/HttpConnector.java @@ -33,7 +33,6 @@ * It uses a ScheduledExecutorService to schedule polling tasks and an ExecutorService for HTTP client execution. * The class also provides methods to initialize, retrieve the stream queue, and shutdown the connector gracefully. * It supports optional fail-safe initialization via cache. - *

* See readme - Http Connector section. */ @Slf4j From 273f2fcc7eb43526ff91c7c534356c5f4fb047c0 Mon Sep 17 00:00:00 2001 From: liran2000 Date: Sun, 27 Apr 2025 16:15:07 +0300 Subject: [PATCH 17/40] updates Signed-off-by: liran2000 --- .../process/storage/connector/sync/http/HttpCacheFetcher.java | 1 + .../process/storage/connector/sync/http/HttpConnector.java | 1 + .../storage/connector/sync/http/HttpConnectorOptions.java | 4 ++-- .../process/storage/connector/sync/http/PayloadCache.java | 1 + .../storage/connector/sync/http/PayloadCacheOptions.java | 2 +- .../storage/connector/sync/http/PayloadCacheWrapper.java | 3 +++ 6 files changed, 9 insertions(+), 3 deletions(-) diff --git a/tools/flagd-http-connector/src/main/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/HttpCacheFetcher.java b/tools/flagd-http-connector/src/main/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/HttpCacheFetcher.java index fbf158ebf..1096f0365 100644 --- a/tools/flagd-http-connector/src/main/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/HttpCacheFetcher.java +++ b/tools/flagd-http-connector/src/main/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/HttpCacheFetcher.java @@ -21,6 +21,7 @@ public class HttpCacheFetcher { /** * Fetches content from the given HTTP endpoint using the provided HttpClient and HttpRequest.Builder. + * * @param httpClient the HttpClient to use for the request * @param httpRequestBuilder the HttpRequest.Builder to build the request * @return the HttpResponse containing the response body as a String diff --git a/tools/flagd-http-connector/src/main/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/HttpConnector.java b/tools/flagd-http-connector/src/main/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/HttpConnector.java index 262bf872c..836dcf930 100644 --- a/tools/flagd-http-connector/src/main/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/HttpConnector.java +++ b/tools/flagd-http-connector/src/main/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/HttpConnector.java @@ -54,6 +54,7 @@ public class HttpConnector implements QueueSource { /** * HttpConnector constructor. + * * @param httpConnectorOptions options for configuring the HttpConnector. */ @Builder diff --git a/tools/flagd-http-connector/src/main/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/HttpConnectorOptions.java b/tools/flagd-http-connector/src/main/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/HttpConnectorOptions.java index a3494e724..87a6efacb 100644 --- a/tools/flagd-http-connector/src/main/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/HttpConnectorOptions.java +++ b/tools/flagd-http-connector/src/main/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/HttpConnectorOptions.java @@ -98,8 +98,8 @@ private void validate(String url, Integer pollIntervalSeconds, Integer linkedBlo String proxyHost, Integer proxyPort, PayloadCacheOptions payloadCacheOptions, PayloadCache payloadCache) { new URL(url).toURI(); - if (linkedBlockingQueueCapacity != null && - (linkedBlockingQueueCapacity < 1 || linkedBlockingQueueCapacity > 1000)) { + if (linkedBlockingQueueCapacity != null + && (linkedBlockingQueueCapacity < 1 || linkedBlockingQueueCapacity > 1000)) { throw new IllegalArgumentException("linkedBlockingQueueCapacity must be between 1 and 1000"); } if (scheduledThreadPoolSize != null && (scheduledThreadPoolSize < 1 || scheduledThreadPoolSize > 10)) { diff --git a/tools/flagd-http-connector/src/main/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/PayloadCache.java b/tools/flagd-http-connector/src/main/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/PayloadCache.java index 25149a938..6aed35754 100644 --- a/tools/flagd-http-connector/src/main/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/PayloadCache.java +++ b/tools/flagd-http-connector/src/main/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/PayloadCache.java @@ -6,5 +6,6 @@ public interface PayloadCache { void put(String payload); + String get(); } diff --git a/tools/flagd-http-connector/src/main/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/PayloadCacheOptions.java b/tools/flagd-http-connector/src/main/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/PayloadCacheOptions.java index cc5edd15c..4431b5ef5 100644 --- a/tools/flagd-http-connector/src/main/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/PayloadCacheOptions.java +++ b/tools/flagd-http-connector/src/main/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/PayloadCacheOptions.java @@ -10,7 +10,7 @@ * specifically the interval at which the cache should be updated. *

* - *

The default update interval is set to 30 minutes. + *

The default update interval is set to 30 minutes. * Change it typically to a value according to cache ttl and tradeoff with not updating it too much for * corner cases. *

diff --git a/tools/flagd-http-connector/src/main/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/PayloadCacheWrapper.java b/tools/flagd-http-connector/src/main/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/PayloadCacheWrapper.java index 080493539..ebaa53017 100644 --- a/tools/flagd-http-connector/src/main/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/PayloadCacheWrapper.java +++ b/tools/flagd-http-connector/src/main/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/PayloadCacheWrapper.java @@ -22,6 +22,7 @@ public class PayloadCacheWrapper { /** * Constructor for PayloadCacheWrapper. + * * @param payloadCache the payload cache to be used * @param payloadCacheOptions the options for configuring the cache */ @@ -36,6 +37,7 @@ public PayloadCacheWrapper(PayloadCache payloadCache, PayloadCacheOptions payloa /** * Updates the payload in the cache if the specified update interval has passed + * * @param payload the payload to be cached */ public void updatePayloadIfNeeded(String payload) { @@ -59,6 +61,7 @@ protected long getCurrentTimeMillis() { /** * Retrieves the cached payload. + * * @return the cached payload */ public String get() { From 8fac5956372b299c419335e2173967eb8d5076d6 Mon Sep 17 00:00:00 2001 From: liran2000 Date: Sun, 27 Apr 2025 16:22:42 +0300 Subject: [PATCH 18/40] updates Signed-off-by: liran2000 --- .../storage/connector/sync/http/PayloadCacheWrapper.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/flagd-http-connector/src/main/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/PayloadCacheWrapper.java b/tools/flagd-http-connector/src/main/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/PayloadCacheWrapper.java index ebaa53017..3e2e20cdc 100644 --- a/tools/flagd-http-connector/src/main/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/PayloadCacheWrapper.java +++ b/tools/flagd-http-connector/src/main/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/PayloadCacheWrapper.java @@ -36,7 +36,7 @@ public PayloadCacheWrapper(PayloadCache payloadCache, PayloadCacheOptions payloa } /** - * Updates the payload in the cache if the specified update interval has passed + * Updates the payload in the cache if the specified update interval has passed. * * @param payload the payload to be cached */ From f2b70ce35331856fc1926716bbbfe55f1f5a2919 Mon Sep 17 00:00:00 2001 From: liran2000 Date: Sat, 3 May 2025 19:51:22 +0300 Subject: [PATCH 19/40] add polling cache support Signed-off-by: liran2000 --- tools/flagd-http-connector/README.md | 40 ++++++++--- ...adCacheWrapper.java => FailSafeCache.java} | 12 ++-- .../connector/sync/http/HttpConnector.java | 41 ++++++++--- .../sync/http/HttpConnectorOptions.java | 39 +++++++++- .../connector/sync/http/PayloadCache.java | 15 +++- ...rapperTest.java => FailSafeCacheTest.java} | 64 +++++++++-------- .../sync/http/HttpConnectorOptionsTest.java | 16 ++--- .../sync/http/HttpConnectorTest.java | 72 +++++++++++++++++-- 8 files changed, 224 insertions(+), 75 deletions(-) rename tools/flagd-http-connector/src/main/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/{PayloadCacheWrapper.java => FailSafeCache.java} (84%) rename tools/flagd-http-connector/src/test/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/{PayloadCacheWrapperTest.java => FailSafeCacheTest.java} (80%) diff --git a/tools/flagd-http-connector/README.md b/tools/flagd-http-connector/README.md index 39a3f3336..18a19576a 100644 --- a/tools/flagd-http-connector/README.md +++ b/tools/flagd-http-connector/README.md @@ -15,6 +15,9 @@ The implementation is using Java HttpClient. ## Use cases and benefits * Reduce infrastructure/devops work, without additional containers needed. +* Reduce latency, since the data is fetched in-process. +* Reduce external network traffic from the Http source even without a flagd separate container / proxy when + polling cache is used. * Use as an additional provider for fallback / internal backup service via multi-provider. ### What happens if the Http source is down when application is starting ? @@ -24,25 +27,43 @@ source downtime window, initial payload is taken from cache to avoid starting w the source is back up. Therefore, the cache ttl expected to be higher than the expected source down-time to recover from during initialization. +### Polling cache +The polling cache is used to store the payload fetched from the URL. +Used when usePollingCache is configured as true. +A key advantage of this cache is that it enables a single microservice within a cluster to handle the polling of a +URL, effectively acting as a flagd/proxy while all other services leverage the shared cache. +This approach optimizes resource usage by preventing redundant polling across services. + ### Sample flow Sample flow can use: - Github as the flags payload source. -- Redis cache as a fail-safe initialization cache. +- Redis cache as a fail-safe initialization cache and as a polling cache. Sample flow of initialization during Github down-time window, showing that application can still use flags values as fetched from cache. ```mermaid sequenceDiagram - participant Provider - participant Github + box Cluster + participant micro-service-1 + participant micro-service-2 + participant micro-service-3 participant Redis + end + participant Github break source downtime - Provider->>Github: initialize - Github->>Provider: failure + micro-service-1->>Github: initialize + Github->>micro-service-1: failure end - Provider->>Redis: fetch - Redis->>Provider: last payload + micro-service-1->>Redis: fetch + Redis->>micro-service-1: failsafe payload + Note right of micro-service-1: polling interval passed + micro-service-1->>Github: fetch + Github->>micro-service-1: payload + micro-service-2->>Redis: fetch + Redis->>micro-service-2: payload + micro-service-3->>Redis: fetch + Redis->>micro-service-3: payload ``` @@ -97,7 +118,6 @@ The Http Connector can be configured using the following properties in the `Http | payloadCacheOptions | PayloadCacheOptions | Options for configuring the payload cache. Default is null. | | payloadCache | PayloadCache | The payload cache to use for caching responses. Default is null. | | useHttpCache | Boolean | Whether to use HTTP caching for the requests. Default is false. | +| useFailsafeCache | Boolean | Whether to use a failsafe cache for initialization. Default is false. | +| usePollingCache | Boolean | Whether to use a polling cache for initialization. Default is false. | | PayloadCacheOptions.updateIntervalSeconds | Integer | The interval, in seconds, at which the cache is updated. By default, this is set to 30 minutes. The goal is to avoid overloading fallback cache writes, since the cache serves only as a fallback mechanism. Typically, this value can be tuned to be shorter than the cache's TTL, balancing the need to minimize unnecessary updates while still handling edge cases effectively. | - - - diff --git a/tools/flagd-http-connector/src/main/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/PayloadCacheWrapper.java b/tools/flagd-http-connector/src/main/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/FailSafeCache.java similarity index 84% rename from tools/flagd-http-connector/src/main/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/PayloadCacheWrapper.java rename to tools/flagd-http-connector/src/main/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/FailSafeCache.java index 3e2e20cdc..f03aab882 100644 --- a/tools/flagd-http-connector/src/main/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/PayloadCacheWrapper.java +++ b/tools/flagd-http-connector/src/main/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/FailSafeCache.java @@ -15,19 +15,21 @@ * conditionally update the cache and {@link #get()} to retrieve the cached payload.

*/ @Slf4j -public class PayloadCacheWrapper { +public class FailSafeCache { + public static final String FAILSAFE_PAYLOAD_CACHE_KEY = FailSafeCache.class.getSimpleName() + + ".failsafe-payload"; private long lastUpdateTimeMs; private long updateIntervalMs; private PayloadCache payloadCache; /** - * Constructor for PayloadCacheWrapper. + * Constructor for FailSafeCache. * * @param payloadCache the payload cache to be used * @param payloadCacheOptions the options for configuring the cache */ @Builder - public PayloadCacheWrapper(PayloadCache payloadCache, PayloadCacheOptions payloadCacheOptions) { + public FailSafeCache(PayloadCache payloadCache, PayloadCacheOptions payloadCacheOptions) { if (payloadCacheOptions.getUpdateIntervalSeconds() < 1) { throw new IllegalArgumentException("pollIntervalSeconds must be larger than 0"); } @@ -48,7 +50,7 @@ public void updatePayloadIfNeeded(String payload) { try { log.debug("updating payload"); - payloadCache.put(payload); + payloadCache.put(FAILSAFE_PAYLOAD_CACHE_KEY, payload); lastUpdateTimeMs = getCurrentTimeMillis(); } catch (Exception e) { log.error("failed updating cache", e); @@ -66,7 +68,7 @@ protected long getCurrentTimeMillis() { */ public String get() { try { - return payloadCache.get(); + return payloadCache.get(FAILSAFE_PAYLOAD_CACHE_KEY); } catch (Exception e) { log.error("failed getting from cache", e); return null; diff --git a/tools/flagd-http-connector/src/main/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/HttpConnector.java b/tools/flagd-http-connector/src/main/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/HttpConnector.java index 836dcf930..77ffed52d 100644 --- a/tools/flagd-http-connector/src/main/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/HttpConnector.java +++ b/tools/flagd-http-connector/src/main/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/HttpConnector.java @@ -38,6 +38,7 @@ @Slf4j public class HttpConnector implements QueueSource { + public static final String POLLING_PAYLOAD_CACHE_KEY = HttpConnector.class.getSimpleName() + ".polling-payload"; private Integer pollIntervalSeconds; private Integer requestTimeoutSeconds; private BlockingQueue queue; @@ -45,9 +46,11 @@ public class HttpConnector implements QueueSource { private ExecutorService httpClientExecutor; private ScheduledExecutorService scheduler; private Map headers; - private PayloadCacheWrapper payloadCacheWrapper; + private FailSafeCache failSafeCache; private PayloadCache payloadCache; private HttpCacheFetcher httpCacheFetcher; + private int payloadCachePollTtlSeconds; + private boolean usePollingCache; @NonNull private String url; @@ -77,8 +80,8 @@ public HttpConnector(HttpConnectorOptions httpConnectorOptions) { .build(); this.queue = new LinkedBlockingQueue<>(httpConnectorOptions.getLinkedBlockingQueueCapacity()); this.payloadCache = httpConnectorOptions.getPayloadCache(); - if (payloadCache != null) { - this.payloadCacheWrapper = PayloadCacheWrapper.builder() + if (payloadCache != null && Boolean.TRUE.equals(httpConnectorOptions.getUseFailsafeCache())) { + this.failSafeCache = FailSafeCache.builder() .payloadCache(payloadCache) .payloadCacheOptions(httpConnectorOptions.getPayloadCacheOptions()) .build(); @@ -86,6 +89,8 @@ public HttpConnector(HttpConnectorOptions httpConnectorOptions) { if (Boolean.TRUE.equals(httpConnectorOptions.getUseHttpCache())) { httpCacheFetcher = new HttpCacheFetcher(); } + payloadCachePollTtlSeconds = pollIntervalSeconds; // safety margin + this.usePollingCache = Boolean.TRUE.equals(httpConnectorOptions.getUsePollingCache()); } @Override @@ -98,8 +103,8 @@ public BlockingQueue getStreamQueue() { boolean success = fetchAndUpdate(); if (!success) { log.info("failed initial fetch"); - if (payloadCache != null) { - updateFromCache(); + if (failSafeCache != null) { + updateFromFailsafeCache(); } } Runnable pollTask = buildPollTask(); @@ -107,9 +112,9 @@ public BlockingQueue getStreamQueue() { return queue; } - private void updateFromCache() { + private void updateFromFailsafeCache() { log.info("taking initial payload from cache to avoid starting with default values"); - String flagData = payloadCache.get(); + String flagData = failSafeCache.get(); if (flagData == null) { log.debug("got null from cache"); return; @@ -124,6 +129,14 @@ protected Runnable buildPollTask() { } private boolean fetchAndUpdate() { + if (payloadCache != null && usePollingCache) { + log.debug("checking cache for polling payload"); + String payload = payloadCache.get(POLLING_PAYLOAD_CACHE_KEY); + if (payload != null) { + log.debug("got payload from polling cache key, skipping update"); + return false; + } + } HttpRequest.Builder requestBuilder = HttpRequest.newBuilder() .uri(URI.create(url)) .timeout(Duration.ofSeconds(requestTimeoutSeconds)) @@ -159,10 +172,18 @@ private boolean fetchAndUpdate() { log.warn("Unable to offer file content to queue: queue is full"); return false; } - if (payloadCacheWrapper != null) { + if (payloadCache != null) { log.debug("scheduling cache update if needed"); - scheduler.execute(() -> - payloadCacheWrapper.updatePayloadIfNeeded(payload) + scheduler.execute(() -> { + if (failSafeCache != null) { + log.debug("updating payload in failsafe cache if needed"); + failSafeCache.updatePayloadIfNeeded(payload); + } + if (payloadCache != null) { + log.debug("updating polling payload in cache"); + payloadCache.put(POLLING_PAYLOAD_CACHE_KEY, payload, payloadCachePollTtlSeconds); + } + } ); } return true; diff --git a/tools/flagd-http-connector/src/main/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/HttpConnectorOptions.java b/tools/flagd-http-connector/src/main/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/HttpConnectorOptions.java index 87a6efacb..ea4637aa0 100644 --- a/tools/flagd-http-connector/src/main/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/HttpConnectorOptions.java +++ b/tools/flagd-http-connector/src/main/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/HttpConnectorOptions.java @@ -40,6 +40,10 @@ public class HttpConnectorOptions { private PayloadCache payloadCache; @Builder.Default private Boolean useHttpCache; + @Builder.Default + private Boolean useFailsafeCache; + @Builder.Default + private Boolean usePollingCache; @NonNull private String url; @@ -50,9 +54,11 @@ public class HttpConnectorOptions { public HttpConnectorOptions(Integer pollIntervalSeconds, Integer linkedBlockingQueueCapacity, Integer scheduledThreadPoolSize, Integer requestTimeoutSeconds, Integer connectTimeoutSeconds, String url, Map headers, ExecutorService httpClientExecutor, String proxyHost, Integer proxyPort, - PayloadCacheOptions payloadCacheOptions, PayloadCache payloadCache, Boolean useHttpCache) { + PayloadCacheOptions payloadCacheOptions, PayloadCache payloadCache, Boolean useHttpCache, + Boolean useFailsafeCache, Boolean usePollingCache) { validate(url, pollIntervalSeconds, linkedBlockingQueueCapacity, scheduledThreadPoolSize, requestTimeoutSeconds, - connectTimeoutSeconds, proxyHost, proxyPort, payloadCacheOptions, payloadCache); + connectTimeoutSeconds, proxyHost, proxyPort, payloadCacheOptions, payloadCache, useFailsafeCache, + usePollingCache); if (pollIntervalSeconds != null) { this.pollIntervalSeconds = pollIntervalSeconds; } @@ -90,13 +96,19 @@ public HttpConnectorOptions(Integer pollIntervalSeconds, Integer linkedBlockingQ if (useHttpCache != null) { this.useHttpCache = useHttpCache; } + if (useFailsafeCache != null) { + this.useFailsafeCache = useFailsafeCache; + } + if (usePollingCache != null) { + this.usePollingCache = usePollingCache; + } } @SneakyThrows private void validate(String url, Integer pollIntervalSeconds, Integer linkedBlockingQueueCapacity, Integer scheduledThreadPoolSize, Integer requestTimeoutSeconds, Integer connectTimeoutSeconds, String proxyHost, Integer proxyPort, PayloadCacheOptions payloadCacheOptions, - PayloadCache payloadCache) { + PayloadCache payloadCache, Boolean useFailsafeCache, Boolean usePollingCache) { new URL(url).toURI(); if (linkedBlockingQueueCapacity != null && (linkedBlockingQueueCapacity < 1 || linkedBlockingQueueCapacity > 1000)) { @@ -128,5 +140,26 @@ private void validate(String url, Integer pollIntervalSeconds, Integer linkedBlo if (payloadCache != null && payloadCacheOptions == null) { throw new IllegalArgumentException("payloadCacheOptions must be set if payloadCache is set"); } + if ((Boolean.TRUE.equals(useFailsafeCache) || Boolean.TRUE.equals(usePollingCache)) && payloadCache == null) { + throw new IllegalArgumentException( + "payloadCache must be set if useFailsafeCache or usePollingCache is set"); + } + + if (payloadCache != null && Boolean.TRUE.equals(usePollingCache)) { + + // verify payloadCache overrides put(String key, String payload, int ttlSeconds) + boolean overridesTtlPutMethod = false; + try { + var method = payloadCache.getClass().getMethod("put", String.class, String.class, int.class); + // Check if the method is declared in the class and not inherited + overridesTtlPutMethod = method.getDeclaringClass() != PayloadCache.class; + } catch (NoSuchMethodException e) { + // Method does not exist + } + if (!overridesTtlPutMethod) { + throw new IllegalArgumentException("when usePollingCache is used, payloadCache must override " + + "put(String key, String payload, int ttlSeconds)"); + } + } } } diff --git a/tools/flagd-http-connector/src/main/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/PayloadCache.java b/tools/flagd-http-connector/src/main/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/PayloadCache.java index 6aed35754..af9280c08 100644 --- a/tools/flagd-http-connector/src/main/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/PayloadCache.java +++ b/tools/flagd-http-connector/src/main/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/PayloadCache.java @@ -5,7 +5,18 @@ */ public interface PayloadCache { - void put(String payload); + void put(String key, String payload); - String get(); + String get(String key); + + /** + * Put a payload into the cache with a time-to-live (TTL) value. + * Must implement if HttpConnectorOptions.usePollingCache is true. + * @param key + * @param payload + * @param ttlSeconds + */ + default void put(String key, String payload, int ttlSeconds) { + throw new UnsupportedOperationException("put with ttl not supported"); + } } diff --git a/tools/flagd-http-connector/src/test/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/PayloadCacheWrapperTest.java b/tools/flagd-http-connector/src/test/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/FailSafeCacheTest.java similarity index 80% rename from tools/flagd-http-connector/src/test/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/PayloadCacheWrapperTest.java rename to tools/flagd-http-connector/src/test/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/FailSafeCacheTest.java index 3ff3ff679..08ee6c1c3 100644 --- a/tools/flagd-http-connector/src/test/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/PayloadCacheWrapperTest.java +++ b/tools/flagd-http-connector/src/test/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/FailSafeCacheTest.java @@ -6,6 +6,8 @@ import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.mock; @@ -20,7 +22,7 @@ import org.junit.jupiter.api.Test; -public class PayloadCacheWrapperTest { +public class FailSafeCacheTest { @Test public void testConstructorInitializesWithValidParameters() { @@ -29,7 +31,7 @@ public void testConstructorInitializesWithValidParameters() { .updateIntervalSeconds(600) .build(); - PayloadCacheWrapper wrapper = PayloadCacheWrapper.builder() + FailSafeCache wrapper = FailSafeCache.builder() .payloadCache(mockCache) .payloadCacheOptions(options) .build(); @@ -40,8 +42,8 @@ public void testConstructorInitializesWithValidParameters() { wrapper.updatePayloadIfNeeded(testPayload); wrapper.get(); - verify(mockCache).put(testPayload); - verify(mockCache).get(); + verify(mockCache).put(any(), eq(testPayload)); + verify(mockCache).get(any()); } @Test @@ -51,7 +53,7 @@ public void testConstructorThrowsExceptionForInvalidInterval() { .updateIntervalSeconds(0) .build(); - PayloadCacheWrapper.PayloadCacheWrapperBuilder payloadCacheWrapperBuilder = PayloadCacheWrapper.builder() + FailSafeCache.FailSafeCacheBuilder payloadCacheWrapperBuilder = FailSafeCache.builder() .payloadCache(mockCache) .payloadCacheOptions(options); IllegalArgumentException exception = assertThrows( @@ -68,7 +70,7 @@ public void testUpdateSkipsWhenIntervalNotPassed() { PayloadCacheOptions options = PayloadCacheOptions.builder() .updateIntervalSeconds(600) .build(); - PayloadCacheWrapper wrapper = PayloadCacheWrapper.builder() + FailSafeCache wrapper = FailSafeCache.builder() .payloadCache(mockCache) .payloadCacheOptions(options) .build(); @@ -79,8 +81,8 @@ public void testUpdateSkipsWhenIntervalNotPassed() { String newPayload = "new-payload"; wrapper.updatePayloadIfNeeded(newPayload); - verify(mockCache, times(1)).put(initialPayload); - verify(mockCache, never()).put(newPayload); + verify(mockCache, times(1)).put(any(), eq(initialPayload)); + verify(mockCache, never()).put(any(), eq(newPayload)); } @Test @@ -89,17 +91,17 @@ public void testUpdatePayloadIfNeededHandlesPutException() { PayloadCacheOptions options = PayloadCacheOptions.builder() .updateIntervalSeconds(600) .build(); - PayloadCacheWrapper wrapper = PayloadCacheWrapper.builder() + FailSafeCache wrapper = FailSafeCache.builder() .payloadCache(mockCache) .payloadCacheOptions(options) .build(); String testPayload = "test-payload"; - doThrow(new RuntimeException("put exception")).when(mockCache).put(testPayload); + doThrow(new RuntimeException("put exception")).when(mockCache).put(any(), eq(testPayload)); wrapper.updatePayloadIfNeeded(testPayload); - verify(mockCache).put(testPayload); + verify(mockCache).put(any(), eq(testPayload)); } @Test @@ -108,7 +110,7 @@ public void testUpdatePayloadIfNeededUpdatesCacheAfterInterval() { PayloadCacheOptions options = PayloadCacheOptions.builder() .updateIntervalSeconds(1) // 1 second interval for quick test .build(); - PayloadCacheWrapper wrapper = PayloadCacheWrapper.builder() + FailSafeCache wrapper = FailSafeCache.builder() .payloadCache(mockCache) .payloadCacheOptions(options) .build(); @@ -120,8 +122,8 @@ public void testUpdatePayloadIfNeededUpdatesCacheAfterInterval() { delay(1100); wrapper.updatePayloadIfNeeded(newPayload); - verify(mockCache).put(initialPayload); - verify(mockCache).put(newPayload); + verify(mockCache).put(any(), eq(initialPayload)); + verify(mockCache).put(any(), eq(newPayload)); } @Test @@ -130,18 +132,18 @@ public void testGetReturnsNullWhenCacheGetThrowsException() { PayloadCacheOptions options = PayloadCacheOptions.builder() .updateIntervalSeconds(600) .build(); - PayloadCacheWrapper wrapper = PayloadCacheWrapper.builder() + FailSafeCache wrapper = FailSafeCache.builder() .payloadCache(mockCache) .payloadCacheOptions(options) .build(); - when(mockCache.get()).thenThrow(new RuntimeException("Cache get failed")); + when(mockCache.get(any())).thenThrow(new RuntimeException("Cache get failed")); String result = wrapper.get(); assertNull(result); - verify(mockCache).get(); + verify(mockCache).get(any()); } @Test @@ -150,18 +152,18 @@ public void test_get_returns_cached_payload() { PayloadCacheOptions options = PayloadCacheOptions.builder() .updateIntervalSeconds(600) .build(); - PayloadCacheWrapper wrapper = PayloadCacheWrapper.builder() + FailSafeCache wrapper = FailSafeCache.builder() .payloadCache(mockCache) .payloadCacheOptions(options) .build(); String expectedPayload = "cached-payload"; - when(mockCache.get()).thenReturn(expectedPayload); + when(mockCache.get(any())).thenReturn(expectedPayload); String actualPayload = wrapper.get(); assertEquals(expectedPayload, actualPayload); - verify(mockCache).get(); + verify(mockCache).get(any()); } @Test @@ -170,7 +172,7 @@ public void test_first_call_updates_cache() { PayloadCacheOptions options = PayloadCacheOptions.builder() .updateIntervalSeconds(600) .build(); - PayloadCacheWrapper wrapper = PayloadCacheWrapper.builder() + FailSafeCache wrapper = FailSafeCache.builder() .payloadCache(mockCache) .payloadCacheOptions(options) .build(); @@ -179,7 +181,7 @@ public void test_first_call_updates_cache() { wrapper.updatePayloadIfNeeded(testPayload); - verify(mockCache).put(testPayload); + verify(mockCache).put(any(), eq(testPayload)); } @Test @@ -188,7 +190,7 @@ public void test_update_payload_once_within_interval() { PayloadCacheOptions options = PayloadCacheOptions.builder() .updateIntervalSeconds(1) // 1 second interval .build(); - PayloadCacheWrapper wrapper = PayloadCacheWrapper.builder() + FailSafeCache wrapper = FailSafeCache.builder() .payloadCache(mockCache) .payloadCacheOptions(options) .build(); @@ -198,7 +200,7 @@ public void test_update_payload_once_within_interval() { wrapper.updatePayloadIfNeeded(testPayload); wrapper.updatePayloadIfNeeded(testPayload); - verify(mockCache, times(1)).put(testPayload); + verify(mockCache, times(1)).put(any(), eq(testPayload)); } @SneakyThrows @@ -208,7 +210,7 @@ public void test_last_update_time_ms_updated_after_successful_cache_update() { PayloadCacheOptions options = PayloadCacheOptions.builder() .updateIntervalSeconds(600) .build(); - PayloadCacheWrapper wrapper = PayloadCacheWrapper.builder() + FailSafeCache wrapper = FailSafeCache.builder() .payloadCache(mockCache) .payloadCacheOptions(options) .build(); @@ -216,9 +218,9 @@ public void test_last_update_time_ms_updated_after_successful_cache_update() { wrapper.updatePayloadIfNeeded(testPayload); - verify(mockCache).put(testPayload); + verify(mockCache).put(any(), eq(testPayload)); - Field lastUpdateTimeMsField = PayloadCacheWrapper.class.getDeclaredField("lastUpdateTimeMs"); + Field lastUpdateTimeMsField = FailSafeCache.class.getDeclaredField("lastUpdateTimeMs"); lastUpdateTimeMsField.setAccessible(true); long lastUpdateTimeMs = (Long) lastUpdateTimeMsField.get(wrapper); @@ -232,7 +234,7 @@ public void test_update_payload_if_needed_respects_update_interval() { PayloadCacheOptions options = PayloadCacheOptions.builder() .updateIntervalSeconds(600) .build(); - PayloadCacheWrapper wrapper = spy(PayloadCacheWrapper.builder() + FailSafeCache wrapper = spy(FailSafeCache.builder() .payloadCache(mockCache) .payloadCacheOptions(options) .build()); @@ -247,21 +249,21 @@ public void test_update_payload_if_needed_respects_update_interval() { wrapper.updatePayloadIfNeeded(testPayload); // Verify the payload was updated - verify(mockCache).put(testPayload); + verify(mockCache).put(any(), eq(testPayload)); // Attempt to update before interval has passed doReturn(initialTime + updateIntervalMs - 1).when(wrapper).getCurrentTimeMillis(); wrapper.updatePayloadIfNeeded(testPayload); // Verify the payload was not updated again - verify(mockCache, times(1)).put(testPayload); + verify(mockCache, times(1)).put(any(), eq(testPayload)); // Update after interval has passed doReturn(initialTime + updateIntervalMs + 1).when(wrapper).getCurrentTimeMillis(); wrapper.updatePayloadIfNeeded(testPayload); // Verify the payload was updated again - verify(mockCache, times(2)).put(testPayload); + verify(mockCache, times(2)).put(any(), eq(testPayload)); } } diff --git a/tools/flagd-http-connector/src/test/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/HttpConnectorOptionsTest.java b/tools/flagd-http-connector/src/test/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/HttpConnectorOptionsTest.java index 96bd81c4d..ffb5c8e89 100644 --- a/tools/flagd-http-connector/src/test/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/HttpConnectorOptionsTest.java +++ b/tools/flagd-http-connector/src/test/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/HttpConnectorOptionsTest.java @@ -103,12 +103,12 @@ public void testSettingPayloadCacheWithValidOptions() { private String payload; @Override - public void put(String payload) { + public void put(String key, String payload) { this.payload = payload; } @Override - public String get() { + public String get(String key) { return this.payload; } }; @@ -174,11 +174,11 @@ public void testAdditionalCustomValuesInitialization() { PayloadCacheOptions cacheOptions = PayloadCacheOptions.builder().build(); PayloadCache cache = new PayloadCache() { @Override - public void put(String payload) { + public void put(String key, String payload) { // do nothing } @Override - public String get() { return null; } + public String get(String key) { return null; } }; HttpConnectorOptions options = HttpConnectorOptions.builder() @@ -232,11 +232,11 @@ public void testBuilderInitializesAllFields() { PayloadCacheOptions cacheOptions = PayloadCacheOptions.builder().build(); PayloadCache cache = new PayloadCache() { @Override - public void put(String payload) { + public void put(String key, String payload) { // do nothing } @Override - public String get() { return null; } + public String get(String key) { return null; } }; HttpConnectorOptions options = HttpConnectorOptions.builder() @@ -372,12 +372,12 @@ public void testProxyHostWithoutProxyPort() { public void testSettingPayloadCacheWithoutOptions() { PayloadCache mockPayloadCache = new PayloadCache() { @Override - public void put(String payload) { + public void put(String key, String payload) { // Mock implementation } @Override - public String get() { + public String get(String key) { return "mockPayload"; } }; diff --git a/tools/flagd-http-connector/src/test/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/HttpConnectorTest.java b/tools/flagd-http-connector/src/test/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/HttpConnectorTest.java index 5298d98f6..94be758f6 100644 --- a/tools/flagd-http-connector/src/test/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/HttpConnectorTest.java +++ b/tools/flagd-http-connector/src/test/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/HttpConnectorTest.java @@ -9,6 +9,8 @@ import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import dev.openfeature.contrib.providers.flagd.Config; @@ -28,8 +30,6 @@ import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; -import dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.QueueSource; -import dev.openfeature.sdk.EvaluationContext; import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; import org.junit.jupiter.api.Test; @@ -86,12 +86,12 @@ void testBuildPollTaskFetchesDataAndAddsToQueue() { PayloadCache payloadCache = new PayloadCache() { private String payload; @Override - public void put(String payload) { + public void put(String key, String payload) { this.payload = payload; } @Override - public String get() { + public String get(String key) { return payload; } }; @@ -102,6 +102,7 @@ public String get() { .useHttpCache(true) .payloadCache(payloadCache) .payloadCacheOptions(PayloadCacheOptions.builder().build()) + .useFailsafeCache(true) .build(); HttpConnector connector = HttpConnector.builder() .httpConnectorOptions(httpConnectorOptions) @@ -194,12 +195,12 @@ void testInitFailureUsingCache() { final String cachedData = "cached data"; PayloadCache payloadCache = new PayloadCache() { @Override - public void put(String payload) { + public void put(String key, String payload) { // do nothing } @Override - public String get() { + public String get(String key) { return cachedData; } }; @@ -208,6 +209,7 @@ public String get() { .url(testUrl) .payloadCache(payloadCache) .payloadCacheOptions(PayloadCacheOptions.builder().build()) + .useFailsafeCache(true) .build(); HttpConnector connector = HttpConnector.builder() .httpConnectorOptions(httpConnectorOptions) @@ -410,6 +412,64 @@ void testQueuePayloadTypeSetToDataOnSuccess() { assertEquals("response body", payload.getFlagData()); } + @Test + public void testSecondFetchFromCache() throws Exception { + // Mock PayloadCache + PayloadCache payloadCache = new PayloadCache() { + private String payload; + @Override + public void put(String key, String payload) { + this.payload = payload; + } + + @Override + public void put(String key, String payload, int ttlSeconds) { + put(key, payload); + } + + @Override + public String get(String key) { + return payload; + } + }; + PayloadCache mockPayloadCache = spy(payloadCache); + when(mockPayloadCache.get(HttpConnector.POLLING_PAYLOAD_CACHE_KEY)) + .thenReturn(null) // First fetch: cache miss + .thenReturn("cached payload"); // Second fetch: cache hit + + // Configure HttpConnectorOptions with usePollingCache = true + HttpConnectorOptions options = HttpConnectorOptions.builder() + .url("http://example.com") + .pollIntervalSeconds(10) + .usePollingCache(true) + .payloadCache(mockPayloadCache) + .payloadCacheOptions(PayloadCacheOptions.builder().build()) + .build(); + + // Create HttpConnector instance + HttpConnector connector = HttpConnector.builder() + .httpConnectorOptions(options) + .build(); + + // Initialize the connector + BlockingQueue queue = connector.getStreamQueue(); + + // First fetch: verify cache was checked but not used + verify(mockPayloadCache, times(1)).get(HttpConnector.POLLING_PAYLOAD_CACHE_KEY); + + // Simulate second fetch + connector.buildPollTask().run(); + + // Second fetch: verify cache was used + verify(mockPayloadCache, times(2)).get(HttpConnector.POLLING_PAYLOAD_CACHE_KEY); + assertEquals(1, queue.size(), "Queue should contain one element after second fetch from cache"); + + assertFalse(queue.isEmpty(), "Queue should contain payload after second fetch from cache"); + QueuePayload payload = queue.poll(); + assertNotNull(payload); + assertEquals(QueuePayloadType.DATA, payload.getType()); + } + @Test public void providerTest() { HttpConnectorOptions httpConnectorOptions = HttpConnectorOptions.builder() From ea8ec7b753a5051d4a860737cf6b1c95c76d367a Mon Sep 17 00:00:00 2001 From: liran2000 Date: Sat, 3 May 2025 19:54:14 +0300 Subject: [PATCH 20/40] add polling cache support Signed-off-by: liran2000 --- tools/flagd-http-connector/README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tools/flagd-http-connector/README.md b/tools/flagd-http-connector/README.md index 18a19576a..e3e0738fc 100644 --- a/tools/flagd-http-connector/README.md +++ b/tools/flagd-http-connector/README.md @@ -34,13 +34,14 @@ A key advantage of this cache is that it enables a single microservice within a URL, effectively acting as a flagd/proxy while all other services leverage the shared cache. This approach optimizes resource usage by preventing redundant polling across services. -### Sample flow +### Sample flow demonstrating the architecture Sample flow can use: - Github as the flags payload source. - Redis cache as a fail-safe initialization cache and as a polling cache. Sample flow of initialization during Github down-time window, showing that application can still use flags -values as fetched from cache. +values as fetched from cache. +Multiple micro-services are using the same cache, and therefore only one of them is responsible for polling the URL. ```mermaid sequenceDiagram box Cluster From 9a723b987c2a8ecd050ea38d76252be6bd283b77 Mon Sep 17 00:00:00 2001 From: liran2000 Date: Sun, 4 May 2025 09:41:06 +0300 Subject: [PATCH 21/40] add polling cache support Signed-off-by: liran2000 --- .../connector/sync/http/HttpConnector.java | 29 +++++++++++++------ .../sync/http/HttpConnectorOptions.java | 3 +- 2 files changed, 22 insertions(+), 10 deletions(-) diff --git a/tools/flagd-http-connector/src/main/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/HttpConnector.java b/tools/flagd-http-connector/src/main/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/HttpConnector.java index 77ffed52d..82781bc7b 100644 --- a/tools/flagd-http-connector/src/main/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/HttpConnector.java +++ b/tools/flagd-http-connector/src/main/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/HttpConnector.java @@ -103,21 +103,32 @@ public BlockingQueue getStreamQueue() { boolean success = fetchAndUpdate(); if (!success) { log.info("failed initial fetch"); - if (failSafeCache != null) { - updateFromFailsafeCache(); - } + updateFromCache(); } Runnable pollTask = buildPollTask(); scheduler.scheduleWithFixedDelay(pollTask, pollIntervalSeconds, pollIntervalSeconds, TimeUnit.SECONDS); return queue; } - private void updateFromFailsafeCache() { + private void updateFromCache() { log.info("taking initial payload from cache to avoid starting with default values"); - String flagData = failSafeCache.get(); + String flagData = null; + if (payloadCache != null) { + flagData = payloadCache.get(POLLING_PAYLOAD_CACHE_KEY); + if (flagData != null) { + log.debug("got payload from polling cache key"); + } + } if (flagData == null) { - log.debug("got null from cache"); - return; + if (failSafeCache == null) { + log.debug("no failsafe cache, skipping"); + return; + } + flagData = failSafeCache.get(); + if (flagData == null) { + log.debug("could not get from failsafe cache"); + return; + } } if (!this.queue.offer(new QueuePayload(QueuePayloadType.DATA, flagData))) { log.warn("init: Unable to offer file content to queue: queue is full"); @@ -134,7 +145,7 @@ private boolean fetchAndUpdate() { String payload = payloadCache.get(POLLING_PAYLOAD_CACHE_KEY); if (payload != null) { log.debug("got payload from polling cache key, skipping update"); - return false; + return true; } } HttpRequest.Builder requestBuilder = HttpRequest.newBuilder() @@ -161,7 +172,7 @@ private boolean fetchAndUpdate() { return false; } else if (response.statusCode() == 304) { log.debug("got 304 Not Modified, skipping update"); - return false; + return true; } if (payload == null) { log.debug("payload is null"); diff --git a/tools/flagd-http-connector/src/main/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/HttpConnectorOptions.java b/tools/flagd-http-connector/src/main/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/HttpConnectorOptions.java index ea4637aa0..b6257994e 100644 --- a/tools/flagd-http-connector/src/main/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/HttpConnectorOptions.java +++ b/tools/flagd-http-connector/src/main/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/HttpConnectorOptions.java @@ -1,5 +1,6 @@ package dev.openfeature.contrib.tools.flagd.resolver.process.storage.connector.sync.http; +import java.lang.reflect.Method; import java.net.URL; import java.util.HashMap; import java.util.Map; @@ -150,7 +151,7 @@ private void validate(String url, Integer pollIntervalSeconds, Integer linkedBlo // verify payloadCache overrides put(String key, String payload, int ttlSeconds) boolean overridesTtlPutMethod = false; try { - var method = payloadCache.getClass().getMethod("put", String.class, String.class, int.class); + Method method = payloadCache.getClass().getMethod("put", String.class, String.class, int.class); // Check if the method is declared in the class and not inherited overridesTtlPutMethod = method.getDeclaringClass() != PayloadCache.class; } catch (NoSuchMethodException e) { From 153774571b54aa73f20bb4bf7e4ada880b250726 Mon Sep 17 00:00:00 2001 From: liran2000 Date: Sun, 4 May 2025 09:52:49 +0300 Subject: [PATCH 22/40] adjust test Signed-off-by: liran2000 --- .../connector/sync/http/HttpCacheFetcherTest.java | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/tools/flagd-http-connector/src/test/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/HttpCacheFetcherTest.java b/tools/flagd-http-connector/src/test/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/HttpCacheFetcherTest.java index fe49219ec..bac64ac73 100644 --- a/tools/flagd-http-connector/src/test/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/HttpCacheFetcherTest.java +++ b/tools/flagd-http-connector/src/test/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/HttpCacheFetcherTest.java @@ -21,7 +21,9 @@ import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.util.Arrays; +import java.util.Collections; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.Optional; import lombok.SneakyThrows; @@ -53,14 +55,13 @@ public void testResponseWith200ButNoCacheHeaders() throws Exception { HttpRequest.Builder requestBuilderMock = mock(HttpRequest.Builder.class); HttpRequest requestMock = mock(HttpRequest.class); HttpResponse responseMock = mock(HttpResponse.class); - HttpHeaders headersMock = mock(HttpHeaders.class); - + HttpHeaders headers = HttpHeaders.of( + Collections.emptyMap(), + (a, b) -> true); when(requestBuilderMock.build()).thenReturn(requestMock); when(httpClientMock.send(eq(requestMock), any(HttpResponse.BodyHandler.class))).thenReturn(responseMock); when(responseMock.statusCode()).thenReturn(200); - when(responseMock.headers()).thenReturn(headersMock); - when(headersMock.firstValue("ETag")).thenReturn(Optional.empty()); - when(headersMock.firstValue("Last-Modified")).thenReturn(Optional.empty()); + when(responseMock.headers()).thenReturn(headers); HttpCacheFetcher fetcher = new HttpCacheFetcher(); HttpResponse response = fetcher.fetchContent(httpClientMock, requestBuilderMock); From e5e49f47b4574fd70948dc88f70b0f56318e67da Mon Sep 17 00:00:00 2001 From: liran2000 Date: Sun, 4 May 2025 10:52:35 +0300 Subject: [PATCH 23/40] adjust test Signed-off-by: liran2000 --- tools/flagd-http-connector/checkstyle.xml | 435 ------------------ .../connector/sync/http/FailSafeCache.java | 13 +- .../connector/sync/http/HttpConnector.java | 5 + .../sync/http/HttpConnectorOptions.java | 9 +- .../sync/http/PayloadCacheOptions.java | 5 + 5 files changed, 29 insertions(+), 438 deletions(-) diff --git a/tools/flagd-http-connector/checkstyle.xml b/tools/flagd-http-connector/checkstyle.xml index f8ca27ad6..bd24b31e3 100644 --- a/tools/flagd-http-connector/checkstyle.xml +++ b/tools/flagd-http-connector/checkstyle.xml @@ -3,440 +3,5 @@ "-//Checkstyle//DTD Checkstyle Configuration 1.3//EN" "https://checkstyle.org/dtds/configuration_1_3.dtd"> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/tools/flagd-http-connector/src/main/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/FailSafeCache.java b/tools/flagd-http-connector/src/main/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/FailSafeCache.java index f03aab882..c8dc47a2f 100644 --- a/tools/flagd-http-connector/src/main/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/FailSafeCache.java +++ b/tools/flagd-http-connector/src/main/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/FailSafeCache.java @@ -1,5 +1,6 @@ package dev.openfeature.contrib.tools.flagd.resolver.process.storage.connector.sync.http; +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import lombok.Builder; import lombok.extern.slf4j.Slf4j; @@ -14,6 +15,10 @@ * the update interval, and then using {@link #updatePayloadIfNeeded(String)} to * conditionally update the cache and {@link #get()} to retrieve the cached payload.

*/ +@SuppressFBWarnings( + value = {"EI_EXPOSE_REP2", "CT_CONSTRUCTOR_THROW"}, + justification = "builder validations" +) @Slf4j public class FailSafeCache { public static final String FAILSAFE_PAYLOAD_CACHE_KEY = FailSafeCache.class.getSimpleName() @@ -30,11 +35,15 @@ public class FailSafeCache { */ @Builder public FailSafeCache(PayloadCache payloadCache, PayloadCacheOptions payloadCacheOptions) { + validate(payloadCacheOptions); + this.updateIntervalMs = payloadCacheOptions.getUpdateIntervalSeconds() * 1000L; + this.payloadCache = payloadCache; + } + + private static void validate(PayloadCacheOptions payloadCacheOptions) { if (payloadCacheOptions.getUpdateIntervalSeconds() < 1) { throw new IllegalArgumentException("pollIntervalSeconds must be larger than 0"); } - this.updateIntervalMs = payloadCacheOptions.getUpdateIntervalSeconds() * 1000L; - this.payloadCache = payloadCache; } /** diff --git a/tools/flagd-http-connector/src/main/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/HttpConnector.java b/tools/flagd-http-connector/src/main/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/HttpConnector.java index 82781bc7b..77cfda2c5 100644 --- a/tools/flagd-http-connector/src/main/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/HttpConnector.java +++ b/tools/flagd-http-connector/src/main/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/HttpConnector.java @@ -21,6 +21,7 @@ import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import lombok.Builder; import lombok.NonNull; import lombok.extern.slf4j.Slf4j; @@ -98,6 +99,10 @@ public void init() throws Exception { log.info("init Http Connector"); } + @SuppressFBWarnings( + value = "EI_EXPOSE_REP", + justification = "parent defines the interface" + ) @Override public BlockingQueue getStreamQueue() { boolean success = fetchAndUpdate(); diff --git a/tools/flagd-http-connector/src/main/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/HttpConnectorOptions.java b/tools/flagd-http-connector/src/main/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/HttpConnectorOptions.java index b6257994e..6a56955cd 100644 --- a/tools/flagd-http-connector/src/main/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/HttpConnectorOptions.java +++ b/tools/flagd-http-connector/src/main/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/HttpConnectorOptions.java @@ -6,14 +6,21 @@ import java.util.Map; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import lombok.Builder; import lombok.Getter; import lombok.NonNull; import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; /** * Represents configuration options for the HTTP connector. */ +@SuppressFBWarnings( + value = {"EI_EXPOSE_REP", "EI_EXPOSE_REP2", "CT_CONSTRUCTOR_THROW"}, + justification = "builder validations" +) +@Slf4j @Getter public class HttpConnectorOptions { @@ -155,7 +162,7 @@ private void validate(String url, Integer pollIntervalSeconds, Integer linkedBlo // Check if the method is declared in the class and not inherited overridesTtlPutMethod = method.getDeclaringClass() != PayloadCache.class; } catch (NoSuchMethodException e) { - // Method does not exist + log.debug("payloadCache does not override put(String key, String payload, int ttlSeconds)"); } if (!overridesTtlPutMethod) { throw new IllegalArgumentException("when usePollingCache is used, payloadCache must override " diff --git a/tools/flagd-http-connector/src/main/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/PayloadCacheOptions.java b/tools/flagd-http-connector/src/main/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/PayloadCacheOptions.java index 4431b5ef5..72dfc6730 100644 --- a/tools/flagd-http-connector/src/main/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/PayloadCacheOptions.java +++ b/tools/flagd-http-connector/src/main/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/PayloadCacheOptions.java @@ -1,5 +1,6 @@ package dev.openfeature.contrib.tools.flagd.resolver.process.storage.connector.sync.http; +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import lombok.Builder; import lombok.Getter; @@ -15,6 +16,10 @@ * corner cases. *

*/ +@SuppressFBWarnings( + value = {"EI_EXPOSE_REP", "EI_EXPOSE_REP2"}, + justification = "builder validations" +) @Builder @Getter public class PayloadCacheOptions { From f3570057c19d962d5caf37eaac561547de9ac609 Mon Sep 17 00:00:00 2001 From: liran2000 Date: Sun, 4 May 2025 11:02:47 +0300 Subject: [PATCH 24/40] adjust Signed-off-by: liran2000 --- tools/flagd-http-connector/pom.xml | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/tools/flagd-http-connector/pom.xml b/tools/flagd-http-connector/pom.xml index 0f979b14d..29f28f9d5 100644 --- a/tools/flagd-http-connector/pom.xml +++ b/tools/flagd-http-connector/pom.xml @@ -38,6 +38,36 @@ 3.17.0 + + + + + + + + + + + + com.google.code.findbugs + annotations + 3.0.1 + test + + + + + + + + + + + + + + + From a8fccd00a9571639e9ad4c8df7da5ee7feac3b74 Mon Sep 17 00:00:00 2001 From: liran2000 Date: Sun, 4 May 2025 11:10:25 +0300 Subject: [PATCH 25/40] adjust Signed-off-by: liran2000 --- tools/flagd-http-connector/checkstyle-suppressions.xml | 8 -------- tools/flagd-http-connector/checkstyle.xml | 7 ------- tools/flagd-http-connector/pom.xml | 2 +- 3 files changed, 1 insertion(+), 16 deletions(-) delete mode 100644 tools/flagd-http-connector/checkstyle-suppressions.xml delete mode 100644 tools/flagd-http-connector/checkstyle.xml diff --git a/tools/flagd-http-connector/checkstyle-suppressions.xml b/tools/flagd-http-connector/checkstyle-suppressions.xml deleted file mode 100644 index 3d374f555..000000000 --- a/tools/flagd-http-connector/checkstyle-suppressions.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/tools/flagd-http-connector/checkstyle.xml b/tools/flagd-http-connector/checkstyle.xml deleted file mode 100644 index bd24b31e3..000000000 --- a/tools/flagd-http-connector/checkstyle.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - diff --git a/tools/flagd-http-connector/pom.xml b/tools/flagd-http-connector/pom.xml index 29f28f9d5..fcdc0b58d 100644 --- a/tools/flagd-http-connector/pom.xml +++ b/tools/flagd-http-connector/pom.xml @@ -5,7 +5,7 @@ dev.openfeature.contrib parent - 0.1.0 + [0.2,) ../../pom.xml dev.openfeature.contrib.tools From 96fd59e4cc0a766ec4eca58e954e3e5a98d8a3f9 Mon Sep 17 00:00:00 2001 From: liran2000 Date: Sun, 4 May 2025 11:15:47 +0300 Subject: [PATCH 26/40] adjust Signed-off-by: liran2000 --- .../process/storage/connector/sync/http/PayloadCache.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tools/flagd-http-connector/src/main/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/PayloadCache.java b/tools/flagd-http-connector/src/main/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/PayloadCache.java index af9280c08..9307d8d2e 100644 --- a/tools/flagd-http-connector/src/main/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/PayloadCache.java +++ b/tools/flagd-http-connector/src/main/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/PayloadCache.java @@ -12,9 +12,9 @@ public interface PayloadCache { /** * Put a payload into the cache with a time-to-live (TTL) value. * Must implement if HttpConnectorOptions.usePollingCache is true. - * @param key - * @param payload - * @param ttlSeconds + * @param key cache key + * @param payload payload to cache + * @param ttlSeconds time-to-live in seconds */ default void put(String key, String payload, int ttlSeconds) { throw new UnsupportedOperationException("put with ttl not supported"); From 367147e9e8d7057fccdbbe656c4e3afc6d00820a Mon Sep 17 00:00:00 2001 From: liran2000 Date: Sun, 4 May 2025 11:26:04 +0300 Subject: [PATCH 27/40] adjust Signed-off-by: liran2000 --- .../process/storage/connector/sync/http/HttpConnector.java | 2 +- .../process/storage/connector/sync/http/PayloadCache.java | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/tools/flagd-http-connector/src/main/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/HttpConnector.java b/tools/flagd-http-connector/src/main/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/HttpConnector.java index 77cfda2c5..0fa44226c 100644 --- a/tools/flagd-http-connector/src/main/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/HttpConnector.java +++ b/tools/flagd-http-connector/src/main/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/HttpConnector.java @@ -6,6 +6,7 @@ import dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.QueuePayloadType; import dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.QueueSource; import dev.openfeature.contrib.tools.flagd.resolver.process.storage.connector.sync.http.util.ConcurrentUtils; +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import java.io.IOException; import java.net.InetSocketAddress; import java.net.ProxySelector; @@ -21,7 +22,6 @@ import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; -import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import lombok.Builder; import lombok.NonNull; import lombok.extern.slf4j.Slf4j; diff --git a/tools/flagd-http-connector/src/main/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/PayloadCache.java b/tools/flagd-http-connector/src/main/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/PayloadCache.java index 9307d8d2e..0643eede1 100644 --- a/tools/flagd-http-connector/src/main/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/PayloadCache.java +++ b/tools/flagd-http-connector/src/main/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/PayloadCache.java @@ -5,13 +5,14 @@ */ public interface PayloadCache { - void put(String key, String payload); - String get(String key); + void put(String key, String payload); + /** * Put a payload into the cache with a time-to-live (TTL) value. * Must implement if HttpConnectorOptions.usePollingCache is true. + * * @param key cache key * @param payload payload to cache * @param ttlSeconds time-to-live in seconds From 41502c16b43b33a00bcb8265c02b4296ca4c384b Mon Sep 17 00:00:00 2001 From: liran2000 Date: Sun, 4 May 2025 13:04:14 +0300 Subject: [PATCH 28/40] readme update Signed-off-by: liran2000 --- tools/flagd-http-connector/README.md | 36 +++++++++++++++------------- 1 file changed, 19 insertions(+), 17 deletions(-) diff --git a/tools/flagd-http-connector/README.md b/tools/flagd-http-connector/README.md index e3e0738fc..b153f398b 100644 --- a/tools/flagd-http-connector/README.md +++ b/tools/flagd-http-connector/README.md @@ -3,7 +3,7 @@ ## Introduction Http Connector is a tool for [flagd](https://github.com/open-feature/flagd) in-process resolver. -This mode performs flag evaluations locally (in-process). +This mode performs flag evaluations locally (in-process). Flag configurations for evaluation are obtained via Http. ## Http Connector functionality @@ -14,18 +14,20 @@ reducing traffic, reducing rate limits effects and changes updates. Can be enabl The implementation is using Java HttpClient. ## Use cases and benefits -* Reduce infrastructure/devops work, without additional containers needed. -* Reduce latency, since the data is fetched in-process. -* Reduce external network traffic from the Http source even without a flagd separate container / proxy when - polling cache is used. -* Use as an additional provider for fallback / internal backup service via multi-provider. +* flagd installation is not required, working independently. + Minimizing infrastructure and DevOps overhead - no extra containers required. +* Low latency by fetching data directly in-process. +* Decreased external network traffic from the HTTP source, even without a standalone flagd container or proxy, + when using polling cache. +* Can serve as an additional provider for fallback or internal backup scenarios using a multi-provider setup. -### What happens if the Http source is down when application is starting ? +### What happens if the Http source is down during application startup? -It supports optional fail-safe initialization via cache, such that on initial fetch error following by -source downtime window, initial payload is taken from cache to avoid starting with default values until -the source is back up. Therefore, the cache ttl expected to be higher than the expected source -down-time to recover from during initialization. +Http Connector supports optional fail-safe initialization using a cache. +If the initial fetch fails due to source unavailability, it can load the initial payload from the cache instead of +falling back to default values. +This ensures smoother startup behavior until the source becomes available again. To be effective, the fallback cache’s +TTL should be longer than the expected duration of the source downtime during initialization. ### Polling cache The polling cache is used to store the payload fetched from the URL. @@ -35,13 +37,13 @@ URL, effectively acting as a flagd/proxy while all other services leverage the s This approach optimizes resource usage by preventing redundant polling across services. ### Sample flow demonstrating the architecture -Sample flow can use: -- Github as the flags payload source. -- Redis cache as a fail-safe initialization cache and as a polling cache. +This example demonstrates an architectural flow using: +- GitHub as the source for flag payload. +- Redis serving as both the fail-safe initialization cache and the polling cache. -Sample flow of initialization during Github down-time window, showing that application can still use flags -values as fetched from cache. -Multiple micro-services are using the same cache, and therefore only one of them is responsible for polling the URL. +Example initialization flow during GitHub downtime, +demonstrates how the application continues to access flag values from the cache even when GitHub is unavailable. +In this setup, multiple microservices share the same cache, with only one service responsible for polling the source URL. ```mermaid sequenceDiagram box Cluster From 350cde9379c35b6f43063185316c7170e0c7e031 Mon Sep 17 00:00:00 2001 From: liran2000 Date: Mon, 5 May 2025 17:24:13 +0300 Subject: [PATCH 29/40] remove redundant lines Signed-off-by: liran2000 --- tools/flagd-http-connector/pom.xml | 24 ------------------------ 1 file changed, 24 deletions(-) diff --git a/tools/flagd-http-connector/pom.xml b/tools/flagd-http-connector/pom.xml index fcdc0b58d..74e91a5d4 100644 --- a/tools/flagd-http-connector/pom.xml +++ b/tools/flagd-http-connector/pom.xml @@ -38,16 +38,6 @@ 3.17.0 - - - - - - - - - - com.google.code.findbugs annotations @@ -55,19 +45,5 @@ test - - - - - - - - - - - - - - From f136dd1e32dada5c977e0c32803567f5fd83adac Mon Sep 17 00:00:00 2001 From: Liran M <77168114+liran2000@users.noreply.github.com> Date: Mon, 5 May 2025 17:24:45 +0300 Subject: [PATCH 30/40] Update tools/flagd-http-connector/README.md Co-authored-by: chrfwow Signed-off-by: Liran M <77168114+liran2000@users.noreply.github.com> --- tools/flagd-http-connector/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/flagd-http-connector/README.md b/tools/flagd-http-connector/README.md index b153f398b..ab3ebad6d 100644 --- a/tools/flagd-http-connector/README.md +++ b/tools/flagd-http-connector/README.md @@ -14,7 +14,7 @@ reducing traffic, reducing rate limits effects and changes updates. Can be enabl The implementation is using Java HttpClient. ## Use cases and benefits -* flagd installation is not required, working independently. +* flagd installation is not required, the Http Connector works independently. Minimizing infrastructure and DevOps overhead - no extra containers required. * Low latency by fetching data directly in-process. * Decreased external network traffic from the HTTP source, even without a standalone flagd container or proxy, From b9d525ac9bfbb9529148ea8630085c9f5d3338fa Mon Sep 17 00:00:00 2001 From: Liran M <77168114+liran2000@users.noreply.github.com> Date: Mon, 5 May 2025 17:24:54 +0300 Subject: [PATCH 31/40] Update tools/flagd-http-connector/README.md Co-authored-by: chrfwow Signed-off-by: Liran M <77168114+liran2000@users.noreply.github.com> --- tools/flagd-http-connector/README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tools/flagd-http-connector/README.md b/tools/flagd-http-connector/README.md index ab3ebad6d..59a545730 100644 --- a/tools/flagd-http-connector/README.md +++ b/tools/flagd-http-connector/README.md @@ -26,8 +26,7 @@ The implementation is using Java HttpClient. Http Connector supports optional fail-safe initialization using a cache. If the initial fetch fails due to source unavailability, it can load the initial payload from the cache instead of falling back to default values. -This ensures smoother startup behavior until the source becomes available again. To be effective, the fallback cache’s -TTL should be longer than the expected duration of the source downtime during initialization. +This ensures smoother startup behavior until the source becomes available again. To be effective, the TTL of the fallback cache should be longer than the expected duration of the source downtime during initialization. ### Polling cache The polling cache is used to store the payload fetched from the URL. From 6653a9fd86e5adab104c4acf4c27f28b2a168bd5 Mon Sep 17 00:00:00 2001 From: liran2000 Date: Tue, 6 May 2025 15:29:00 +0300 Subject: [PATCH 32/40] updates Signed-off-by: liran2000 --- .../storage/connector/sync/http/HttpConnector.java | 10 ++++++++-- .../connector/sync/http/HttpConnectorOptions.java | 8 +++++++- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/tools/flagd-http-connector/src/main/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/HttpConnector.java b/tools/flagd-http-connector/src/main/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/HttpConnector.java index 0fa44226c..4636762a5 100644 --- a/tools/flagd-http-connector/src/main/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/HttpConnector.java +++ b/tools/flagd-http-connector/src/main/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/HttpConnector.java @@ -55,6 +55,7 @@ public class HttpConnector implements QueueSource { @NonNull private String url; + private URI uri; /** * HttpConnector constructor. @@ -71,6 +72,7 @@ public HttpConnector(HttpConnectorOptions httpConnectorOptions) { httpConnectorOptions.getProxyPort())); } this.url = httpConnectorOptions.getUrl(); + uri = URI.create(url); this.headers = httpConnectorOptions.getHeaders(); this.httpClientExecutor = httpConnectorOptions.getHttpClientExecutor(); scheduler = Executors.newScheduledThreadPool(httpConnectorOptions.getScheduledThreadPoolSize()); @@ -154,7 +156,7 @@ private boolean fetchAndUpdate() { } } HttpRequest.Builder requestBuilder = HttpRequest.newBuilder() - .uri(URI.create(url)) + .uri(uri) .timeout(Duration.ofSeconds(requestTimeoutSeconds)) .GET(); headers.forEach(requestBuilder::header); @@ -188,6 +190,11 @@ private boolean fetchAndUpdate() { log.warn("Unable to offer file content to queue: queue is full"); return false; } + updateCache(payload); + return true; + } + + private void updateCache(String payload) { if (payloadCache != null) { log.debug("scheduling cache update if needed"); scheduler.execute(() -> { @@ -202,7 +209,6 @@ private boolean fetchAndUpdate() { } ); } - return true; } private static boolean isSuccessful(HttpResponse response) { diff --git a/tools/flagd-http-connector/src/main/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/HttpConnectorOptions.java b/tools/flagd-http-connector/src/main/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/HttpConnectorOptions.java index 6a56955cd..b342f1fdb 100644 --- a/tools/flagd-http-connector/src/main/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/HttpConnectorOptions.java +++ b/tools/flagd-http-connector/src/main/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/HttpConnectorOptions.java @@ -1,6 +1,8 @@ package dev.openfeature.contrib.tools.flagd.resolver.process.storage.connector.sync.http; import java.lang.reflect.Method; +import java.net.MalformedURLException; +import java.net.URISyntaxException; import java.net.URL; import java.util.HashMap; import java.util.Map; @@ -117,7 +119,7 @@ private void validate(String url, Integer pollIntervalSeconds, Integer linkedBlo Integer scheduledThreadPoolSize, Integer requestTimeoutSeconds, Integer connectTimeoutSeconds, String proxyHost, Integer proxyPort, PayloadCacheOptions payloadCacheOptions, PayloadCache payloadCache, Boolean useFailsafeCache, Boolean usePollingCache) { - new URL(url).toURI(); + validateUrl(url); if (linkedBlockingQueueCapacity != null && (linkedBlockingQueueCapacity < 1 || linkedBlockingQueueCapacity > 1000)) { throw new IllegalArgumentException("linkedBlockingQueueCapacity must be between 1 and 1000"); @@ -170,4 +172,8 @@ private void validate(String url, Integer pollIntervalSeconds, Integer linkedBlo } } } + + private static void validateUrl(String url) throws URISyntaxException, MalformedURLException { + new URL(url).toURI(); + } } From 55f2a9d5e9b14aa189500e3c5e377bdd776b7f43 Mon Sep 17 00:00:00 2001 From: liran2000 Date: Tue, 6 May 2025 15:38:02 +0300 Subject: [PATCH 33/40] updates Signed-off-by: liran2000 --- .../storage/connector/sync/http/HttpConnectorOptions.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/flagd-http-connector/src/main/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/HttpConnectorOptions.java b/tools/flagd-http-connector/src/main/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/HttpConnectorOptions.java index b342f1fdb..20b838d91 100644 --- a/tools/flagd-http-connector/src/main/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/HttpConnectorOptions.java +++ b/tools/flagd-http-connector/src/main/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/HttpConnectorOptions.java @@ -1,5 +1,6 @@ package dev.openfeature.contrib.tools.flagd.resolver.process.storage.connector.sync.http; +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import java.lang.reflect.Method; import java.net.MalformedURLException; import java.net.URISyntaxException; @@ -8,7 +9,6 @@ import java.util.Map; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; -import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import lombok.Builder; import lombok.Getter; import lombok.NonNull; From bedc394380758aa1b6eca9df87d3cd9f71cf756f Mon Sep 17 00:00:00 2001 From: liran2000 Date: Tue, 6 May 2025 15:53:26 +0300 Subject: [PATCH 34/40] spotless apply Signed-off-by: liran2000 --- .../connector/sync/http/FailSafeCache.java | 8 +- .../connector/sync/http/HttpCacheFetcher.java | 3 +- .../connector/sync/http/HttpConnector.java | 50 ++-- .../sync/http/HttpConnectorOptions.java | 78 ++++-- .../sync/http/PayloadCacheOptions.java | 5 +- .../sync/http/FailSafeCacheTest.java | 131 +++++----- .../sync/http/HttpCacheFetcherTest.java | 99 ++++--- .../http/HttpConnectorIntegrationTest.java | 20 +- .../sync/http/HttpConnectorOptionsTest.java | 241 +++++++++--------- .../sync/http/HttpConnectorTest.java | 178 ++++++------- 10 files changed, 427 insertions(+), 386 deletions(-) diff --git a/tools/flagd-http-connector/src/main/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/FailSafeCache.java b/tools/flagd-http-connector/src/main/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/FailSafeCache.java index c8dc47a2f..1df4be637 100644 --- a/tools/flagd-http-connector/src/main/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/FailSafeCache.java +++ b/tools/flagd-http-connector/src/main/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/FailSafeCache.java @@ -16,13 +16,11 @@ * conditionally update the cache and {@link #get()} to retrieve the cached payload.

*/ @SuppressFBWarnings( - value = {"EI_EXPOSE_REP2", "CT_CONSTRUCTOR_THROW"}, - justification = "builder validations" -) + value = {"EI_EXPOSE_REP2", "CT_CONSTRUCTOR_THROW"}, + justification = "builder validations") @Slf4j public class FailSafeCache { - public static final String FAILSAFE_PAYLOAD_CACHE_KEY = FailSafeCache.class.getSimpleName() - + ".failsafe-payload"; + public static final String FAILSAFE_PAYLOAD_CACHE_KEY = FailSafeCache.class.getSimpleName() + ".failsafe-payload"; private long lastUpdateTimeMs; private long updateIntervalMs; private PayloadCache payloadCache; diff --git a/tools/flagd-http-connector/src/main/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/HttpCacheFetcher.java b/tools/flagd-http-connector/src/main/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/HttpCacheFetcher.java index 1096f0365..08b125274 100644 --- a/tools/flagd-http-connector/src/main/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/HttpCacheFetcher.java +++ b/tools/flagd-http-connector/src/main/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/HttpCacheFetcher.java @@ -41,7 +41,8 @@ public HttpResponse fetchContent(HttpClient httpClient, HttpRequest.Buil if (httpResponse.statusCode() == 200) { if (httpResponse.headers() != null) { cachedETag = httpResponse.headers().firstValue("ETag").orElse(null); - cachedLastModified = httpResponse.headers().firstValue("Last-Modified").orElse(null); + cachedLastModified = + httpResponse.headers().firstValue("Last-Modified").orElse(null); } log.debug("fetched new content"); } else if (httpResponse.statusCode() == 304) { diff --git a/tools/flagd-http-connector/src/main/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/HttpConnector.java b/tools/flagd-http-connector/src/main/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/HttpConnector.java index 4636762a5..8c086e153 100644 --- a/tools/flagd-http-connector/src/main/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/HttpConnector.java +++ b/tools/flagd-http-connector/src/main/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/HttpConnector.java @@ -53,8 +53,8 @@ public class HttpConnector implements QueueSource { private int payloadCachePollTtlSeconds; private boolean usePollingCache; - @NonNull - private String url; + @NonNull private String url; + private URI uri; /** @@ -68,8 +68,8 @@ public HttpConnector(HttpConnectorOptions httpConnectorOptions) { this.requestTimeoutSeconds = httpConnectorOptions.getRequestTimeoutSeconds(); ProxySelector proxySelector = NO_PROXY; if (httpConnectorOptions.getProxyHost() != null && httpConnectorOptions.getProxyPort() != null) { - proxySelector = ProxySelector.of(new InetSocketAddress(httpConnectorOptions.getProxyHost(), - httpConnectorOptions.getProxyPort())); + proxySelector = ProxySelector.of( + new InetSocketAddress(httpConnectorOptions.getProxyHost(), httpConnectorOptions.getProxyPort())); } this.url = httpConnectorOptions.getUrl(); uri = URI.create(url); @@ -77,17 +77,17 @@ public HttpConnector(HttpConnectorOptions httpConnectorOptions) { this.httpClientExecutor = httpConnectorOptions.getHttpClientExecutor(); scheduler = Executors.newScheduledThreadPool(httpConnectorOptions.getScheduledThreadPoolSize()); this.client = HttpClient.newBuilder() - .connectTimeout(Duration.ofSeconds(httpConnectorOptions.getConnectTimeoutSeconds())) - .proxy(proxySelector) - .executor(this.httpClientExecutor) - .build(); + .connectTimeout(Duration.ofSeconds(httpConnectorOptions.getConnectTimeoutSeconds())) + .proxy(proxySelector) + .executor(this.httpClientExecutor) + .build(); this.queue = new LinkedBlockingQueue<>(httpConnectorOptions.getLinkedBlockingQueueCapacity()); this.payloadCache = httpConnectorOptions.getPayloadCache(); if (payloadCache != null && Boolean.TRUE.equals(httpConnectorOptions.getUseFailsafeCache())) { this.failSafeCache = FailSafeCache.builder() - .payloadCache(payloadCache) - .payloadCacheOptions(httpConnectorOptions.getPayloadCacheOptions()) - .build(); + .payloadCache(payloadCache) + .payloadCacheOptions(httpConnectorOptions.getPayloadCacheOptions()) + .build(); } if (Boolean.TRUE.equals(httpConnectorOptions.getUseHttpCache())) { httpCacheFetcher = new HttpCacheFetcher(); @@ -101,10 +101,7 @@ public void init() throws Exception { log.info("init Http Connector"); } - @SuppressFBWarnings( - value = "EI_EXPOSE_REP", - justification = "parent defines the interface" - ) + @SuppressFBWarnings(value = "EI_EXPOSE_REP", justification = "parent defines the interface") @Override public BlockingQueue getStreamQueue() { boolean success = fetchAndUpdate(); @@ -156,9 +153,9 @@ private boolean fetchAndUpdate() { } } HttpRequest.Builder requestBuilder = HttpRequest.newBuilder() - .uri(uri) - .timeout(Duration.ofSeconds(requestTimeoutSeconds)) - .GET(); + .uri(uri) + .timeout(Duration.ofSeconds(requestTimeoutSeconds)) + .GET(); headers.forEach(requestBuilder::header); HttpResponse response; @@ -198,16 +195,15 @@ private void updateCache(String payload) { if (payloadCache != null) { log.debug("scheduling cache update if needed"); scheduler.execute(() -> { - if (failSafeCache != null) { - log.debug("updating payload in failsafe cache if needed"); - failSafeCache.updatePayloadIfNeeded(payload); - } - if (payloadCache != null) { - log.debug("updating polling payload in cache"); - payloadCache.put(POLLING_PAYLOAD_CACHE_KEY, payload, payloadCachePollTtlSeconds); - } + if (failSafeCache != null) { + log.debug("updating payload in failsafe cache if needed"); + failSafeCache.updatePayloadIfNeeded(payload); + } + if (payloadCache != null) { + log.debug("updating polling payload in cache"); + payloadCache.put(POLLING_PAYLOAD_CACHE_KEY, payload, payloadCachePollTtlSeconds); } - ); + }); } } diff --git a/tools/flagd-http-connector/src/main/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/HttpConnectorOptions.java b/tools/flagd-http-connector/src/main/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/HttpConnectorOptions.java index 20b838d91..9437f7f1d 100644 --- a/tools/flagd-http-connector/src/main/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/HttpConnectorOptions.java +++ b/tools/flagd-http-connector/src/main/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/HttpConnectorOptions.java @@ -20,55 +20,88 @@ */ @SuppressFBWarnings( value = {"EI_EXPOSE_REP", "EI_EXPOSE_REP2", "CT_CONSTRUCTOR_THROW"}, - justification = "builder validations" -) + justification = "builder validations") @Slf4j @Getter public class HttpConnectorOptions { @Builder.Default private Integer pollIntervalSeconds = 60; + @Builder.Default private Integer connectTimeoutSeconds = 10; + @Builder.Default private Integer requestTimeoutSeconds = 10; + @Builder.Default private Integer linkedBlockingQueueCapacity = 100; + @Builder.Default private Integer scheduledThreadPoolSize = 2; + @Builder.Default private Map headers = new HashMap<>(); + @Builder.Default private ExecutorService httpClientExecutor = Executors.newFixedThreadPool(1); + @Builder.Default private String proxyHost; + @Builder.Default private Integer proxyPort; + @Builder.Default private PayloadCacheOptions payloadCacheOptions; + @Builder.Default private PayloadCache payloadCache; + @Builder.Default private Boolean useHttpCache; + @Builder.Default private Boolean useFailsafeCache; + @Builder.Default private Boolean usePollingCache; - @NonNull - private String url; + + @NonNull private String url; /** * HttpConnectorOptions constructor. */ @Builder - public HttpConnectorOptions(Integer pollIntervalSeconds, Integer linkedBlockingQueueCapacity, - Integer scheduledThreadPoolSize, Integer requestTimeoutSeconds, Integer connectTimeoutSeconds, String url, - Map headers, ExecutorService httpClientExecutor, String proxyHost, Integer proxyPort, - PayloadCacheOptions payloadCacheOptions, PayloadCache payloadCache, Boolean useHttpCache, - Boolean useFailsafeCache, Boolean usePollingCache) { - validate(url, pollIntervalSeconds, linkedBlockingQueueCapacity, scheduledThreadPoolSize, requestTimeoutSeconds, - connectTimeoutSeconds, proxyHost, proxyPort, payloadCacheOptions, payloadCache, useFailsafeCache, - usePollingCache); + public HttpConnectorOptions( + Integer pollIntervalSeconds, + Integer linkedBlockingQueueCapacity, + Integer scheduledThreadPoolSize, + Integer requestTimeoutSeconds, + Integer connectTimeoutSeconds, + String url, + Map headers, + ExecutorService httpClientExecutor, + String proxyHost, + Integer proxyPort, + PayloadCacheOptions payloadCacheOptions, + PayloadCache payloadCache, + Boolean useHttpCache, + Boolean useFailsafeCache, + Boolean usePollingCache) { + validate( + url, + pollIntervalSeconds, + linkedBlockingQueueCapacity, + scheduledThreadPoolSize, + requestTimeoutSeconds, + connectTimeoutSeconds, + proxyHost, + proxyPort, + payloadCacheOptions, + payloadCache, + useFailsafeCache, + usePollingCache); if (pollIntervalSeconds != null) { this.pollIntervalSeconds = pollIntervalSeconds; } @@ -115,10 +148,19 @@ public HttpConnectorOptions(Integer pollIntervalSeconds, Integer linkedBlockingQ } @SneakyThrows - private void validate(String url, Integer pollIntervalSeconds, Integer linkedBlockingQueueCapacity, - Integer scheduledThreadPoolSize, Integer requestTimeoutSeconds, Integer connectTimeoutSeconds, - String proxyHost, Integer proxyPort, PayloadCacheOptions payloadCacheOptions, - PayloadCache payloadCache, Boolean useFailsafeCache, Boolean usePollingCache) { + private void validate( + String url, + Integer pollIntervalSeconds, + Integer linkedBlockingQueueCapacity, + Integer scheduledThreadPoolSize, + Integer requestTimeoutSeconds, + Integer connectTimeoutSeconds, + String proxyHost, + Integer proxyPort, + PayloadCacheOptions payloadCacheOptions, + PayloadCache payloadCache, + Boolean useFailsafeCache, + Boolean usePollingCache) { validateUrl(url); if (linkedBlockingQueueCapacity != null && (linkedBlockingQueueCapacity < 1 || linkedBlockingQueueCapacity > 1000)) { @@ -152,7 +194,7 @@ private void validate(String url, Integer pollIntervalSeconds, Integer linkedBlo } if ((Boolean.TRUE.equals(useFailsafeCache) || Boolean.TRUE.equals(usePollingCache)) && payloadCache == null) { throw new IllegalArgumentException( - "payloadCache must be set if useFailsafeCache or usePollingCache is set"); + "payloadCache must be set if useFailsafeCache or usePollingCache is set"); } if (payloadCache != null && Boolean.TRUE.equals(usePollingCache)) { @@ -168,7 +210,7 @@ private void validate(String url, Integer pollIntervalSeconds, Integer linkedBlo } if (!overridesTtlPutMethod) { throw new IllegalArgumentException("when usePollingCache is used, payloadCache must override " - + "put(String key, String payload, int ttlSeconds)"); + + "put(String key, String payload, int ttlSeconds)"); } } } diff --git a/tools/flagd-http-connector/src/main/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/PayloadCacheOptions.java b/tools/flagd-http-connector/src/main/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/PayloadCacheOptions.java index 72dfc6730..14d26d1b9 100644 --- a/tools/flagd-http-connector/src/main/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/PayloadCacheOptions.java +++ b/tools/flagd-http-connector/src/main/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/PayloadCacheOptions.java @@ -17,9 +17,8 @@ *

*/ @SuppressFBWarnings( - value = {"EI_EXPOSE_REP", "EI_EXPOSE_REP2"}, - justification = "builder validations" -) + value = {"EI_EXPOSE_REP", "EI_EXPOSE_REP2"}, + justification = "builder validations") @Builder @Getter public class PayloadCacheOptions { diff --git a/tools/flagd-http-connector/src/test/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/FailSafeCacheTest.java b/tools/flagd-http-connector/src/test/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/FailSafeCacheTest.java index 08ee6c1c3..e402cba54 100644 --- a/tools/flagd-http-connector/src/test/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/FailSafeCacheTest.java +++ b/tools/flagd-http-connector/src/test/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/FailSafeCacheTest.java @@ -21,20 +21,18 @@ import lombok.SneakyThrows; import org.junit.jupiter.api.Test; - public class FailSafeCacheTest { @Test public void testConstructorInitializesWithValidParameters() { PayloadCache mockCache = mock(PayloadCache.class); - PayloadCacheOptions options = PayloadCacheOptions.builder() - .updateIntervalSeconds(600) - .build(); + PayloadCacheOptions options = + PayloadCacheOptions.builder().updateIntervalSeconds(600).build(); FailSafeCache wrapper = FailSafeCache.builder() - .payloadCache(mockCache) - .payloadCacheOptions(options) - .build(); + .payloadCache(mockCache) + .payloadCacheOptions(options) + .build(); assertNotNull(wrapper); @@ -49,17 +47,13 @@ public void testConstructorInitializesWithValidParameters() { @Test public void testConstructorThrowsExceptionForInvalidInterval() { PayloadCache mockCache = mock(PayloadCache.class); - PayloadCacheOptions options = PayloadCacheOptions.builder() - .updateIntervalSeconds(0) - .build(); + PayloadCacheOptions options = + PayloadCacheOptions.builder().updateIntervalSeconds(0).build(); - FailSafeCache.FailSafeCacheBuilder payloadCacheWrapperBuilder = FailSafeCache.builder() - .payloadCache(mockCache) - .payloadCacheOptions(options); - IllegalArgumentException exception = assertThrows( - IllegalArgumentException.class, - payloadCacheWrapperBuilder::build - ); + FailSafeCache.FailSafeCacheBuilder payloadCacheWrapperBuilder = + FailSafeCache.builder().payloadCache(mockCache).payloadCacheOptions(options); + IllegalArgumentException exception = + assertThrows(IllegalArgumentException.class, payloadCacheWrapperBuilder::build); assertEquals("pollIntervalSeconds must be larger than 0", exception.getMessage()); } @@ -67,13 +61,12 @@ public void testConstructorThrowsExceptionForInvalidInterval() { @Test public void testUpdateSkipsWhenIntervalNotPassed() { PayloadCache mockCache = mock(PayloadCache.class); - PayloadCacheOptions options = PayloadCacheOptions.builder() - .updateIntervalSeconds(600) - .build(); + PayloadCacheOptions options = + PayloadCacheOptions.builder().updateIntervalSeconds(600).build(); FailSafeCache wrapper = FailSafeCache.builder() - .payloadCache(mockCache) - .payloadCacheOptions(options) - .build(); + .payloadCache(mockCache) + .payloadCacheOptions(options) + .build(); String initialPayload = "initial-payload"; wrapper.updatePayloadIfNeeded(initialPayload); @@ -88,13 +81,12 @@ public void testUpdateSkipsWhenIntervalNotPassed() { @Test public void testUpdatePayloadIfNeededHandlesPutException() { PayloadCache mockCache = mock(PayloadCache.class); - PayloadCacheOptions options = PayloadCacheOptions.builder() - .updateIntervalSeconds(600) - .build(); + PayloadCacheOptions options = + PayloadCacheOptions.builder().updateIntervalSeconds(600).build(); FailSafeCache wrapper = FailSafeCache.builder() - .payloadCache(mockCache) - .payloadCacheOptions(options) - .build(); + .payloadCache(mockCache) + .payloadCacheOptions(options) + .build(); String testPayload = "test-payload"; doThrow(new RuntimeException("put exception")).when(mockCache).put(any(), eq(testPayload)); @@ -108,12 +100,12 @@ public void testUpdatePayloadIfNeededHandlesPutException() { public void testUpdatePayloadIfNeededUpdatesCacheAfterInterval() { PayloadCache mockCache = mock(PayloadCache.class); PayloadCacheOptions options = PayloadCacheOptions.builder() - .updateIntervalSeconds(1) // 1 second interval for quick test - .build(); + .updateIntervalSeconds(1) // 1 second interval for quick test + .build(); FailSafeCache wrapper = FailSafeCache.builder() - .payloadCache(mockCache) - .payloadCacheOptions(options) - .build(); + .payloadCache(mockCache) + .payloadCacheOptions(options) + .build(); String initialPayload = "initial-payload"; String newPayload = "new-payload"; @@ -129,13 +121,12 @@ public void testUpdatePayloadIfNeededUpdatesCacheAfterInterval() { @Test public void testGetReturnsNullWhenCacheGetThrowsException() { PayloadCache mockCache = mock(PayloadCache.class); - PayloadCacheOptions options = PayloadCacheOptions.builder() - .updateIntervalSeconds(600) - .build(); + PayloadCacheOptions options = + PayloadCacheOptions.builder().updateIntervalSeconds(600).build(); FailSafeCache wrapper = FailSafeCache.builder() - .payloadCache(mockCache) - .payloadCacheOptions(options) - .build(); + .payloadCache(mockCache) + .payloadCacheOptions(options) + .build(); when(mockCache.get(any())).thenThrow(new RuntimeException("Cache get failed")); @@ -149,13 +140,12 @@ public void testGetReturnsNullWhenCacheGetThrowsException() { @Test public void test_get_returns_cached_payload() { PayloadCache mockCache = mock(PayloadCache.class); - PayloadCacheOptions options = PayloadCacheOptions.builder() - .updateIntervalSeconds(600) - .build(); + PayloadCacheOptions options = + PayloadCacheOptions.builder().updateIntervalSeconds(600).build(); FailSafeCache wrapper = FailSafeCache.builder() - .payloadCache(mockCache) - .payloadCacheOptions(options) - .build(); + .payloadCache(mockCache) + .payloadCacheOptions(options) + .build(); String expectedPayload = "cached-payload"; when(mockCache.get(any())).thenReturn(expectedPayload); @@ -169,13 +159,12 @@ public void test_get_returns_cached_payload() { @Test public void test_first_call_updates_cache() { PayloadCache mockCache = mock(PayloadCache.class); - PayloadCacheOptions options = PayloadCacheOptions.builder() - .updateIntervalSeconds(600) - .build(); + PayloadCacheOptions options = + PayloadCacheOptions.builder().updateIntervalSeconds(600).build(); FailSafeCache wrapper = FailSafeCache.builder() - .payloadCache(mockCache) - .payloadCacheOptions(options) - .build(); + .payloadCache(mockCache) + .payloadCacheOptions(options) + .build(); String testPayload = "initial-payload"; @@ -188,12 +177,12 @@ public void test_first_call_updates_cache() { public void test_update_payload_once_within_interval() { PayloadCache mockCache = mock(PayloadCache.class); PayloadCacheOptions options = PayloadCacheOptions.builder() - .updateIntervalSeconds(1) // 1 second interval - .build(); + .updateIntervalSeconds(1) // 1 second interval + .build(); FailSafeCache wrapper = FailSafeCache.builder() - .payloadCache(mockCache) - .payloadCacheOptions(options) - .build(); + .payloadCache(mockCache) + .payloadCacheOptions(options) + .build(); String testPayload = "test-payload"; @@ -207,13 +196,12 @@ public void test_update_payload_once_within_interval() { @Test public void test_last_update_time_ms_updated_after_successful_cache_update() { PayloadCache mockCache = mock(PayloadCache.class); - PayloadCacheOptions options = PayloadCacheOptions.builder() - .updateIntervalSeconds(600) - .build(); + PayloadCacheOptions options = + PayloadCacheOptions.builder().updateIntervalSeconds(600).build(); FailSafeCache wrapper = FailSafeCache.builder() - .payloadCache(mockCache) - .payloadCacheOptions(options) - .build(); + .payloadCache(mockCache) + .payloadCacheOptions(options) + .build(); String testPayload = "test-payload"; wrapper.updatePayloadIfNeeded(testPayload); @@ -224,20 +212,20 @@ public void test_last_update_time_ms_updated_after_successful_cache_update() { lastUpdateTimeMsField.setAccessible(true); long lastUpdateTimeMs = (Long) lastUpdateTimeMsField.get(wrapper); - assertTrue(System.currentTimeMillis() - lastUpdateTimeMs < 1000, - "lastUpdateTimeMs should be updated to current time"); + assertTrue( + System.currentTimeMillis() - lastUpdateTimeMs < 1000, + "lastUpdateTimeMs should be updated to current time"); } @Test public void test_update_payload_if_needed_respects_update_interval() { PayloadCache mockCache = mock(PayloadCache.class); - PayloadCacheOptions options = PayloadCacheOptions.builder() - .updateIntervalSeconds(600) - .build(); + PayloadCacheOptions options = + PayloadCacheOptions.builder().updateIntervalSeconds(600).build(); FailSafeCache wrapper = spy(FailSafeCache.builder() - .payloadCache(mockCache) - .payloadCacheOptions(options) - .build()); + .payloadCache(mockCache) + .payloadCacheOptions(options) + .build()); String testPayload = "test-payload"; long initialTime = System.currentTimeMillis(); @@ -265,5 +253,4 @@ public void test_update_payload_if_needed_respects_update_interval() { // Verify the payload was updated again verify(mockCache, times(2)).put(any(), eq(testPayload)); } - } diff --git a/tools/flagd-http-connector/src/test/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/HttpCacheFetcherTest.java b/tools/flagd-http-connector/src/test/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/HttpCacheFetcherTest.java index bac64ac73..9a2ae348f 100644 --- a/tools/flagd-http-connector/src/test/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/HttpCacheFetcherTest.java +++ b/tools/flagd-http-connector/src/test/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/HttpCacheFetcherTest.java @@ -1,7 +1,7 @@ package dev.openfeature.contrib.tools.flagd.resolver.process.storage.connector.sync.http; -import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; @@ -23,11 +23,10 @@ import java.util.Arrays; import java.util.Collections; import java.util.HashMap; -import java.util.List; import java.util.Map; -import java.util.Optional; import lombok.SneakyThrows; import org.junit.jupiter.api.Test; + public class HttpCacheFetcherTest { @Test @@ -36,10 +35,13 @@ public void testFirstRequestSendsNoCacheHeaders() throws Exception { HttpRequest.Builder requestBuilderMock = mock(HttpRequest.Builder.class); HttpRequest requestMock = mock(HttpRequest.class); HttpResponse responseMock = spy(HttpResponse.class); - doReturn(HttpHeaders.of(new HashMap<>(), (a, b) -> true)).when(responseMock).headers(); + doReturn(HttpHeaders.of(new HashMap<>(), (a, b) -> true)) + .when(responseMock) + .headers(); when(requestBuilderMock.build()).thenReturn(requestMock); - when(httpClientMock.send(eq(requestMock), any(HttpResponse.BodyHandler.class))).thenReturn(responseMock); + when(httpClientMock.send(eq(requestMock), any(HttpResponse.BodyHandler.class))) + .thenReturn(responseMock); when(responseMock.statusCode()).thenReturn(200); HttpCacheFetcher fetcher = new HttpCacheFetcher(); @@ -55,11 +57,10 @@ public void testResponseWith200ButNoCacheHeaders() throws Exception { HttpRequest.Builder requestBuilderMock = mock(HttpRequest.Builder.class); HttpRequest requestMock = mock(HttpRequest.class); HttpResponse responseMock = mock(HttpResponse.class); - HttpHeaders headers = HttpHeaders.of( - Collections.emptyMap(), - (a, b) -> true); + HttpHeaders headers = HttpHeaders.of(Collections.emptyMap(), (a, b) -> true); when(requestBuilderMock.build()).thenReturn(requestMock); - when(httpClientMock.send(eq(requestMock), any(HttpResponse.BodyHandler.class))).thenReturn(responseMock); + when(httpClientMock.send(eq(requestMock), any(HttpResponse.BodyHandler.class))) + .thenReturn(responseMock); when(responseMock.statusCode()).thenReturn(200); when(responseMock.headers()).thenReturn(headers); @@ -83,10 +84,13 @@ public void testFetchContentReturnsHttpResponse() throws Exception { HttpRequest.Builder requestBuilderMock = mock(HttpRequest.Builder.class); HttpRequest requestMock = mock(HttpRequest.class); HttpResponse responseMock = spy(HttpResponse.class); - doReturn(HttpHeaders.of(new HashMap<>(), (a, b) -> true)).when(responseMock).headers(); + doReturn(HttpHeaders.of(new HashMap<>(), (a, b) -> true)) + .when(responseMock) + .headers(); when(requestBuilderMock.build()).thenReturn(requestMock); - when(httpClientMock.send(eq(requestMock), any(HttpResponse.BodyHandler.class))).thenReturn(responseMock); + when(httpClientMock.send(eq(requestMock), any(HttpResponse.BodyHandler.class))) + .thenReturn(responseMock); when(responseMock.statusCode()).thenReturn(404); HttpCacheFetcher fetcher = new HttpCacheFetcher(); @@ -101,10 +105,13 @@ public void test200ResponseNoEtagOrLastModified() throws Exception { HttpRequest.Builder requestBuilderMock = mock(HttpRequest.Builder.class); HttpRequest requestMock = mock(HttpRequest.class); HttpResponse responseMock = spy(HttpResponse.class); - doReturn(HttpHeaders.of(new HashMap<>(), (a, b) -> true)).when(responseMock).headers(); + doReturn(HttpHeaders.of(new HashMap<>(), (a, b) -> true)) + .when(responseMock) + .headers(); when(requestBuilderMock.build()).thenReturn(requestMock); - when(httpClientMock.send(eq(requestMock), any(HttpResponse.BodyHandler.class))).thenReturn(responseMock); + when(httpClientMock.send(eq(requestMock), any(HttpResponse.BodyHandler.class))) + .thenReturn(responseMock); when(responseMock.statusCode()).thenReturn(200); HttpCacheFetcher fetcher = new HttpCacheFetcher(); @@ -125,12 +132,18 @@ public void testUpdateCacheOn200Response() throws Exception { HttpRequest requestMock = mock(HttpRequest.class); HttpResponse responseMock = spy(HttpResponse.class); doReturn(HttpHeaders.of( - Map.of("Last-Modified", Arrays.asList("Wed, 21 Oct 2015 07:28:00 GMT"), - "ETag", Arrays.asList("etag-value")), - (a, b) -> true)).when(responseMock).headers(); + Map.of( + "Last-Modified", + Arrays.asList("Wed, 21 Oct 2015 07:28:00 GMT"), + "ETag", + Arrays.asList("etag-value")), + (a, b) -> true)) + .when(responseMock) + .headers(); when(requestBuilderMock.build()).thenReturn(requestMock); - when(httpClientMock.send(eq(requestMock), any(HttpResponse.BodyHandler.class))).thenReturn(responseMock); + when(httpClientMock.send(eq(requestMock), any(HttpResponse.BodyHandler.class))) + .thenReturn(responseMock); when(responseMock.statusCode()).thenReturn(200); HttpCacheFetcher fetcher = new HttpCacheFetcher(); fetcher.fetchContent(httpClientMock, requestBuilderMock); @@ -149,12 +162,13 @@ public void testRequestWithCachedEtagIncludesIfNoneMatchHeader() throws Exceptio HttpRequest.Builder requestBuilderMock = mock(HttpRequest.Builder.class); HttpRequest requestMock = mock(HttpRequest.class); HttpResponse responseMock = spy(HttpResponse.class); - doReturn(HttpHeaders.of( - Map.of("ETag", Arrays.asList("12345")), - (a, b) -> true)).when(responseMock).headers(); + doReturn(HttpHeaders.of(Map.of("ETag", Arrays.asList("12345")), (a, b) -> true)) + .when(responseMock) + .headers(); when(requestBuilderMock.build()).thenReturn(requestMock); - when(httpClientMock.send(eq(requestMock), any(HttpResponse.BodyHandler.class))).thenReturn(responseMock); + when(httpClientMock.send(eq(requestMock), any(HttpResponse.BodyHandler.class))) + .thenReturn(responseMock); when(responseMock.statusCode()).thenReturn(200); HttpCacheFetcher fetcher = new HttpCacheFetcher(); @@ -184,10 +198,13 @@ public void testResponseWithUnexpectedStatusCode() throws Exception { HttpRequest.Builder requestBuilderMock = mock(HttpRequest.Builder.class); HttpRequest requestMock = mock(HttpRequest.class); HttpResponse responseMock = spy(HttpResponse.class); - doReturn(HttpHeaders.of(new HashMap<>(), (a, b) -> true)).when(responseMock).headers(); + doReturn(HttpHeaders.of(new HashMap<>(), (a, b) -> true)) + .when(responseMock) + .headers(); when(requestBuilderMock.build()).thenReturn(requestMock); - when(httpClientMock.send(eq(requestMock), any(HttpResponse.BodyHandler.class))).thenReturn(responseMock); + when(httpClientMock.send(eq(requestMock), any(HttpResponse.BodyHandler.class))) + .thenReturn(responseMock); when(responseMock.statusCode()).thenReturn(500); HttpCacheFetcher fetcher = new HttpCacheFetcher(); @@ -205,11 +222,13 @@ public void testRequestIncludesIfModifiedSinceHeaderWhenLastModifiedCached() thr HttpRequest requestMock = mock(HttpRequest.class); HttpResponse responseMock = spy(HttpResponse.class); doReturn(HttpHeaders.of( - Map.of("Last-Modified", Arrays.asList("Wed, 21 Oct 2015 07:28:00 GMT")), - (a, b) -> true)).when(responseMock).headers(); + Map.of("Last-Modified", Arrays.asList("Wed, 21 Oct 2015 07:28:00 GMT")), (a, b) -> true)) + .when(responseMock) + .headers(); when(requestBuilderMock.build()).thenReturn(requestMock); - when(httpClientMock.send(eq(requestMock), any(HttpResponse.BodyHandler.class))).thenReturn(responseMock); + when(httpClientMock.send(eq(requestMock), any(HttpResponse.BodyHandler.class))) + .thenReturn(responseMock); when(responseMock.statusCode()).thenReturn(200); HttpCacheFetcher fetcher = new HttpCacheFetcher(); @@ -229,8 +248,8 @@ public void testCalls200And304Responses() throws Exception { when(requestBuilderMock.build()).thenReturn(requestMock); when(httpClientMock.send(eq(requestMock), any(HttpResponse.BodyHandler.class))) - .thenReturn(responseMock200) - .thenReturn(responseMock304); + .thenReturn(responseMock200) + .thenReturn(responseMock304); when(responseMock200.statusCode()).thenReturn(200); when(responseMock304.statusCode()).thenReturn(304); @@ -248,10 +267,13 @@ public void testRequestIncludesBothEtagAndLastModifiedHeaders() throws Exception HttpRequest.Builder requestBuilderMock = mock(HttpRequest.Builder.class); HttpRequest requestMock = mock(HttpRequest.class); HttpResponse responseMock = spy(HttpResponse.class); - doReturn(HttpHeaders.of(new HashMap<>(), (a, b) -> true)).when(responseMock).headers(); + doReturn(HttpHeaders.of(new HashMap<>(), (a, b) -> true)) + .when(responseMock) + .headers(); when(requestBuilderMock.build()).thenReturn(requestMock); - when(httpClientMock.send(eq(requestMock), any(HttpResponse.BodyHandler.class))).thenReturn(responseMock); + when(httpClientMock.send(eq(requestMock), any(HttpResponse.BodyHandler.class))) + .thenReturn(responseMock); when(responseMock.statusCode()).thenReturn(200); HttpCacheFetcher fetcher = new HttpCacheFetcher(); @@ -277,7 +299,7 @@ public void testHttpClientSendExceptionPropagation() { when(requestBuilderMock.build()).thenReturn(requestMock); when(httpClientMock.send(eq(requestMock), any(HttpResponse.BodyHandler.class))) - .thenThrow(new IOException("Network error")); + .thenThrow(new IOException("Network error")); HttpCacheFetcher fetcher = new HttpCacheFetcher(); assertThrows(IOException.class, () -> { @@ -292,12 +314,18 @@ public void testOnlyEtagAndLastModifiedHeadersCached() throws Exception { HttpRequest requestMock = mock(HttpRequest.class); HttpResponse responseMock = spy(HttpResponse.class); doReturn(HttpHeaders.of( - Map.of("Last-Modified", Arrays.asList("last-modified-value"), - "ETag", Arrays.asList("etag-value")), - (a, b) -> true)).when(responseMock).headers(); + Map.of( + "Last-Modified", + Arrays.asList("last-modified-value"), + "ETag", + Arrays.asList("etag-value")), + (a, b) -> true)) + .when(responseMock) + .headers(); when(requestBuilderMock.build()).thenReturn(requestMock); - when(httpClientMock.send(eq(requestMock), any(HttpResponse.BodyHandler.class))).thenReturn(responseMock); + when(httpClientMock.send(eq(requestMock), any(HttpResponse.BodyHandler.class))) + .thenReturn(responseMock); when(responseMock.statusCode()).thenReturn(200); HttpCacheFetcher fetcher = new HttpCacheFetcher(); @@ -305,5 +333,4 @@ public void testOnlyEtagAndLastModifiedHeadersCached() throws Exception { verify(requestBuilderMock, never()).header(eq("Some-Other-Header"), anyString()); } - } diff --git a/tools/flagd-http-connector/src/test/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/HttpConnectorIntegrationTest.java b/tools/flagd-http-connector/src/test/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/HttpConnectorIntegrationTest.java index bd198c1c4..36d61c1cb 100644 --- a/tools/flagd-http-connector/src/test/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/HttpConnectorIntegrationTest.java +++ b/tools/flagd-http-connector/src/test/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/HttpConnectorIntegrationTest.java @@ -28,18 +28,19 @@ void testGithubRawContent() { assumeTrue(parseBoolean("integrationTestsEnabled")); HttpConnector connector = null; try { - String testUrl = "https://raw.githubusercontent.com/open-feature/java-sdk-contrib/58fe5da7d4e2f6f4ae2c1caf3411a01e84a1dc1a/providers/flagd/version.txt"; + String testUrl = + "https://raw.githubusercontent.com/open-feature/java-sdk-contrib/58fe5da7d4e2f6f4ae2c1caf3411a01e84a1dc1a/providers/flagd/version.txt"; HttpConnectorOptions httpConnectorOptions = HttpConnectorOptions.builder() - .url(testUrl) - .connectTimeoutSeconds(10) - .requestTimeoutSeconds(10) - .useHttpCache(true) - .pollIntervalSeconds(5) - .build(); + .url(testUrl) + .connectTimeoutSeconds(10) + .requestTimeoutSeconds(10) + .useHttpCache(true) + .pollIntervalSeconds(5) + .build(); connector = HttpConnector.builder() - .httpConnectorOptions(httpConnectorOptions) - .build(); + .httpConnectorOptions(httpConnectorOptions) + .build(); BlockingQueue queue = connector.getStreamQueue(); delay(20000); assertEquals(1, queue.size()); @@ -53,5 +54,4 @@ void testGithubRawContent() { public static boolean parseBoolean(String key) { return Boolean.parseBoolean(System.getProperty(key, System.getenv(key))); } - } diff --git a/tools/flagd-http-connector/src/test/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/HttpConnectorOptionsTest.java b/tools/flagd-http-connector/src/test/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/HttpConnectorOptionsTest.java index ffb5c8e89..a762a28c8 100644 --- a/tools/flagd-http-connector/src/test/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/HttpConnectorOptionsTest.java +++ b/tools/flagd-http-connector/src/test/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/HttpConnectorOptionsTest.java @@ -1,25 +1,24 @@ package dev.openfeature.contrib.tools.flagd.resolver.process.storage.connector.sync.http; -import org.junit.jupiter.api.Test; -import java.net.MalformedURLException; -import java.util.HashMap; -import java.util.Map; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; - import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; +import java.net.MalformedURLException; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import org.junit.jupiter.api.Test; + public class HttpConnectorOptionsTest { @Test public void testDefaultValuesInitialization() { - HttpConnectorOptions options = HttpConnectorOptions.builder() - .url("https://example.com") - .build(); + HttpConnectorOptions options = + HttpConnectorOptions.builder().url("https://example.com").build(); assertEquals(60, options.getPollIntervalSeconds().intValue()); assertEquals(10, options.getConnectTimeoutSeconds().intValue()); @@ -40,11 +39,8 @@ public void testDefaultValuesInitialization() { @Test public void testInvalidUrlFormat() { MalformedURLException exception = assertThrows( - MalformedURLException.class, - () -> HttpConnectorOptions.builder() - .url("invalid-url") - .build() - ); + MalformedURLException.class, + () -> HttpConnectorOptions.builder().url("invalid-url").build()); assertNotNull(exception); } @@ -52,13 +48,13 @@ public void testInvalidUrlFormat() { @Test public void testCustomValuesInitialization() { HttpConnectorOptions options = HttpConnectorOptions.builder() - .pollIntervalSeconds(120) - .connectTimeoutSeconds(20) - .requestTimeoutSeconds(30) - .linkedBlockingQueueCapacity(200) - .scheduledThreadPoolSize(5) - .url("http://example.com") - .build(); + .pollIntervalSeconds(120) + .connectTimeoutSeconds(20) + .requestTimeoutSeconds(30) + .linkedBlockingQueueCapacity(200) + .scheduledThreadPoolSize(5) + .url("http://example.com") + .build(); assertEquals(120, options.getPollIntervalSeconds().intValue()); assertEquals(20, options.getConnectTimeoutSeconds().intValue()); @@ -75,9 +71,9 @@ public void testCustomHeadersMap() { customHeaders.put("Content-Type", "application/json"); HttpConnectorOptions options = HttpConnectorOptions.builder() - .url("http://example.com") - .headers(customHeaders) - .build(); + .url("http://example.com") + .headers(customHeaders) + .build(); assertEquals("Bearer token", options.getHeaders().get("Authorization")); assertEquals("application/json", options.getHeaders().get("Content-Type")); @@ -87,18 +83,17 @@ public void testCustomHeadersMap() { public void testCustomExecutorService() { ExecutorService customExecutor = Executors.newFixedThreadPool(5); HttpConnectorOptions options = HttpConnectorOptions.builder() - .url("https://example.com") - .httpClientExecutor(customExecutor) - .build(); + .url("https://example.com") + .httpClientExecutor(customExecutor) + .build(); assertEquals(customExecutor, options.getHttpClientExecutor()); } @Test public void testSettingPayloadCacheWithValidOptions() { - PayloadCacheOptions cacheOptions = PayloadCacheOptions.builder() - .updateIntervalSeconds(1800) - .build(); + PayloadCacheOptions cacheOptions = + PayloadCacheOptions.builder().updateIntervalSeconds(1800).build(); PayloadCache payloadCache = new PayloadCache() { private String payload; @@ -114,10 +109,10 @@ public String get(String key) { }; HttpConnectorOptions options = HttpConnectorOptions.builder() - .url("https://example.com") - .payloadCacheOptions(cacheOptions) - .payloadCache(payloadCache) - .build(); + .url("https://example.com") + .payloadCacheOptions(cacheOptions) + .payloadCache(payloadCache) + .build(); assertNotNull(options.getPayloadCacheOptions()); assertNotNull(options.getPayloadCache()); @@ -127,10 +122,10 @@ public String get(String key) { @Test public void testProxyConfigurationWithValidHostAndPort() { HttpConnectorOptions options = HttpConnectorOptions.builder() - .url("https://example.com") - .proxyHost("proxy.example.com") - .proxyPort(8080) - .build(); + .url("https://example.com") + .proxyHost("proxy.example.com") + .proxyPort(8080) + .build(); assertEquals("proxy.example.com", options.getProxyHost()); assertEquals(8080, options.getProxyPort().intValue()); @@ -140,17 +135,17 @@ public void testProxyConfigurationWithValidHostAndPort() { public void testLinkedBlockingQueueCapacityOutOfRange() { IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> { HttpConnectorOptions.builder() - .url("https://example.com") - .linkedBlockingQueueCapacity(0) - .build(); + .url("https://example.com") + .linkedBlockingQueueCapacity(0) + .build(); }); assertEquals("linkedBlockingQueueCapacity must be between 1 and 1000", exception.getMessage()); exception = assertThrows(IllegalArgumentException.class, () -> { HttpConnectorOptions.builder() - .url("https://example.com") - .linkedBlockingQueueCapacity(1001) - .build(); + .url("https://example.com") + .linkedBlockingQueueCapacity(1001) + .build(); }); assertEquals("linkedBlockingQueueCapacity must be between 1 and 1000", exception.getMessage()); } @@ -159,9 +154,9 @@ public void testLinkedBlockingQueueCapacityOutOfRange() { public void testPollIntervalSecondsOutOfRange() { IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> { HttpConnectorOptions.builder() - .url("https://example.com") - .pollIntervalSeconds(700) - .build(); + .url("https://example.com") + .pollIntervalSeconds(700) + .build(); }); assertEquals("pollIntervalSeconds must be between 1 and 600", exception.getMessage()); } @@ -177,25 +172,28 @@ public void testAdditionalCustomValuesInitialization() { public void put(String key, String payload) { // do nothing } + @Override - public String get(String key) { return null; } + public String get(String key) { + return null; + } }; HttpConnectorOptions options = HttpConnectorOptions.builder() - .url("https://example.com") - .pollIntervalSeconds(120) - .connectTimeoutSeconds(20) - .requestTimeoutSeconds(30) - .linkedBlockingQueueCapacity(200) - .scheduledThreadPoolSize(4) - .headers(headers) - .httpClientExecutor(executorService) - .proxyHost("proxy.example.com") - .proxyPort(8080) - .payloadCacheOptions(cacheOptions) - .payloadCache(cache) - .useHttpCache(true) - .build(); + .url("https://example.com") + .pollIntervalSeconds(120) + .connectTimeoutSeconds(20) + .requestTimeoutSeconds(30) + .linkedBlockingQueueCapacity(200) + .scheduledThreadPoolSize(4) + .headers(headers) + .httpClientExecutor(executorService) + .proxyHost("proxy.example.com") + .proxyPort(8080) + .payloadCacheOptions(cacheOptions) + .payloadCache(cache) + .useHttpCache(true) + .build(); assertEquals(120, options.getPollIntervalSeconds().intValue()); assertEquals(20, options.getConnectTimeoutSeconds().intValue()); @@ -217,9 +215,9 @@ public void put(String key, String payload) { public void testRequestTimeoutSecondsOutOfRange() { IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> { HttpConnectorOptions.builder() - .url("https://example.com") - .requestTimeoutSeconds(61) - .build(); + .url("https://example.com") + .requestTimeoutSeconds(61) + .build(); }); assertEquals("requestTimeoutSeconds must be between 1 and 60", exception.getMessage()); } @@ -235,25 +233,28 @@ public void testBuilderInitializesAllFields() { public void put(String key, String payload) { // do nothing } + @Override - public String get(String key) { return null; } + public String get(String key) { + return null; + } }; HttpConnectorOptions options = HttpConnectorOptions.builder() - .pollIntervalSeconds(120) - .connectTimeoutSeconds(20) - .requestTimeoutSeconds(30) - .linkedBlockingQueueCapacity(200) - .scheduledThreadPoolSize(4) - .headers(headers) - .httpClientExecutor(executorService) - .proxyHost("proxy.example.com") - .proxyPort(8080) - .payloadCacheOptions(cacheOptions) - .payloadCache(cache) - .useHttpCache(true) - .url("https://example.com") - .build(); + .pollIntervalSeconds(120) + .connectTimeoutSeconds(20) + .requestTimeoutSeconds(30) + .linkedBlockingQueueCapacity(200) + .scheduledThreadPoolSize(4) + .headers(headers) + .httpClientExecutor(executorService) + .proxyHost("proxy.example.com") + .proxyPort(8080) + .payloadCacheOptions(cacheOptions) + .payloadCache(cache) + .useHttpCache(true) + .url("https://example.com") + .build(); assertEquals(120, options.getPollIntervalSeconds().intValue()); assertEquals(20, options.getConnectTimeoutSeconds().intValue()); @@ -274,9 +275,9 @@ public void put(String key, String payload) { public void testScheduledThreadPoolSizeOutOfRange() { IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> { HttpConnectorOptions.builder() - .url("https://example.com") - .scheduledThreadPoolSize(11) - .build(); + .url("https://example.com") + .scheduledThreadPoolSize(11) + .build(); }); assertEquals("scheduledThreadPoolSize must be between 1 and 10", exception.getMessage()); } @@ -285,10 +286,10 @@ public void testScheduledThreadPoolSizeOutOfRange() { public void testProxyPortOutOfRange() { IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> { HttpConnectorOptions.builder() - .url("https://example.com") - .proxyHost("proxy.example.com") - .proxyPort(70000) // Invalid port, out of range - .build(); + .url("https://example.com") + .proxyHost("proxy.example.com") + .proxyPort(70000) // Invalid port, out of range + .build(); }); assertEquals("proxyPort must be between 1 and 65535", exception.getMessage()); } @@ -297,17 +298,17 @@ public void testProxyPortOutOfRange() { public void testConnectTimeoutSecondsOutOfRange() { IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> { HttpConnectorOptions.builder() - .url("https://example.com") - .connectTimeoutSeconds(0) - .build(); + .url("https://example.com") + .connectTimeoutSeconds(0) + .build(); }); assertEquals("connectTimeoutSeconds must be between 1 and 60", exception.getMessage()); exception = assertThrows(IllegalArgumentException.class, () -> { HttpConnectorOptions.builder() - .url("https://example.com") - .connectTimeoutSeconds(61) - .build(); + .url("https://example.com") + .connectTimeoutSeconds(61) + .build(); }); assertEquals("connectTimeoutSeconds must be between 1 and 60", exception.getMessage()); } @@ -316,9 +317,9 @@ public void testConnectTimeoutSecondsOutOfRange() { public void testProxyPortWithoutProxyHost() { IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> { HttpConnectorOptions.builder() - .url("https://example.com") - .proxyPort(8080) - .build(); + .url("https://example.com") + .proxyPort(8080) + .build(); }); assertEquals("proxyHost must be set if proxyPort is set", exception.getMessage()); } @@ -326,20 +327,20 @@ public void testProxyPortWithoutProxyHost() { @Test public void testDefaultValuesWhenNullParametersProvided() { HttpConnectorOptions options = HttpConnectorOptions.builder() - .url("https://example.com") - .pollIntervalSeconds(null) - .linkedBlockingQueueCapacity(null) - .scheduledThreadPoolSize(null) - .requestTimeoutSeconds(null) - .connectTimeoutSeconds(null) - .headers(null) - .httpClientExecutor(null) - .proxyHost(null) - .proxyPort(null) - .payloadCacheOptions(null) - .payloadCache(null) - .useHttpCache(null) - .build(); + .url("https://example.com") + .pollIntervalSeconds(null) + .linkedBlockingQueueCapacity(null) + .scheduledThreadPoolSize(null) + .requestTimeoutSeconds(null) + .connectTimeoutSeconds(null) + .headers(null) + .httpClientExecutor(null) + .proxyHost(null) + .proxyPort(null) + .payloadCacheOptions(null) + .payloadCache(null) + .useHttpCache(null) + .build(); assertEquals(60, options.getPollIntervalSeconds().intValue()); assertEquals(10, options.getConnectTimeoutSeconds().intValue()); @@ -361,9 +362,9 @@ public void testDefaultValuesWhenNullParametersProvided() { public void testProxyHostWithoutProxyPort() { IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> { HttpConnectorOptions.builder() - .url("https://example.com") - .proxyHost("proxy.example.com") - .build(); + .url("https://example.com") + .proxyHost("proxy.example.com") + .build(); }); assertEquals("proxyPort must be set if proxyHost is set", exception.getMessage()); } @@ -384,9 +385,9 @@ public String get(String key) { IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> { HttpConnectorOptions.builder() - .url("https://example.com") - .payloadCache(mockPayloadCache) - .build(); + .url("https://example.com") + .payloadCache(mockPayloadCache) + .build(); }); assertEquals("payloadCacheOptions must be set if payloadCache is set", exception.getMessage()); @@ -396,9 +397,9 @@ public String get(String key) { public void testPayloadCacheOptionsWithoutPayloadCache() { IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> { HttpConnectorOptions.builder() - .url("https://example.com") - .payloadCacheOptions(PayloadCacheOptions.builder().build()) - .build(); + .url("https://example.com") + .payloadCacheOptions(PayloadCacheOptions.builder().build()) + .build(); }); assertEquals("payloadCache must be set if payloadCacheOptions is set", exception.getMessage()); } diff --git a/tools/flagd-http-connector/src/test/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/HttpConnectorTest.java b/tools/flagd-http-connector/src/test/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/HttpConnectorTest.java index 94be758f6..e5db49006 100644 --- a/tools/flagd-http-connector/src/test/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/HttpConnectorTest.java +++ b/tools/flagd-http-connector/src/test/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/HttpConnectorTest.java @@ -47,15 +47,15 @@ void testGetStreamQueueInitialAndScheduledPolls() { when(mockResponse.statusCode()).thenReturn(200); when(mockResponse.body()).thenReturn("test data"); when(mockClient.send(any(HttpRequest.class), any(HttpResponse.BodyHandler.class))) - .thenReturn(mockResponse); + .thenReturn(mockResponse); HttpConnectorOptions httpConnectorOptions = HttpConnectorOptions.builder() - .url(testUrl) - .httpClientExecutor(Executors.newSingleThreadExecutor()) - .build(); + .url(testUrl) + .httpClientExecutor(Executors.newSingleThreadExecutor()) + .build(); HttpConnector connector = HttpConnector.builder() - .httpConnectorOptions(httpConnectorOptions) - .build(); + .httpConnectorOptions(httpConnectorOptions) + .build(); Field clientField = HttpConnector.class.getDeclaredField("client"); clientField.setAccessible(true); @@ -81,10 +81,11 @@ void testBuildPollTaskFetchesDataAndAddsToQueue() { when(mockResponse.statusCode()).thenReturn(200); when(mockResponse.body()).thenReturn("test data"); when(mockClient.send(any(HttpRequest.class), any(HttpResponse.BodyHandler.class))) - .thenReturn(mockResponse); + .thenReturn(mockResponse); PayloadCache payloadCache = new PayloadCache() { private String payload; + @Override public void put(String key, String payload) { this.payload = payload; @@ -96,17 +97,17 @@ public String get(String key) { } }; HttpConnectorOptions httpConnectorOptions = HttpConnectorOptions.builder() - .url(testUrl) - .proxyHost("proxy-host") - .proxyPort(8080) - .useHttpCache(true) - .payloadCache(payloadCache) - .payloadCacheOptions(PayloadCacheOptions.builder().build()) - .useFailsafeCache(true) - .build(); + .url(testUrl) + .proxyHost("proxy-host") + .proxyPort(8080) + .useHttpCache(true) + .payloadCache(payloadCache) + .payloadCacheOptions(PayloadCacheOptions.builder().build()) + .useFailsafeCache(true) + .build(); HttpConnector connector = HttpConnector.builder() - .httpConnectorOptions(httpConnectorOptions) - .build(); + .httpConnectorOptions(httpConnectorOptions) + .build(); connector.init(); Field clientField = HttpConnector.class.getDeclaredField("client"); @@ -134,13 +135,11 @@ void testHttpRequestIncludesHeaders() { testHeaders.put("Authorization", "Bearer token"); testHeaders.put("Content-Type", "application/json"); - HttpConnectorOptions httpConnectorOptions = HttpConnectorOptions.builder() - .url(testUrl) - .headers(testHeaders) - .build(); + HttpConnectorOptions httpConnectorOptions = + HttpConnectorOptions.builder().url(testUrl).headers(testHeaders).build(); HttpConnector connector = HttpConnector.builder() - .httpConnectorOptions(httpConnectorOptions) - .build(); + .httpConnectorOptions(httpConnectorOptions) + .build(); Field headersField = HttpConnector.class.getDeclaredField("headers"); headersField.setAccessible(true); @@ -162,12 +161,11 @@ void testSuccessfulHttpResponseAddsDataToQueue() { when(mockClient.send(any(HttpRequest.class), any(HttpResponse.BodyHandler.class))) .thenReturn(mockResponse); - HttpConnectorOptions httpConnectorOptions = HttpConnectorOptions.builder() - .url(testUrl) - .build(); + HttpConnectorOptions httpConnectorOptions = + HttpConnectorOptions.builder().url(testUrl).build(); HttpConnector connector = HttpConnector.builder() - .httpConnectorOptions(httpConnectorOptions) - .build(); + .httpConnectorOptions(httpConnectorOptions) + .build(); Field clientField = HttpConnector.class.getDeclaredField("client"); clientField.setAccessible(true); @@ -190,7 +188,7 @@ void testInitFailureUsingCache() { HttpResponse mockResponse = mock(HttpResponse.class); when(mockResponse.statusCode()).thenReturn(200); when(mockClient.send(any(HttpRequest.class), any(HttpResponse.BodyHandler.class))) - .thenThrow(new IOException("Simulated IO Exception")); + .thenThrow(new IOException("Simulated IO Exception")); final String cachedData = "cached data"; PayloadCache payloadCache = new PayloadCache() { @@ -206,14 +204,14 @@ public String get(String key) { }; HttpConnectorOptions httpConnectorOptions = HttpConnectorOptions.builder() - .url(testUrl) - .payloadCache(payloadCache) - .payloadCacheOptions(PayloadCacheOptions.builder().build()) - .useFailsafeCache(true) - .build(); + .url(testUrl) + .payloadCache(payloadCache) + .payloadCacheOptions(PayloadCacheOptions.builder().build()) + .useFailsafeCache(true) + .build(); HttpConnector connector = HttpConnector.builder() - .httpConnectorOptions(httpConnectorOptions) - .build(); + .httpConnectorOptions(httpConnectorOptions) + .build(); Field clientField = HttpConnector.class.getDeclaredField("client"); clientField.setAccessible(true); @@ -234,12 +232,12 @@ void testQueueBecomesFull() { String testUrl = "http://example.com"; int queueCapacity = 1; HttpConnectorOptions httpConnectorOptions = HttpConnectorOptions.builder() - .url(testUrl) - .linkedBlockingQueueCapacity(queueCapacity) - .build(); + .url(testUrl) + .linkedBlockingQueueCapacity(queueCapacity) + .build(); HttpConnector connector = HttpConnector.builder() - .httpConnectorOptions(httpConnectorOptions) - .build(); + .httpConnectorOptions(httpConnectorOptions) + .build(); BlockingQueue queue = connector.getStreamQueue(); @@ -258,12 +256,12 @@ void testShutdownProperlyTerminatesSchedulerAndHttpClientExecutor() throws Inter String testUrl = "http://example.com"; HttpConnectorOptions httpConnectorOptions = HttpConnectorOptions.builder() - .url(testUrl) - .httpClientExecutor(mockHttpClientExecutor) - .build(); + .url(testUrl) + .httpClientExecutor(mockHttpClientExecutor) + .build(); HttpConnector connector = HttpConnector.builder() - .httpConnectorOptions(httpConnectorOptions) - .build(); + .httpConnectorOptions(httpConnectorOptions) + .build(); Field schedulerField = HttpConnector.class.getDeclaredField("scheduler"); schedulerField.setAccessible(true); @@ -283,14 +281,13 @@ void testHttpResponseNonSuccessStatusCode() { HttpResponse mockResponse = mock(HttpResponse.class); when(mockResponse.statusCode()).thenReturn(404); when(mockClient.send(any(HttpRequest.class), any(HttpResponse.BodyHandler.class))) - .thenReturn(mockResponse); + .thenReturn(mockResponse); - HttpConnectorOptions httpConnectorOptions = HttpConnectorOptions.builder() - .url(testUrl) - .build(); + HttpConnectorOptions httpConnectorOptions = + HttpConnectorOptions.builder().url(testUrl).build(); HttpConnector connector = HttpConnector.builder() - .httpConnectorOptions(httpConnectorOptions) - .build(); + .httpConnectorOptions(httpConnectorOptions) + .build(); Field clientField = HttpConnector.class.getDeclaredField("client"); clientField.setAccessible(true); @@ -306,19 +303,18 @@ void testHttpResponseNonSuccessStatusCode() { void testHttpRequestFailsWithException() { String testUrl = "http://example.com"; HttpClient mockClient = mock(HttpClient.class); - HttpConnectorOptions httpConnectorOptions = HttpConnectorOptions.builder() - .url(testUrl) - .build(); + HttpConnectorOptions httpConnectorOptions = + HttpConnectorOptions.builder().url(testUrl).build(); HttpConnector connector = HttpConnector.builder() - .httpConnectorOptions(httpConnectorOptions) - .build(); + .httpConnectorOptions(httpConnectorOptions) + .build(); Field clientField = HttpConnector.class.getDeclaredField("client"); clientField.setAccessible(true); clientField.set(connector, mockClient); when(mockClient.send(any(HttpRequest.class), any(HttpResponse.BodyHandler.class))) - .thenThrow(new RuntimeException("Test exception")); + .thenThrow(new RuntimeException("Test exception")); BlockingQueue queue = connector.getStreamQueue(); @@ -330,19 +326,18 @@ void testHttpRequestFailsWithException() { void testHttpRequestFailsWithIoexception() { String testUrl = "http://example.com"; HttpClient mockClient = mock(HttpClient.class); - HttpConnectorOptions httpConnectorOptions = HttpConnectorOptions.builder() - .url(testUrl) - .build(); + HttpConnectorOptions httpConnectorOptions = + HttpConnectorOptions.builder().url(testUrl).build(); HttpConnector connector = HttpConnector.builder() - .httpConnectorOptions(httpConnectorOptions) - .build(); + .httpConnectorOptions(httpConnectorOptions) + .build(); Field clientField = HttpConnector.class.getDeclaredField("client"); clientField.setAccessible(true); clientField.set(connector, mockClient); when(mockClient.send(any(HttpRequest.class), any(HttpResponse.BodyHandler.class))) - .thenThrow(new IOException("Simulated IO Exception")); + .thenThrow(new IOException("Simulated IO Exception")); connector.getStreamQueue(); @@ -360,12 +355,11 @@ void testScheduledPollingContinuesAtFixedIntervals() { when(mockResponse.statusCode()).thenReturn(200); when(mockResponse.body()).thenReturn("test data"); - HttpConnectorOptions httpConnectorOptions = HttpConnectorOptions.builder() - .url(testUrl) - .build(); + HttpConnectorOptions httpConnectorOptions = + HttpConnectorOptions.builder().url(testUrl).build(); HttpConnector connector = spy(HttpConnector.builder() - .httpConnectorOptions(httpConnectorOptions) - .build()); + .httpConnectorOptions(httpConnectorOptions) + .build()); doReturn(mockResponse).when(connector).execute(any()); @@ -391,14 +385,13 @@ void testQueuePayloadTypeSetToDataOnSuccess() { when(mockResponse.statusCode()).thenReturn(200); when(mockResponse.body()).thenReturn("response body"); when(mockClient.send(any(HttpRequest.class), any(HttpResponse.BodyHandler.class))) - .thenReturn(mockResponse); + .thenReturn(mockResponse); - HttpConnectorOptions httpConnectorOptions = HttpConnectorOptions.builder() - .url(testUrl) - .build(); + HttpConnectorOptions httpConnectorOptions = + HttpConnectorOptions.builder().url(testUrl).build(); HttpConnector connector = HttpConnector.builder() - .httpConnectorOptions(httpConnectorOptions) - .build(); + .httpConnectorOptions(httpConnectorOptions) + .build(); Field clientField = HttpConnector.class.getDeclaredField("client"); clientField.setAccessible(true); @@ -417,6 +410,7 @@ public void testSecondFetchFromCache() throws Exception { // Mock PayloadCache PayloadCache payloadCache = new PayloadCache() { private String payload; + @Override public void put(String key, String payload) { this.payload = payload; @@ -434,22 +428,21 @@ public String get(String key) { }; PayloadCache mockPayloadCache = spy(payloadCache); when(mockPayloadCache.get(HttpConnector.POLLING_PAYLOAD_CACHE_KEY)) - .thenReturn(null) // First fetch: cache miss - .thenReturn("cached payload"); // Second fetch: cache hit + .thenReturn(null) // First fetch: cache miss + .thenReturn("cached payload"); // Second fetch: cache hit // Configure HttpConnectorOptions with usePollingCache = true HttpConnectorOptions options = HttpConnectorOptions.builder() - .url("http://example.com") - .pollIntervalSeconds(10) - .usePollingCache(true) - .payloadCache(mockPayloadCache) - .payloadCacheOptions(PayloadCacheOptions.builder().build()) - .build(); + .url("http://example.com") + .pollIntervalSeconds(10) + .usePollingCache(true) + .payloadCache(mockPayloadCache) + .payloadCacheOptions(PayloadCacheOptions.builder().build()) + .build(); // Create HttpConnector instance - HttpConnector connector = HttpConnector.builder() - .httpConnectorOptions(options) - .build(); + HttpConnector connector = + HttpConnector.builder().httpConnectorOptions(options).build(); // Initialize the connector BlockingQueue queue = connector.getStreamQueue(); @@ -472,15 +465,13 @@ public String get(String key) { @Test public void providerTest() { - HttpConnectorOptions httpConnectorOptions = HttpConnectorOptions.builder() - .url("http://example.com") - .build(); + HttpConnectorOptions httpConnectorOptions = + HttpConnectorOptions.builder().url("http://example.com").build(); HttpConnector connector = HttpConnector.builder() - .httpConnectorOptions(httpConnectorOptions) - .build(); + .httpConnectorOptions(httpConnectorOptions) + .build(); - FlagdOptions options = - FlagdOptions.builder() + FlagdOptions options = FlagdOptions.builder() .resolverType(Config.Resolver.IN_PROCESS) .customConnector(connector) .build(); @@ -494,5 +485,4 @@ public void providerTest() { protected static void delay(long ms) { Thread.sleep(ms); } - } From 694ee51c23396ae76d1746c51abdd862722abecc Mon Sep 17 00:00:00 2001 From: liran2000 Date: Sat, 10 May 2025 09:19:32 +0300 Subject: [PATCH 35/40] updates Signed-off-by: liran2000 --- tools/flagd-http-connector/CHANGELOG.md | 34 ---- tools/flagd-http-connector/README.md | 59 ++++++- .../src/test/resources/testing-flags.json | 156 ++++++++++++++++++ 3 files changed, 213 insertions(+), 36 deletions(-) create mode 100644 tools/flagd-http-connector/src/test/resources/testing-flags.json diff --git a/tools/flagd-http-connector/CHANGELOG.md b/tools/flagd-http-connector/CHANGELOG.md index 069e8ebdc..825c32f0d 100644 --- a/tools/flagd-http-connector/CHANGELOG.md +++ b/tools/flagd-http-connector/CHANGELOG.md @@ -1,35 +1 @@ # Changelog - -## [0.1.2](https://github.com/open-feature/java-sdk-contrib/compare/dev.openfeature.contrib.tools.junitopenfeature-v0.1.1...dev.openfeature.contrib.tools.junitopenfeature-v0.1.2) (2024-12-03) - - -### ✨ New Features - -* added interception of parameterized tests to Junit OpenFeature Extension ([#1093](https://github.com/open-feature/java-sdk-contrib/issues/1093)) ([a78c906](https://github.com/open-feature/java-sdk-contrib/commit/a78c906b24b53f7d25eb01aad85ed614eb30ca05)) - -## [0.1.1](https://github.com/open-feature/java-sdk-contrib/compare/dev.openfeature.contrib.tools.junitopenfeature-v0.1.0...dev.openfeature.contrib.tools.junitopenfeature-v0.1.1) (2024-09-27) - - -### 🐛 Bug Fixes - -* race condition causing default when multiple flags are used ([#983](https://github.com/open-feature/java-sdk-contrib/issues/983)) ([356a973](https://github.com/open-feature/java-sdk-contrib/commit/356a973cf2b6ddf82b8311ea200fa30df4f1d048)) - -## [0.1.0](https://github.com/open-feature/java-sdk-contrib/compare/dev.openfeature.contrib.tools.junitopenfeature-v0.0.3...dev.openfeature.contrib.tools.junitopenfeature-v0.1.0) (2024-09-25) - - -### 🐛 Bug Fixes - -* **deps:** update dependency org.apache.commons:commons-lang3 to v3.17.0 ([#932](https://github.com/open-feature/java-sdk-contrib/issues/932)) ([c598d9f](https://github.com/open-feature/java-sdk-contrib/commit/c598d9f0a61f2324fb85d72fdfea34811283c575)) - - -### 🐛 Bug Fixes - -* added missing dependency and installation instruction ([#895](https://github.com/open-feature/java-sdk-contrib/issues/895)) ([6748d02](https://github.com/open-feature/java-sdk-contrib/commit/6748d02403f0ceecb6cb9ecdfb2fecf98423a7db)) -* **deps:** update dependency org.apache.commons:commons-lang3 to v3.16.0 ([#908](https://github.com/open-feature/java-sdk-contrib/issues/908)) ([d21cfe3](https://github.com/open-feature/java-sdk-contrib/commit/d21cfe3ac7da1ff6e1a4dc2ee4b0db5c24ed4847)) - -## [0.0.2](https://github.com/open-feature/java-sdk-contrib/compare/dev.openfeature.contrib.tools.junitopenfeature-v0.0.1...dev.openfeature.contrib.tools.junitopenfeature-v0.0.2) (2024-07-29) - - -### ✨ New Features - -* Add JUnit5 extension for OpenFeature ([#888](https://github.com/open-feature/java-sdk-contrib/issues/888)) ([9fff9db](https://github.com/open-feature/java-sdk-contrib/commit/9fff9db4bcee3c3ae8128a1b2fb040f53df1d5ed)) diff --git a/tools/flagd-http-connector/README.md b/tools/flagd-http-connector/README.md index 59a545730..506decf65 100644 --- a/tools/flagd-http-connector/README.md +++ b/tools/flagd-http-connector/README.md @@ -36,7 +36,27 @@ URL, effectively acting as a flagd/proxy while all other services leverage the s This approach optimizes resource usage by preventing redundant polling across services. ### Sample flow demonstrating the architecture -This example demonstrates an architectural flow using: + +#### Basic Simple Configuration + +This example demonstrates a simple flow using: +- GitHub as the source for flag payload. + +```mermaid +sequenceDiagram + participant service + participant Github + + service->>Github: fetch + Github->>service: payload + Note right of service: polling interval passed + service->>Github: fetch + Github->>service: payload +``` + +#### Configuration with fail-safe cache and polling cache + +This example demonstrates a micro-services architectural flow using: - GitHub as the source for flag payload. - Redis serving as both the fail-safe initialization cache and the polling cache. @@ -85,7 +105,6 @@ sequenceDiagram ### Usage example ```java - HttpConnectorOptions httpConnectorOptions = HttpConnectorOptions.builder() .url("http://example.com/flags") .build(); @@ -102,6 +121,42 @@ FlagdOptions options = FlagdProvider flagdProvider = new FlagdProvider(options); ``` +#### HttpConnector using fail-safe cache and polling cache + +```java +PayloadCache payloadCache = new PayloadCache() { + + @Override + public void put(String key, String payload) { + // implement put in cache + } + + @Override + public void put(String key, String payload, int ttlSeconds) { + // implement put in cache with TTL + } + + @Override + public String get(String key) { + // implement get from cache and return + } +}; + +HttpConnectorOptions httpConnectorOptions = HttpConnectorOptions.builder() + .url(testUrl) + .useHttpCache(true) + .payloadCache(payloadCache) + .payloadCacheOptions(PayloadCacheOptions.builder().build()) + .useFailsafeCache(true) + .pollIntervalSeconds(10) + .usePollingCache(true) + .build(); + +HttpConnector connector = HttpConnector.builder() + .httpConnectorOptions(httpConnectorOptions) + .build(); +``` + ### Configuration The Http Connector can be configured using the following properties in the `HttpConnectorOptions` class.: diff --git a/tools/flagd-http-connector/src/test/resources/testing-flags.json b/tools/flagd-http-connector/src/test/resources/testing-flags.json new file mode 100644 index 000000000..fb0ebd102 --- /dev/null +++ b/tools/flagd-http-connector/src/test/resources/testing-flags.json @@ -0,0 +1,156 @@ +{ + "flags": { + "boolean-flag": { + "state": "ENABLED", + "variants": { + "on": true, + "off": false + }, + "defaultVariant": "on" + }, + "string-flag": { + "state": "ENABLED", + "variants": { + "greeting": "hi", + "parting": "bye" + }, + "defaultVariant": "greeting" + }, + "integer-flag": { + "state": "ENABLED", + "variants": { + "one": 1, + "ten": 10 + }, + "defaultVariant": "ten" + }, + "float-flag": { + "state": "ENABLED", + "variants": { + "tenth": 0.1, + "half": 0.5 + }, + "defaultVariant": "half" + }, + "object-flag": { + "state": "ENABLED", + "variants": { + "empty": {}, + "template": { + "showImages": true, + "title": "Check out these pics!", + "imagesPerPage": 100 + } + }, + "defaultVariant": "template" + }, + "context-aware": { + "state": "ENABLED", + "variants": { + "internal": "INTERNAL", + "external": "EXTERNAL" + }, + "defaultVariant": "external", + "targeting": { + "if": [ + { + "and": [ + { + "==": [ + { + "var": [ + "fn" + ] + }, + "Sulisław" + ] + }, + { + "==": [ + { + "var": [ + "ln" + ] + }, + "Świętopełk" + ] + }, + { + "==": [ + { + "var": [ + "age" + ] + }, + 29 + ] + }, + { + "==": [ + { + "var": [ + "customer" + ] + }, + false + ] + } + ] + }, + "internal", + "external" + ] + } + }, + "timestamp-flag": { + "state": "ENABLED", + "variants": { + "past": -1, + "future": 1, + "none": 0 + }, + "defaultVariant": "none", + "targeting": { + "if": [ + { + ">": [ { "var": "$flagd.timestamp" }, { "var": "time" } ] + }, + "past", + { + "if": [ + { + "<": [ { "var": "$flagd.timestamp" }, { "var": "time" } ] + }, + "future", "none" + ] + } + ] + } + }, + "wrong-flag": { + "state": "ENABLED", + "variants": { + "one": "uno", + "two": "dos" + }, + "defaultVariant": "one" + }, + "targeting-key-flag": { + "state": "ENABLED", + "variants": { + "miss": "miss", + "hit": "hit" + }, + "defaultVariant": "miss", + "targeting": { + "if": [ + { + "==": [ { "var": "targetingKey" }, "5c3d8535-f81a-4478-a6d3-afaa4d51199e" ] + }, + "hit", + null + ] + } + } + } +} From e1686224f4a780b9d556a36bb92ee2a6d4bb220a Mon Sep 17 00:00:00 2001 From: liran2000 Date: Sat, 10 May 2025 09:26:39 +0300 Subject: [PATCH 36/40] updates Signed-off-by: liran2000 --- .../connector/sync/http/HttpConnectorIntegrationTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/flagd-http-connector/src/test/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/HttpConnectorIntegrationTest.java b/tools/flagd-http-connector/src/test/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/HttpConnectorIntegrationTest.java index 36d61c1cb..cce19fdce 100644 --- a/tools/flagd-http-connector/src/test/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/HttpConnectorIntegrationTest.java +++ b/tools/flagd-http-connector/src/test/java/dev/openfeature/contrib/tools/flagd/resolver/process/storage/connector/sync/http/HttpConnectorIntegrationTest.java @@ -29,7 +29,7 @@ void testGithubRawContent() { HttpConnector connector = null; try { String testUrl = - "https://raw.githubusercontent.com/open-feature/java-sdk-contrib/58fe5da7d4e2f6f4ae2c1caf3411a01e84a1dc1a/providers/flagd/version.txt"; + "https://raw.githubusercontent.com/open-feature/java-sdk-contrib/main/tools/flagd-http-connector/src/test/resources/testing-flags.json"; HttpConnectorOptions httpConnectorOptions = HttpConnectorOptions.builder() .url(testUrl) From f37b55e2c81499becac2446e5e65f42d11646827 Mon Sep 17 00:00:00 2001 From: liran2000 Date: Sat, 10 May 2025 09:31:06 +0300 Subject: [PATCH 37/40] updates Signed-off-by: liran2000 --- tools/flagd-http-connector/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/flagd-http-connector/README.md b/tools/flagd-http-connector/README.md index 506decf65..bf9904c87 100644 --- a/tools/flagd-http-connector/README.md +++ b/tools/flagd-http-connector/README.md @@ -35,7 +35,7 @@ A key advantage of this cache is that it enables a single microservice within a URL, effectively acting as a flagd/proxy while all other services leverage the shared cache. This approach optimizes resource usage by preventing redundant polling across services. -### Sample flow demonstrating the architecture +### Sample flows demonstrating the architecture #### Basic Simple Configuration From 0f4b6395803bdde70e647c17b3945f0197320ee1 Mon Sep 17 00:00:00 2001 From: liran2000 Date: Mon, 12 May 2025 11:33:27 +0300 Subject: [PATCH 38/40] update readme Signed-off-by: liran2000 --- tools/flagd-http-connector/README.md | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/tools/flagd-http-connector/README.md b/tools/flagd-http-connector/README.md index bf9904c87..6d738de58 100644 --- a/tools/flagd-http-connector/README.md +++ b/tools/flagd-http-connector/README.md @@ -23,10 +23,11 @@ The implementation is using Java HttpClient. ### What happens if the Http source is down during application startup? -Http Connector supports optional fail-safe initialization using a cache. +Http Connector supports optional resilient fail-safe initialization using a cache. If the initial fetch fails due to source unavailability, it can load the initial payload from the cache instead of falling back to default values. -This ensures smoother startup behavior until the source becomes available again. To be effective, the TTL of the fallback cache should be longer than the expected duration of the source downtime during initialization. +This ensures smoother startup behavior until the source becomes available again. To be effective, the TTL of the +fallback cache should be longer than the expected duration of the source downtime during initialization. ### Polling cache The polling cache is used to store the payload fetched from the URL. @@ -54,7 +55,10 @@ sequenceDiagram Github->>service: payload ``` -#### Configuration with fail-safe cache and polling cache +#### A More Scalable Configuration Utilizing Fail-Safe and Polling Caching Mechanisms + +This configuration aim to reduce network requests to the source URL, to improve performance and to improve the +application's resilience to source downtime. This example demonstrates a micro-services architectural flow using: - GitHub as the source for flag payload. @@ -62,7 +66,9 @@ This example demonstrates a micro-services architectural flow using: Example initialization flow during GitHub downtime, demonstrates how the application continues to access flag values from the cache even when GitHub is unavailable. -In this setup, multiple microservices share the same cache, with only one service responsible for polling the source URL. +In this setup, multiple microservices share the same cache, with only one service responsible for polling the source +URL. + ```mermaid sequenceDiagram box Cluster From 0e7a171063374f242098e82efcff09aa2d8008a9 Mon Sep 17 00:00:00 2001 From: liran2000 Date: Tue, 13 May 2025 09:00:14 +0300 Subject: [PATCH 39/40] revert flagd provider changes Signed-off-by: liran2000 --- providers/flagd/src/main/resources/simplelogger.properties | 2 +- .../src/test/resources/simplelogger.properties | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/providers/flagd/src/main/resources/simplelogger.properties b/providers/flagd/src/main/resources/simplelogger.properties index 80c478930..d9d489e82 100644 --- a/providers/flagd/src/main/resources/simplelogger.properties +++ b/providers/flagd/src/main/resources/simplelogger.properties @@ -1,3 +1,3 @@ -org.slf4j.simpleLogger.defaultLogLevel=debug +org.org.slf4j.simpleLogger.defaultLogLevel=debug io.grpc.level=trace diff --git a/tools/flagd-http-connector/src/test/resources/simplelogger.properties b/tools/flagd-http-connector/src/test/resources/simplelogger.properties index 50b5bd1bd..5d813a29e 100644 --- a/tools/flagd-http-connector/src/test/resources/simplelogger.properties +++ b/tools/flagd-http-connector/src/test/resources/simplelogger.properties @@ -1,2 +1,3 @@ -org.slf4j.simpleLogger.defaultLogLevel=info +org.slf4j.simpleLogger.defaultLogLevel=debug +org.slf4j.simpleLogger.logFile=System.out io.grpc.level=info From bd8d1fc772ec8424f61f5eddf0a6783e863f187e Mon Sep 17 00:00:00 2001 From: liran2000 Date: Tue, 13 May 2025 09:01:02 +0300 Subject: [PATCH 40/40] revert flagd provider changes Signed-off-by: liran2000 --- providers/flagd/src/test/resources/simplelogger.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/providers/flagd/src/test/resources/simplelogger.properties b/providers/flagd/src/test/resources/simplelogger.properties index 769e4e8bf..d2ca1bbdc 100644 --- a/providers/flagd/src/test/resources/simplelogger.properties +++ b/providers/flagd/src/test/resources/simplelogger.properties @@ -1,4 +1,4 @@ -org.slf4j.simpleLogger.defaultLogLevel=debug +org.org.slf4j.simpleLogger.defaultLogLevel=debug org.slf4j.simpleLogger.showDateTime= io.grpc.level=trace