Skip to content

Commit 2ca4aef

Browse files
committed
feat: added auto refresh token #388
1 parent f15eba5 commit 2ca4aef

32 files changed

Lines changed: 322 additions & 103 deletions

README.md

Lines changed: 23 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -135,28 +135,29 @@ switcher.poolsize=2
135135

136136
### Configuration Properties Reference
137137

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

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

pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
<groupId>com.switcherapi</groupId>
99
<artifactId>switcher-client</artifactId>
1010
<packaging>jar</packaging>
11-
<version>2.5.3-SNAPSHOT</version>
11+
<version>2.6.0-SNAPSHOT</version>
1212

1313
<name>Switcher Client</name>
1414
<description>Switcher Client SDK for working with Switcher API</description>

src/main/java/com/switcherapi/client/ContextBuilder.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -234,4 +234,9 @@ public ContextBuilder poolConnectionSize(Integer poolSize) {
234234
Optional.ofNullable(poolSize).orElse(DEFAULT_POOL_SIZE));
235235
return this;
236236
}
237+
238+
public ContextBuilder autoRefreshToken(boolean autoRefreshToken) {
239+
switcherProperties.setValue(ContextKey.AUTO_REFRESH_TOKEN, autoRefreshToken);
240+
return this;
241+
}
237242
}

