Reputation: 231
I want to create a java application , where we want to make rest calls for multiple users , with the help of an access token. I am using 1 thread per user . The access token, that I am using , is valid for 1 hour.Once the token expires , I will get an 401 error , and have to update the token for all the threads , and continue. I am thinking of using a volatile variable which I have made static to update all the threads. My requirement , is , the moment I get to know in one of threads that the token has expired , I want all the threads to stop processing and wait till the new token is generated(this takes couple of seconds).Also once generated , the token should be updated automatically , without each thread failing due to expired token.
Below is a sample code that I have written :
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
public class Sample {
public static void main(String[] args) {
String[] myStrings = { "User1" , "User2" , "User3" };
ScheduledExecutorService scheduledExecutorService = Executors
.newScheduledThreadPool(myStrings.length);
TokenGenerator.getToken();
for(String str : myStrings){
scheduledExecutorService.scheduleAtFixedRate(new Task(str), 0, 5, TimeUnit.SECONDS);
}
}
}
class Task implements Runnable{
private String name;
public Task(String name){
this.name = name;
}
@Override
public void run() {
getResponse(TokenGenerator.token);
}
private void getResponse(String token) {
// Make http calls
// if token expire , call getToken again. Pause all the running threads , and
// update the token for all threads
TokenGenerator.getToken();
}
}
class TokenGenerator {
public static volatile String token;
public static void getToken() {
token = "new Token everytime";
}
}
Is there a better approach to this problem? The above code does not satisfies my use case, since once a thread starts generating a new token, all other threads are not being paused. Request to suggest some improvements..
Upvotes: 8
Views: 3176
Reputation: 18148
You can put the token in an AtomicReference and use a Semaphore to pause the threads:
public class TokenWrapper {
private final AtomicReference<Token> tokenRef = new AtomicReference<>(null);
private final Semaphore semaphore = new Semaphore(Integer.MAX_VALUE);
public TokenWrapper() {
Token newToken = // refresh token
tokenRef.set(newToken);
}
public Token getToken() {
Token token = null;
while((token = tokenRef.get()) == null) {
semaphore.acquire();
}
return token;
}
public Token refreshToken(Token oldToken) {
if(tokenRef.compareAndSet(oldToken, null)) {
semaphore.drainPermits();
Token newToken = // refresh token
tokenRef.set(newToken);
semaphore.release(Integer.MAX_VALUE);
return newToken;
} else return getToken();
}
}
public class RESTService {
private static final TokenWrapper tokenWrapper = new TokenWrapper();
public void run() {
Token token = tokenWrapper.getToken();
Response response = // call service with token
if(response.getStatus == 401) {
tokenWrapper.refreshToken(token);
}
}
}
refreshToken()
uses an atomic compareAndSet
on tokenRef
to ensure that only one thread will refresh the token, and then calls drainPermits()
on the semaphore
to cause other threads to wait until the token is refreshed. getToken()
returns the token if it isn't null
, else waits on the semaphore
- this is done in a loop because it's possible that a thread will have to spin for a few cycles between tokenRef
being set to null
and drainPermits()
being called on semaphore
.
Edit: Modified the signature of refreshToken(Token oldToken)
so that the old token is passed in rather than being read inside of the method - this is to prevent a situation where RESTService_A refreshes the token, RESTService_B gets a 401 with the old expired token, and then RESTService_B calls refreshToken
after RESTService_A's call to refreshToken
has completed, resulting in the token being refreshed twice. With the new signature, RESTService_B will pass in the old expired token, and so the compareAndSet
call will fail when the old token fails to match the new token, resulting in refreshToken
only being called once.
Upvotes: 9
Reputation: 5423
Since you need to do two things(http calls and update token), you could try two way checker.
one to check if the token is expired or not
and the other one is to check if any other thread is trying to update the token
.
to demonstrate the idea here is a small code( its abit dirty way of doing it so it might need some cleaning up)
...
private string token
private volatile static isTokenExpired=false //checking if the token is expired or not
private volatile static waitingForTokenRefresher=false; //checking if we should wait for update.
@Override
public void run(){
while(tokenisExpired){
//wait
}
//http calls find out if token is good to go
//check if no one else uses the token:
if( token is actually expired){
if(!waitingForTokenRefresher){
isTokenExpired=true;
waitingForTokenRefresher=true;
//refresh token
waitingForTokenRefresher=false
isTokenExpired=false;
}
}
while(!waitingForTokenRefresher){
//wait...
}
}
Upvotes: 1
Reputation: 3992
You could use the following pattern, accessing the token only using its getter and calling loadToken
when you receive the error response.
class TokenGenerator {
private String token = null;
public synchronized String getToken() {
if (token == null) {
loadToken();
}
return token;
}
public synchronized void loadToken() {
token = "load here";
}
}
To address your problem of pausing threads, you can just call getToken()
whereever you want halt the Thread
which will automatically block in case loading the token is currently active.
class Task implements Runnable{
private String name;
private TokenGenerator tokenGenerator;
public Task(String name, TokenGenerator tokenGenerator) {
this.name = name;
this.tokenGenerator = tokenGenerator;
}
@Override
public void run() {
getResponse(tokenGenerator.getToken());
}
private void getResponse(String token) {
// Make http calls
// if token expire , call getToken again. Pause all the running threads , and
// update the token for all threads
tokenGenerator.loadToken();
}
}
Upvotes: 1