Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 23 additions & 22 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -135,28 +135,29 @@ switcher.poolsize=2

### Configuration Properties Reference

| Property | Required | Default | Description |
|-------------------------------------|----------|---------|--------------------------------------------------------------------------------------|
| `switcher.context` | ✅ | - | Fully qualified class name extending SwitcherContext |
| `switcher.url` | ✅ | - | Switcher API endpoint URL |
| `switcher.apikey` | ✅ | - | API key for authentication |
| `switcher.component` | ✅ | - | Your application/component identifier |
| `switcher.domain` | ✅ | - | Domain name in Switcher API |
| `switcher.environment` | ❌ | default | Environment name (dev, staging, default) |
| `switcher.local` | ❌ | false | Enable local-only mode |
| `switcher.check` | ❌ | false | Validate switcher keys on startup |
| `switcher.relay.restrict` | ❌ | true | Defines if client will trigger local snapshot relay verification |
| `switcher.snapshot.location` | ❌ | - | Directory for snapshot files |
| `switcher.snapshot.auto` | ❌ | false | Auto-load snapshots on startup |
| `switcher.snapshot.skipvalidation` | ❌ | false | Skip snapshot validation on load |
| `switcher.snapshot.updateinterval` | ❌ | - | Interval for automatic snapshot updates (e.g., "5s", "2m") |
| `switcher.snapshot.watcher` | ❌ | false | Monitor snapshot files for changes |
| `switcher.silent` | ❌ | - | Enable silent mode (e.g., "5s", "2m") |
| `switcher.timeout` | ❌ | 3000 | API timeout in milliseconds |
| `switcher.poolsize` | ❌ | 2 | Thread pool size for API calls |
| `switcher.regextimeout` (v1-only) | ❌ | 3000 | Time in ms given to Timed Match Worker used for local Regex (ReDoS safety mechanism) |
| `switcher.truststore.path` | ❌ | - | Path to custom truststore file |
| `switcher.truststore.password` | ❌ | - | Password for custom truststore |
| Property | Required | Default | Description |
|------------------------------------|----------|---------|--------------------------------------------------------------------------------------|
| `switcher.context` | ✅ | - | Fully qualified class name extending SwitcherContext |
| `switcher.url` | ✅ | - | Switcher API endpoint URL |
| `switcher.apikey` | ✅ | - | API key for authentication |
| `switcher.component` | ✅ | - | Your application/component identifier |
| `switcher.domain` | ✅ | - | Domain name in Switcher API |
| `switcher.environment` | ❌ | default | Environment name (dev, staging, default) |
| `switcher.local` | ❌ | false | Enable local-only mode |
| `switcher.check` | ❌ | false | Validate switcher keys on startup |
| `switcher.autorefreshtoken` | ❌ | false | Automatically refresh API token before expiration |
| `switcher.relay.restrict` | ❌ | true | Defines if client will trigger local snapshot relay verification |
| `switcher.snapshot.location` | ❌ | - | Directory for snapshot files |
| `switcher.snapshot.auto` | ❌ | false | Auto-load snapshots on startup |
| `switcher.snapshot.skipvalidation` | ❌ | false | Skip snapshot validation on load |
| `switcher.snapshot.updateinterval` | ❌ | - | Interval for automatic snapshot updates (e.g., "5s", "2m") |
| `switcher.snapshot.watcher` | ❌ | false | Monitor snapshot files for changes |
| `switcher.silent` | ❌ | - | Enable silent mode (e.g., "5s", "2m") |
| `switcher.timeout` | ❌ | 3000 | API timeout in milliseconds |
| `switcher.poolsize` | ❌ | 2 | Thread pool size for API calls |
| `switcher.regextimeout` (v1-only) | ❌ | 3000 | Time in ms given to Timed Match Worker used for local Regex (ReDoS safety mechanism) |
| `switcher.truststore.path` | ❌ | - | Path to custom truststore file |
| `switcher.truststore.password` | ❌ | - | Password for custom truststore |

> 💡 **Environment Variables**: Use `${ENV_VAR:default_value}` syntax for environment variable substitution.

Expand Down
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
<groupId>com.switcherapi</groupId>
<artifactId>switcher-client</artifactId>
<packaging>jar</packaging>
<version>2.5.3-SNAPSHOT</version>
<version>2.6.0-SNAPSHOT</version>

<name>Switcher Client</name>
<description>Switcher Client SDK for working with Switcher API</description>
Expand Down
5 changes: 5 additions & 0 deletions src/main/java/com/switcherapi/client/ContextBuilder.java
Original file line number Diff line number Diff line change
Expand Up @@ -234,4 +234,9 @@ public ContextBuilder poolConnectionSize(Integer poolSize) {
Optional.ofNullable(poolSize).orElse(DEFAULT_POOL_SIZE));
return this;
}