src/main/java/com/switcherapi/client/SwitcherConfig.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ abstract class SwitcherConfig {
1212

1313
protected boolean local;
1414
protected boolean check;
15+
protected boolean autoRefreshToken;
1516
protected String silent;
1617
protected Integer timeout;
1718
protected Integer poolSize;
@@ -38,6 +39,7 @@ protected void updateSwitcherConfig(SwitcherProperties properties) {
3839
setEnvironment(properties.getValue(ContextKey.ENVIRONMENT));
3940
setLocal(properties.getBoolean(ContextKey.LOCAL_MODE));
4041
setCheck(properties.getBoolean(ContextKey.CHECK_SWITCHERS));
42+
setAutoRefreshToken(properties.getBoolean(ContextKey.AUTO_REFRESH_TOKEN));
4143
setSilent(properties.getValue(ContextKey.SILENT_MODE));
4244
setTimeout(properties.getInt(ContextKey.TIMEOUT_MS));
4345
setPoolSize(properties.getInt(ContextKey.POOL_CONNECTION_SIZE));
@@ -105,6 +107,10 @@ public void setCheck(boolean check) {
105107
this.check = check;
106108
}
107109

110+
public void setAutoRefreshToken(boolean autoRefreshToken) {
111+
this.autoRefreshToken = autoRefreshToken;
112+
}
113+
108114
public void setSilent(String silent) {
109115
this.silent = silent;
110116
}

src/main/java/com/switcherapi/client/SwitcherContextBase.java

Lines changed: 36 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,8 @@ public abstract class SwitcherContextBase extends SwitcherConfig {
8787
protected static Set<String> switcherKeys;
8888
protected static Map<String, SwitcherRequest> switchers;
8989
protected static SwitcherExecutor switcherExecutor;
90-
private static ScheduledExecutorService scheduledExecutorService;
90+
private static ScheduledExecutorService scheduledSnapshotExecutorService;
91+
private static ScheduledExecutorService scheduledTokenExecutorService;
9192
private static ExecutorService watcherExecutorService;
9293
private static SnapshotWatcher watcherSnapshot;
9394
protected static SwitcherContextBase contextBase;
@@ -111,6 +112,7 @@ protected void configureClient() {
111112
.restrictRelay(relay.isRestrict())
112113
.silentMode(silent)
113114
.timeoutMs(timeout)
115+
.autoRefreshToken(autoRefreshToken)
114116
.poolConnectionSize(poolSize)
115117
.snapshotLocation(snapshot.getLocation())
116118
.snapshotAutoLoad(snapshot.isAuto())
@@ -197,9 +199,12 @@ public static void initializeClient() {
197199
* @return SwitcherExecutor instance
198200
*/
199201
private static SwitcherExecutor buildInstance() {
202+
initTokenExecutorService();
203+
200204
final ClientWS clientWS = initRemotePoolExecutorService();
201205
final SwitcherValidator validatorService = new ValidatorService();
202-
final ClientRemote clientRemote = new ClientRemoteService(clientWS, switcherProperties);
206+
final ClientRemote clientRemote = new ClientRemoteService(
207+
clientWS, switcherProperties, scheduledTokenExecutorService);
203208
final ClientLocal clientLocal = new ClientLocalService(validatorService);
204209

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

309-
if (Objects.nonNull(scheduledExecutorService)) {
314+
if (Objects.nonNull(scheduledSnapshotExecutorService)) {
310315
terminateSnapshotAutoUpdateWorker();
311316
}
312317

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

326331
initSnapshotExecutorService();
327-
return scheduledExecutorService.scheduleAtFixedRate(runnableSnapshotValidate, 0, interval, TimeUnit.MILLISECONDS);
332+
return scheduledSnapshotExecutorService.scheduleAtFixedRate(runnableSnapshotValidate, 0, interval, TimeUnit.MILLISECONDS);
328333
}
329334

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

341346
/**
342-
* Configure Executor Service for Snapshot Update Worker
347+
* Configure Scheduled Executor Service for Snapshot Update Worker
343348
*/
344349
private static void initSnapshotExecutorService() {
345-
scheduledExecutorService = Executors.newSingleThreadScheduledExecutor(r -> {
350+
scheduledSnapshotExecutorService = Executors.newSingleThreadScheduledExecutor(r -> {
346351
Thread thread = new Thread(r);
347352
thread.setName(WorkerName.SNAPSHOT_UPDATE_WORKER.toString());
348353
thread.setDaemon(true);
349354
return thread;
350355
});
351356
}
352357

358+
/**
359+
* Configure Scheduled Executor Service for Token Refresh Worker
360+
*/
361+
private static void initTokenExecutorService() {
362+
scheduledTokenExecutorService = Executors.newSingleThreadScheduledExecutor(r -> {
363+
Thread thread = new Thread(r);
364+
thread.setName(WorkerName.SWITCHER_TOKEN_WORKER.toString());
365+
thread.setDaemon(true);
366+
return thread;
367+
});
368+
}
369+
353370
/**
354371
* Configure Executor Service for Snapshot Watch Worker
355372
*/
@@ -538,9 +555,19 @@ public static void configure(ContextBuilder builder) {
538555
* Cancel existing scheduled task for updating local Snapshot
539556
*/
540557
public static void terminateSnapshotAutoUpdateWorker() {
541-
if (Objects.nonNull(scheduledExecutorService)) {
542-
scheduledExecutorService.shutdownNow();
543-
scheduledExecutorService = null;
558+
if (Objects.nonNull(scheduledSnapshotExecutorService)) {
559+
scheduledSnapshotExecutorService.shutdownNow();
560+
scheduledSnapshotExecutorService = null;
561+
}
562+
}
563+
564+
/**
565+
* Cancel existing scheduled task for token refresh
566+
*/
567+
public static void terminateTokenRefreshWorker() {
568+
if (Objects.nonNull(scheduledTokenExecutorService)) {
569+
scheduledTokenExecutorService.shutdownNow();
570+
scheduledTokenExecutorService = null;
544571
}
545572
}
546573

src/main/java/com/switcherapi/client/SwitcherPropertiesImpl.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ private void initDefaults() {
3030
setValue(ContextKey.LOCAL_MODE, false);
3131
setValue(ContextKey.CHECK_SWITCHERS, false);
3232
setValue(ContextKey.RESTRICT_RELAY, true);
33+
setValue(ContextKey.AUTO_REFRESH_TOKEN, false);
3334
}
3435

3536
@Override
@@ -49,6 +50,7 @@ public void loadFromProperties(Properties prop) {
4950
setValue(ContextKey.LOCAL_MODE, getBoolDefault(resolveProperties(ContextKey.LOCAL_MODE.getParam(), prop), false));
5051
setValue(ContextKey.CHECK_SWITCHERS, getBoolDefault(resolveProperties(ContextKey.CHECK_SWITCHERS.getParam(), prop), false));
5152
setValue(ContextKey.RESTRICT_RELAY, getBoolDefault(resolveProperties(ContextKey.RESTRICT_RELAY.getParam(), prop), true));
53+
setValue(ContextKey.AUTO_REFRESH_TOKEN, getBoolDefault(resolveProperties(ContextKey.AUTO_REFRESH_TOKEN.getParam(), prop), false));
5254
setValue(ContextKey.REGEX_TIMEOUT, getIntDefault(resolveProperties(ContextKey.REGEX_TIMEOUT.getParam(), prop), DEFAULT_REGEX_TIMEOUT));
5355
setValue(ContextKey.TRUSTSTORE_PATH, resolveProperties(ContextKey.TRUSTSTORE_PATH.getParam(), prop));
5456
setValue(ContextKey.TRUSTSTORE_PASSWORD, resolveProperties(ContextKey.TRUSTSTORE_PASSWORD.getParam(), prop));

src/main/java/com/switcherapi/client/model/ContextKey.java

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -109,8 +109,13 @@ public enum ContextKey {
109109
/**
110110
* (Number) Defines a fixed number of threads for the pool connection (default is 2).
111111
*/
112-
POOL_CONNECTION_SIZE("switcher.poolsize");
113-
112+
POOL_CONNECTION_SIZE("switcher.poolsize"),
113+
114+
/**
115+
* (boolean) Enables automatic refresh of authentication token (default is false)
116+
*/
117+
AUTO_REFRESH_TOKEN("switcher.autorefreshtoken");
118+
114119
private final String param;
115120

116121
ContextKey(String param) {

src/main/java/com/switcherapi/client/remote/dto/AuthResponse.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,10 @@ public boolean isExpired() {
2626
return (this.exp * 1000) < System.currentTimeMillis();
2727
}
2828

29+
public long getExp() {
30+
return this.exp;
31+
}
32+
2933
@Override
3034
public String toString() {
3135
return "AuthResponse{" +

src/main/java/com/switcherapi/client/service/WorkerName.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@ public enum WorkerName {
55
SNAPSHOT_WATCH_WORKER("switcherapi-snapshot-watcher"),
66
SNAPSHOT_UPDATE_WORKER("switcherapi-snapshot-update"),
77
SWITCHER_REMOTE_WORKER("switcherapi-remote-pool"),
8-
SWITCHER_ASYNC_WORKER("switcherapi-async");
8+
SWITCHER_ASYNC_WORKER("switcherapi-async"),
9+
SWITCHER_TOKEN_WORKER("switcherapi-token-refresh");
910

1011
private final String name;
1112

src/main/java/com/switcherapi/client/service/remote/ClientRemoteService.java

Lines changed: 58 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,34 +6,52 @@
66
import com.switcherapi.client.exception.SwitcherRemoteException;
77
import com.switcherapi.client.model.ContextKey;
88
import com.switcherapi.client.model.criteria.Snapshot;
9-
import com.switcherapi.client.remote.dto.*;
109
import com.switcherapi.client.remote.ClientWS;
10+
import com.switcherapi.client.remote.dto.AuthResponse;
11+
import com.switcherapi.client.remote.dto.CriteriaRequest;
12+
import com.switcherapi.client.remote.dto.CriteriaResponse;
13+
import com.switcherapi.client.remote.dto.SnapshotVersionResponse;
14+
import com.switcherapi.client.remote.dto.SwitchersCheck;
1115
import com.switcherapi.client.utils.SwitcherUtils;
1216
import org.apache.commons.lang3.StringUtils;
17+
import org.slf4j.Logger;
18+
import org.slf4j.LoggerFactory;
1319

1420
import java.util.Date;
21+
import java.util.Objects;
1522
import java.util.Optional;
1623
import java.util.Set;
24+
import java.util.concurrent.ScheduledExecutorService;
25+
import java.util.concurrent.ScheduledFuture;
26+
import java.util.concurrent.TimeUnit;
1727

1828
/**
1929
* @author Roger Floriano (petruki)
2030
* @since 2019-12-24
2131
*/
2232
public class ClientRemoteService implements ClientRemote {
2333

34+
private static final Logger log = LoggerFactory.getLogger(ClientRemoteService.class);
35+
36+
private final ScheduledExecutorService scheduledExecutorService;
37+
2438
private final SwitcherProperties switcherProperties;
25-
39+
2640
private final ClientWS clientWs;
2741

2842
private AuthResponse authResponse;
2943

44+
private ScheduledFuture<?> refreshFuture;
45+
3046
private enum TokenStatus {
3147
VALID, INVALID, SILENT
3248
}
3349

34-
public ClientRemoteService(ClientWS clientWs, SwitcherProperties switcherProperties) {
50+
public ClientRemoteService(ClientWS clientWs, SwitcherProperties switcherProperties,
51+
ScheduledExecutorService scheduledExecutorService) {
3552
this.clientWs = clientWs;
3653
this.switcherProperties = switcherProperties;
54+
this.scheduledExecutorService = scheduledExecutorService;
3755
}
3856

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

93111
private void auth(TokenStatus tokenStatus) {
94112
if (tokenStatus == TokenStatus.INVALID) {
113+
log.debug("Auth token is invalid or expired. Attempting to authenticate...");
95114
this.authResponse = this.clientWs.auth().orElseGet(AuthResponse::new);
115+
116+
if (isAutoRefreshable()) {
117+
scheduleNextAuth();
118+
}
96119
}
97120

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

112-
if (optAuthResponse.get().getToken().equals(ContextKey.SILENT_MODE.getParam())
135+
if (ContextKey.SILENT_MODE.getParam().equals(optAuthResponse.get().getToken())
113136
&& !optAuthResponse.get().isExpired()) {
114137
return TokenStatus.SILENT;
115138
}
@@ -129,4 +152,35 @@ private void setSilentModeExpiration() throws SwitcherInvalidDateTimeArgumentExc
129152
}
130153
}
131154

155+
private void scheduleNextAuth() {
156+
long msUntilExpiry = (authResponse.getExp() * 1000L) - (System.currentTimeMillis());
157+
long refreshAt = Math.max(msUntilExpiry - 5000, 0); // 5s before expiry
158+
159+
terminateAutoRefresh();
160+
refreshFuture = scheduledExecutorService.schedule(() -> {
161+
try {
162+
log.debug("Auto-refreshing auth token...");
163+
this.authResponse = this.clientWs.auth().orElseGet(AuthResponse::new);
164+
scheduleNextAuth();
165+
} catch (Exception e) {
166+
log.error("Failed to auto-refresh auth token: {}", e.getMessage());
167+
terminateAutoRefresh();
168+
}
169+
}, refreshAt, TimeUnit.MILLISECONDS);
170+
}
171+
172+
private boolean isAutoRefreshable() {
173+
return switcherProperties.getBoolean(ContextKey.AUTO_REFRESH_TOKEN) &&
174+
(Objects.isNull(refreshFuture) || refreshFuture.isDone());
175+
}
176+
177+
private void terminateAutoRefresh() {
178+
if (Objects.nonNull(refreshFuture)) {
179+
refreshFuture.cancel(true);
180+
refreshFuture = null;
181+
log.debug("Terminated existing auto-refresh task.");
182+
}
183+
}
132184
}
185+
186+

0 commit comments

Comments
 (0)