WebClient: Waiting for blocking call in API to perform a new call

Viewed 145

I am currently making a microservice to create test users for our automated test environment. The database is reachable through another API, so in order to create a test user, I need to perform a call to this API.

The test users should be created and then later disposed of when the test has been executed. The identifier of the test users is the SSN (national identifier digit), and it is unique to each citizen. My API/microservice generates a new user with a generated SSN and should post it to a DB over the API to the backend service controlling the database. This backend service is not reactive.

The problem is that in the database, there are already many existing users which are used by other tests executed manually. The existing test users cannot be tampered with, so I need to verify that the generated SSN is not already existing in the DB.

My approach is as follows:

generate a new ssn
while(ssn exists in db){
    generate new ssn
}
post generated user to db

However, when placing a .block() on the check if the user exists (bad practice, I know...) the program halts in a deadlock and nothing happens.

My controller:

@ResponseBody
@PostMapping("normal")
public Mono<User> createNormalUser() throws Exception {
    return userService.createNormalUser();
}

@ResponseBody
@GetMapping("{ssn}")
public Mono<User> getUserBySSN(@PathVariable String ssn){
    return userService.getUserBySsn(ssn);
}

My service:

public Mono<User> createNormalUser(){
   String ssn = generateNewSsnNotInDB();

   Mono<UserResource> newUserMono = Mono.just(
           UserResource.builder()
                   .ssn(ssn)
                   .email(ssn + "-test@somedomain.com")
                   .state("NORMAL")
                   .preferred2FaMethod("some2FAMethod")
                   .build()
   );
   return postUser(newUserMono)
           .then(updatePassword(ssn))
           .then(setState(ssn, "NORMAL"));
}

private String generateNewSsnNotInDB() {
   String ssn;
   boolean userExists = false;

   do {
       ssn = ssnGenerator.generateOneValidSsnOnDate(ssnGenerator.generateRandomSsnDate());
       userExists = checkIfUserExists(ssn);
   } while (userExists);
   return ssn;
}

private boolean checkIfUserExists(String ssn) {
   User user;
   try {
       user = getUserBySsn(ssn).share().block();
       return true;
   } catch (WebClientResponseException.NotFound exception) {
       return false;
   }
}

public Mono<User> getUserBySsn(String ssn) {
   return webClient.get()
           .uri(userBySsnURI(ssn))
           .retrieve()
           .bodyToMono(User.class);
}

public Mono<User> postUser(Mono<UserResource> userMono) {
   return webClient.post()
           .uri(setUserURI())
           .body(userMono, UserResource.class)
           .retrieve()
           .bodyToMono(User.class);
}

public Mono<User> postUser(User user) {
   user.setPid(generateNewSsnNotInDB());
   UserResource res = UserResource.builder()
           .ssn(user.getPid())
           .email(user.getEmail())
           .phoneNumber(user.getPhoneNumber())
           .state(user.getState())
           .preferred2FaMethod(user.getPreferred2FaMethod())
           .password(user.getPassword())
           .build();
   log.info("Resource generated in post-user is: " + res.toString());
   return postUser(Mono.just(res));
}

public Mono<User> updatePassword(String ssn) {
   Mono<User> user = Mono.just(User.builder()
           .pid(ssn)
           .password("password01")
           .build());
   return webClient.patch()
           .uri(setUpdatePasswordURI())
           .body(user, User.class)
           .retrieve()
           .bodyToMono(User.class);
}

private Mono<User> setState(String ssn, String state) {
   return webClient.put()
           .uri(updateStateURI(ssn, state))
           .retrieve()
           .bodyToMono(User.class);
}

I have chained the calls in the createNormalUser function because the backend requires this sequence in order to set the required attributes for the user. I am not sure why this is the required sequence, and changing this is not part of my scope.

I have also omitted some functions which probably aren't relevant for this question.

Can somebody please help me in the right direction on how to perform the calls with checkIfUsersExist and then post the user? I have been trying to wrap my head around this for a week now with no luck.

The strangest thing is that if I first call getUser with a valid ssn, then postUser works fine. If I try to call postUser without calling getUser first, it deadlocks on the .block().

1 Answers

Avoid the block() call and user chained calls instead as follows (createNormalUser() and generateNewSsnNotInDB() were updated and checkIfUserExists() deleted):

public Mono<User> createNormalUser(){
    Mono<UserResource> newUserMono = generateNewSsnNotInDB().map( ssn ->
           UserResource.builder()
                   .ssn(ssn)
                   .email(ssn + "-test@somedomain.com")
                   .state("NORMAL")
                   .preferred2FaMethod("some2FAMethod")
                   .build()
   );
   return postUser(newUserMono)
           .then(updatePassword(ssn))
           .then(setState(ssn, "NORMAL"));
}

private Mono<String> generateNewSsnNotInDB() {
   return Mono.just(ssnGenerator.generateOneValidSsnOnDate(ssnGenerator.generateRandomSsnDate()))
      .flatMap(ssn -> getUserBySsn(ssn))
      .switchIfEmpty(Mono.defer(() -> generateNewSsnNotInDB()));
}

public Mono<User> getUserBySsn(String ssn) {
   return webClient.get()
           .uri(userBySsnURI(ssn))
           .retrieve()
           .bodyToMono(User.class);
}

public Mono<User> postUser(Mono<UserResource> userMono) {
   return webClient.post()
           .uri(setUserURI())
           .body(userMono, UserResource.class)
           .retrieve()
           .bodyToMono(User.class);
}

public Mono<User> postUser(User user) {
   user.setPid(generateNewSsnNotInDB());
   UserResource res = UserResource.builder()
           .ssn(user.getPid())
           .email(user.getEmail())
           .phoneNumber(user.getPhoneNumber())
           .state(user.getState())
           .preferred2FaMethod(user.getPreferred2FaMethod())
           .password(user.getPassword())
           .build();
   log.info("Resource generated in post-user is: " + res.toString());
   return postUser(Mono.just(res));
}

public Mono<User> updatePassword(String ssn) {
   Mono<User> user = Mono.just(User.builder()
           .pid(ssn)
           .password("password01")
           .build());
   return webClient.patch()
           .uri(setUpdatePasswordURI())
           .body(user, User.class)
           .retrieve()
           .bodyToMono(User.class);
}

private Mono<User> setState(String ssn, String state) {
   return webClient.put()
           .uri(updateStateURI(ssn, state))
           .retrieve()
           .bodyToMono(User.class);
}
Related