public ContextBuilder autoRefreshToken(boolean autoRefreshToken) {
switcherProperties.setValue(ContextKey.AUTO_REFRESH_TOKEN, autoRefreshToken);
return this;
}
}
6 changes: 6 additions & 0 deletions src/main/java/com/switcherapi/client/SwitcherConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ abstract class SwitcherConfig {

protected boolean local;
protected boolean check;
protected boolean autoRefreshToken;
protected String silent;
protected Integer timeout;
protected Integer poolSize;
Expand All @@ -38,6 +39,7 @@ protected void updateSwitcherConfig(SwitcherProperties properties) {
setEnvironment(properties.getValue(ContextKey.ENVIRONMENT));
setLocal(properties.getBoolean(ContextKey.LOCAL_MODE));
setCheck(properties.getBoolean(ContextKey.CHECK_SWITCHERS));
setAutoRefreshToken(properties.getBoolean(ContextKey.AUTO_REFRESH_TOKEN));
setSilent(properties.getValue(ContextKey.SILENT_MODE));
setTimeout(properties.getInt(ContextKey.TIMEOUT_MS));
setPoolSize(properties.getInt(ContextKey.POOL_CONNECTION_SIZE));
Expand Down Expand Up @@ -105,6 +107,10 @@ public void setCheck(boolean check) {
this.check = check;
}

public void setAutoRefreshToken(boolean autoRefreshToken) {
this.autoRefreshToken = autoRefreshToken;
}

public void setSilent(String silent) {
this.silent = silent;
}
Expand Down
45 changes: 36 additions & 9 deletions src/main/java/com/switcherapi/client/SwitcherContextBase.java
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,8 @@ public abstract class SwitcherContextBase extends SwitcherConfig {
protected static Set<String> switcherKeys;
protected static Map<String, SwitcherRequest> switchers;
protected static SwitcherExecutor switcherExecutor;
private static ScheduledExecutorService scheduledExecutorService;
private static ScheduledExecutorService scheduledSnapshotExecutorService;
private static ScheduledExecutorService scheduledTokenExecutorService;
private static ExecutorService watcherExecutorService;
private static SnapshotWatcher watcherSnapshot;
protected static SwitcherContextBase contextBase;
Expand All @@ -111,6 +112,7 @@ protected void configureClient() {
.restrictRelay(relay.isRestrict())
.silentMode(silent)
.timeoutMs(timeout)
.autoRefreshToken(autoRefreshToken)
.poolConnectionSize(poolSize)
.snapshotLocation(snapshot.getLocation())
.snapshotAutoLoad(snapshot.isAuto())
Expand Down Expand Up @@ -197,9 +199,12 @@ public static void initializeClient() {
* @return SwitcherExecutor instance
*/
private static SwitcherExecutor buildInstance() {
initTokenExecutorService();

final ClientWS clientWS = initRemotePoolExecutorService();
final SwitcherValidator validatorService = new ValidatorService();
final ClientRemote clientRemote = new ClientRemoteService(clientWS, switcherProperties);
final ClientRemote clientRemote = new ClientRemoteService(
clientWS, switcherProperties, scheduledTokenExecutorService);
final ClientLocal clientLocal = new ClientLocalService(validatorService);

if (contextBol(ContextKey.LOCAL_MODE)) {
Expand Down Expand Up @@ -306,7 +311,7 @@ public static ScheduledFuture<?> scheduleSnapshotAutoUpdate(String intervalValue
return null;
}

if (Objects.nonNull(scheduledExecutorService)) {
if (Objects.nonNull(scheduledSnapshotExecutorService)) {
terminateSnapshotAutoUpdateWorker();
}

Expand All @@ -324,7 +329,7 @@ public static ScheduledFuture<?> scheduleSnapshotAutoUpdate(String intervalValue
};

initSnapshotExecutorService();
return scheduledExecutorService.scheduleAtFixedRate(runnableSnapshotValidate, 0, interval, TimeUnit.MILLISECONDS);
return scheduledSnapshotExecutorService.scheduleAtFixedRate(runnableSnapshotValidate, 0, interval, TimeUnit.MILLISECONDS);
}

/**
Expand All @@ -339,17 +344,29 @@ public static ScheduledFuture<?> scheduleSnapshotAutoUpdate(String intervalValue
}

/**
* Configure Executor Service for Snapshot Update Worker
* Configure Scheduled Executor Service for Snapshot Update Worker
*/
private static void initSnapshotExecutorService() {
scheduledExecutorService = Executors.newSingleThreadScheduledExecutor(r -> {
scheduledSnapshotExecutorService = Executors.newSingleThreadScheduledExecutor(r -> {
Thread thread = new Thread(r);
thread.setName(WorkerName.SNAPSHOT_UPDATE_WORKER.toString());
thread.setDaemon(true);
return thread;
});
}

/**
* Configure Scheduled Executor Service for Token Refresh Worker
*/
private static void initTokenExecutorService() {
scheduledTokenExecutorService = Executors.newSingleThreadScheduledExecutor(r -> {
Thread thread = new Thread(r);
thread.setName(WorkerName.SWITCHER_TOKEN_WORKER.toString());
thread.setDaemon(true);
return thread;
});
}

/**
* Configure Executor Service for Snapshot Watch Worker
*/
Expand Down Expand Up @@ -538,9 +555,19 @@ public static void configure(ContextBuilder builder) {
* Cancel existing scheduled task for updating local Snapshot
*/
public static void terminateSnapshotAutoUpdateWorker() {
if (Objects.nonNull(scheduledExecutorService)) {
scheduledExecutorService.shutdownNow();
scheduledExecutorService = null;
if (Objects.nonNull(scheduledSnapshotExecutorService)) {
scheduledSnapshotExecutorService.shutdownNow();
scheduledSnapshotExecutorService = null;
}
}

/**
* Cancel existing scheduled task for token refresh
*/
public static void terminateTokenRefreshWorker() {
if (Objects.nonNull(scheduledTokenExecutorService)) {
scheduledTokenExecutorService.shutdownNow();
scheduledTokenExecutorService = null;
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ private void initDefaults() {
setValue(ContextKey.LOCAL_MODE, false);
setValue(ContextKey.CHECK_SWITCHERS, false);
setValue(ContextKey.RESTRICT_RELAY, true);
setValue(ContextKey.AUTO_REFRESH_TOKEN, false);
}

@Override
Expand All @@ -49,6 +50,7 @@ public void loadFromProperties(Properties prop) {
setValue(ContextKey.LOCAL_MODE, getBoolDefault(resolveProperties(ContextKey.LOCAL_MODE.getParam(), prop), false));
setValue(ContextKey.CHECK_SWITCHERS, getBoolDefault(resolveProperties(ContextKey.CHECK_SWITCHERS.getParam(), prop), false));
setValue(ContextKey.RESTRICT_RELAY, getBoolDefault(resolveProperties(ContextKey.RESTRICT_RELAY.getParam(), prop), true));
setValue(ContextKey.AUTO_REFRESH_TOKEN, getBoolDefault(resolveProperties(ContextKey.AUTO_REFRESH_TOKEN.getParam(), prop), false));
setValue(ContextKey.REGEX_TIMEOUT, getIntDefault(resolveProperties(ContextKey.REGEX_TIMEOUT.getParam(), prop), DEFAULT_REGEX_TIMEOUT));
setValue(ContextKey.TRUSTSTORE_PATH, resolveProperties(ContextKey.TRUSTSTORE_PATH.getParam(), prop));
setValue(ContextKey.TRUSTSTORE_PASSWORD, resolveProperties(ContextKey.TRUSTSTORE_PASSWORD.getParam(), prop));
Expand Down
9 changes: 7 additions & 2 deletions src/main/java/com/switcherapi/client/model/ContextKey.java
Original file line number Diff line number Diff line change
Expand Up @@ -109,8 +109,13 @@ public enum ContextKey {
/**
* (Number) Defines a fixed number of threads for the pool connection (default is 2).
*/
POOL_CONNECTION_SIZE("switcher.poolsize");

POOL_CONNECTION_SIZE("switcher.poolsize"),

/**
* (boolean) Enables automatic refresh of authentication token (default is false)
*/
AUTO_REFRESH_TOKEN("switcher.autorefreshtoken");

private final String param;

ContextKey(String param) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ public boolean isExpired() {
return (this.exp * 1000) < System.currentTimeMillis();
}

public long getExp() {
return this.exp;
}

@Override
public String toString() {
return "AuthResponse{" +
Expand Down
3 changes: 2 additions & 1 deletion src/main/java/com/switcherapi/client/service/WorkerName.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ public enum WorkerName {
SNAPSHOT_WATCH_WORKER("switcherapi-snapshot-watcher"),
SNAPSHOT_UPDATE_WORKER("switcherapi-snapshot-update"),
SWITCHER_REMOTE_WORKER("switcherapi-remote-pool"),
SWITCHER_ASYNC_WORKER("switcherapi-async");
SWITCHER_ASYNC_WORKER("switcherapi-async"),
SWITCHER_TOKEN_WORKER("switcherapi-token-refresh");

private final String name;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,34 +6,52 @@
import com.switcherapi.client.exception.SwitcherRemoteException;
import com.switcherapi.client.model.ContextKey;
import com.switcherapi.client.model.criteria.Snapshot;
import com.switcherapi.client.remote.dto.*;
import com.switcherapi.client.remote.ClientWS;
import com.switcherapi.client.remote.dto.AuthResponse;
import com.switcherapi.client.remote.dto.CriteriaRequest;
import com.switcherapi.client.remote.dto.CriteriaResponse;
import com.switcherapi.client.remote.dto.SnapshotVersionResponse;
import com.switcherapi.client.remote.dto.SwitchersCheck;
import com.switcherapi.client.utils.SwitcherUtils;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.Date;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;

/**
* @author Roger Floriano (petruki)
* @since 2019-12-24
*/
public class ClientRemoteService implements ClientRemote {

private static final Logger log = LoggerFactory.getLogger(ClientRemoteService.class);

private final ScheduledExecutorService scheduledExecutorService;

private final SwitcherProperties switcherProperties;

private final ClientWS clientWs;

private AuthResponse authResponse;

private ScheduledFuture<?> refreshFuture;

private enum TokenStatus {
VALID, INVALID, SILENT
}

public ClientRemoteService(ClientWS clientWs, SwitcherProperties switcherProperties) {
public ClientRemoteService(ClientWS clientWs, SwitcherProperties switcherProperties,
ScheduledExecutorService scheduledExecutorService) {
this.clientWs = clientWs;
this.switcherProperties = switcherProperties;
this.scheduledExecutorService = scheduledExecutorService;
}

@Override
Expand Down Expand Up @@ -92,7 +110,12 @@ public SwitchersCheck checkSwitchers(final Set<String> switchers) {

private void auth(TokenStatus tokenStatus) {
if (tokenStatus == TokenStatus.INVALID) {
log.debug("Auth token is invalid or expired. Attempting to authenticate...");
this.authResponse = this.clientWs.auth().orElseGet(AuthResponse::new);

if (isAutoRefreshable()) {
scheduleNextAuth();
}
}

if (tokenStatus == TokenStatus.SILENT) {
Expand All @@ -109,7 +132,7 @@ private TokenStatus isTokenValid() throws SwitcherRemoteException,
return TokenStatus.INVALID;
}

if (optAuthResponse.get().getToken().equals(ContextKey.SILENT_MODE.getParam())
if (ContextKey.SILENT_MODE.getParam().equals(optAuthResponse.get().getToken())
&& !optAuthResponse.get().isExpired()) {
return TokenStatus.SILENT;
}
Expand All @@ -129,4 +152,35 @@ private void setSilentModeExpiration() throws SwitcherInvalidDateTimeArgumentExc
}
}

private void scheduleNextAuth() {
long msUntilExpiry = (authResponse.getExp() * 1000L) - (System.currentTimeMillis());
long refreshAt = Math.max(msUntilExpiry - 5000, 0); // 5s before expiry

terminateAutoRefresh();
refreshFuture = scheduledExecutorService.schedule(() -> {
try {
log.debug("Auto-refreshing auth token...");
this.authResponse = this.clientWs.auth().orElseGet(AuthResponse::new);
scheduleNextAuth();
} catch (Exception e) {
log.error("Failed to auto-refresh auth token: {}", e.getMessage());
terminateAutoRefresh();
}
}, refreshAt, TimeUnit.MILLISECONDS);
}

private boolean isAutoRefreshable() {
return switcherProperties.getBoolean(ContextKey.AUTO_REFRESH_TOKEN) &&
(Objects.isNull(refreshFuture) || refreshFuture.isDone());
}

private void terminateAutoRefresh() {
if (Objects.nonNull(refreshFuture)) {
refreshFuture.cancel(true);
refreshFuture = null;
log.debug("Terminated existing auto-refresh task.");
}
}
}


Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ class SwitcherBasicCriteriaResponseTest extends MockWebServerHelper {

@BeforeAll
static void setup() throws IOException {
MockWebServerHelper.setupMockServer();
setupMockServer();

Switchers.loadProperties(); // Load default properties from resources
Switchers.configure(ContextBuilder.builder() // Override default properties
Expand All @@ -40,7 +40,7 @@ static void setup() throws IOException {

@AfterAll
static void tearDown() {
MockWebServerHelper.tearDownMockServer();
tearDownMockServer();
}

@BeforeEach
Expand Down
Loading
Loading