Unexpected return value - spring

I'm trying to implement password comparison. First I tried this:
#Autowired
private PasswordEncoder passwordEncoder;
#Autowired
private OldPasswordsService oldPasswordsService;
Optional<OldPasswords> list = oldPasswordsService.findEncryptedPassword(passwordEncoder.encode("new password entered form web reset form"));
OldPasswords value = list.get();
boolean matches = passwordEncoder.matches("new password entered form web reset form", value.getEncryptedPassword());
if (matches)
{
return new ResponseEntity<>("PASSWORD_ALREADY_USED", HttpStatus.BAD_REQUEST);
}
else
{
OldPasswords oldPasswords = new OldPasswords();
oldPasswords.setEncryptedPassword(passwordEncoder.encode(resetDTO.getPassword()));
oldPasswords.setCreatedAt(LocalDateTime.now());
oldPasswordsService.save(oldPasswords);
}
Table for old passwords:
#Table(name = "old_passwords")
public class OldPasswords implements Serializable {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "id", unique = true, updatable = false, nullable = false)
private int id;
#Column(name = "encrypted_password", length = 255)
private String encryptedPassword;
I tried to implement this:
......
return this.userService.findByLogin(resetDTO.getName()).map(user -> {
Optional<OldPasswords> list = oldPasswordsService.findEncryptedPassword(passwordEncoder.encode(resetDTO.getPassword()));
list.ifPresent(value -> {
boolean matches = passwordEncoder.matches(resetDTO.getPassword(), value.getEncryptedPassword());
if (matches) {
return new ResponseEntity<>("PASSWORD_ALREADY_USED", HttpStatus.BAD_REQUEST);
}
}).orElse(() -> {
OldPasswords oldPasswords = new OldPasswords();
oldPasswords.setEncryptedPassword(passwordEncoder.encode(resetDTO.getPassword()));
oldPasswordsService.save(oldPasswords);
});
return ok().build();
}).orElseGet(() -> notFound().build());
}
But I get for this line:
return new ResponseEntity<>("PASSWORD_ALREADY_USED", HttpStatus.BAD_REQUEST);
Error
Unexpected return value
Do you know how I can fix this?

It is probably List!
So with:
#Autowired
private PasswordEncoder passwordEncoder;
#Autowired
private OldPasswordsService oldPasswordsService;
...and
#Autowired
private UserXXXService userService;
...let's assume, you are trying to actually:
#RequestMapping(...)
public ResponseEntity<String> resetPassword(ResetPasswordDTO dto) {
...
...that's how i would probalby go about it:
final Optional<User> user = this.userService.findByLogin(dto.getName()); // Optional or null?, let's assume Optional
// if user (login) exists:
if(user.isPresent()) {
// check old passwords, the method name/data structure lets assume it's rather List than Optional:
java.util.List<OldPasswords> list = oldPasswordsService.findEncryptedPassword(passwordEncoder.encode(dto.getPassword()));
if(list.isEmpty()) {// list is empty...
// do your things..
OldPasswords oP= new OldPasswords();
oP.setEncryptedPassword(passwordEncoder.encode(dto.getPassword()));
oldPasswordsService.save(oP);
return ResonseEntity.ok().build(); // ok!
} else {// otherwise:
return new ResponseEntity<>("PASSWORD_ALREADY_USED", HttpStatus.BAD_REQUEST);
}
} else { // otherwise (user.login not exists)
return ResponseEntity<>.notFound().build();
}
}
(nor tested nor compiled)
..one technical question/detail remains/hidden: I miss the "binding" of "user" and "old_password"... so, should it check the old passwords of one user or all?
The 1st one sounds more fair/correct: old passwords should be "user based":
#Entity
// some unique constraints, when you have..., would be nice
public class OldPasswords implements Serializable { // singular is better for entity/table names!
....
#ManyToOne
#JoinColumn("user_id") // if you don't want to map the entity (for some reasons), you should still map the "id".
private User user;
// getter/setter ...
}
...
(with least impact,) you could then:
public interface OldPasswordsRepository extends Repository<OldPasswords, Integer> { // <- Integer ???
List<OldPasswords> findByUserAndEncryptedPassword(User user, String pwd); // to use that...
}
EDIT: To match against the last three passwords, #Peter, your datastructure needs (additionally to user) some "tweak" - like an created timestamp!;) (a better choice, than a "version/age column")
#Column(nullable = false) // final + instantiation is suited here
final Date created = new Date(); //long, LocalDateTime, DateTime... Timestamp, java.sql.Date... and many alternatives.
// getter
..then you could (in the repository):
// untested!
List<OldPasswords> findTop3ByUserOrderByCreatedDesc(User user);
..and use it in the controller/service, like:
... // if user is present
List<OldPasswords> list = oldPasswordsRpository.findTop3ByUserOrderByCreatedDesc(user);
for(OldPasswords opw:list) {
// compare opw to dto.getPasword if match: return "bad request"
}
// after that (no bad request), store new (& old) password... (everywhere relevant),
// ...and when nothing fails:
return ResponseEntity.ok().build();
// else: user not present -> return not found

Related

Derive tenant id for signed in user in OAuth2 based multi-tenant Spring Boot application using Spring Security

I am working on a Spring Boot B2B application, where I would like to onboard a considerable number of tenants. Each of the tenants has its own authentication providers. I have decided to support only OAuth2-based authentication.
As of now, my application has been single-tenant, that's why I did not have the need to derive tenant id for any user. But with multi-tenancy, I need to serve the resources based on the tenant, the user belongs, what role the user has for the given tenant, etc. In other words, each and every flow in my application is going to be dependent on the tenant id information of a user.
In order to achieve the same, I have added tenantId column to the existing User entity as follows:
#Entity
#Table(name = "user")
public class User implements Serializable {
private static final long serialVersionUID = 1L;
#Id
private String id;
#NotNull
private String login;
#Column(name = "first_name", length = 50)
private String firstName;
#Column(name = "last_name", length = 50)
private String lastName;
#Email
#Column(length = 254, unique = true)
private String email;
...
#Column(length = 254, unique = true)
private String tenantId;
...
}
Now, I am deriving and capturing tenant id information for a given user at the time of signup to my application as follows:
User getUser(AbstractAuthenticationToken authToken) { <---THIS FUNCTION GETS CALLED WHILE USER SIGNS UP FOR THE APPLICATION
Map<String, Object> attributes;
User user = null;
if (authToken instanceof OAuth2AuthenticationToken) {
attributes = ((OAuth2AuthenticationToken) authToken).getPrincipal().getAttributes();
} else if (authToken instanceof JwtAuthenticationToken) {
attributes = ((JwtAuthenticationToken) authToken).getTokenAttributes();
} else {
throw new IllegalArgumentException("AuthenticationToken is not OAuth2 or JWT!");
}
if (user == null) {
user = getUser(attributes);
}
return Pair.of(user, attributes);
}
private static User getUser(Map<String, Object> details) {
User user = new User();
if (details.get("uid") != null) {
user.setId((String) details.get("uid"));
user.setLogin((String) details.get("sub"));
} else if (details.get("user_id") != null) {
user.setId((String) details.get("user_id"));
} else {
user.setId((String) details.get("sub"));
}
if (details.get("email") != null) {
user.setEmail(((String) details.get("email")).toLowerCase());
} else {
user.setEmail((String) details.get("sub"));
}
user.setTenantId(getTenantIdFromEmail(user.getEmail()));
...
}
private getTenantIdFromEmail(String email) {
String subDomain = getSubDomain(email);
return tenantRepository.findBySubDomainName(subDomain).getTenantId();
}
Now, whenever I need tenant id information for a signed-in user, I can do like below:
public String getTenantId() {
Optional<User> signedInUserOptional =
userRepository.findByLogin(SecurityUtils.getCurrentUserLogin();
return signedInUserOptional.isPresent() ? signedInUserOptional.get().getTenantId() : null;
}
Here, I have two questions as follows:
I am not sure if the code to derive tenant id would work correctly across different Ouath2 authentication providers of multiple tenants, if not, could anyone please help here? (by different authentication providers, I meant, each of the tenants may have their own internal authentication providers)
In almost each application flow (for example, doing any CRUD operation on a resource), I have to derive tenant id information as mentioned above. Is there any way to make this a little efficient i.e. compute it once for a signed-in user and then pass it across the entire application flow for the user session?
Consider using ThreadLocal variables and set this during successful authentication ex:OAuthFilter in your case.
public class TenantIdContextHolder {
private static final ThreadLocal<String> contextHolder = new ThreadLocal<>();
public static void setCurrentTenant(String tenantId) {
contextHolder.set(tenantId);
}
public static String getCurrentTenant() {
return contextHolder.get();
}
public static void clear() {
contextHolder.remove();
}
}
Usage
private static User getUser(Map<String, Object> details) {
User user = new User();
if (details.get("uid") != null) {
user.setId((String) details.get("uid"));
user.setLogin((String) details.get("sub"));
} else if (details.get("user_id") != null) {
user.setId((String) details.get("user_id"));
} else {
user.setId((String) details.get("sub"));
}
if (details.get("email") != null) {
user.setEmail(((String) details.get("email")).toLowerCase());
} else {
user.setEmail((String) details.get("sub"));
}
TenantIdContextHolder.setCurrentTenant(getTenantIdFromEmail(user.getEmail()));
...
}
Getting current tenant anytime
String currentTenant = TenantIdContextHolder.getCurrentTenant();

Spring WebFlux - Convert Flux to List<Object>

I am learning Spring WebFlux.
My Entity goes like this:
#Table("users")
public class User {
#Id
private Integer id;
private String name;
private int age;
private double salary;
}
I have a Repository (R2 using H2 Database) like below:
public interface UserRepository extends ReactiveCrudRepository<User,Integer> {
}
And my controller is:
#Autowired
private UserRepository userRepository;
private static List<User> userList = new ArrayList<>();
#PostConstruct
public void initializeStockObjects() {
User stock1 = new User(11, "aaaa", 123, 123);
User stock2 = new User(12, "bbb", 123, 123);
User stock3 = new User(13, "ccc", 123, 123);
userList.add(stock1);
userList.add(stock2);
userList.add(stock3);
}
#RequestMapping(value = "/livelistofusers", method = RequestMethod.GET, produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<List<User>> getUsers() {
return getUserData(userList);
}
public Flux<List<User>> getUserData(List<User> userList) {
Flux<Long> interval = Flux.interval(Duration.ofSeconds(3));
interval.subscribe((i) -> userList.forEach(user -> addNewUser(user)));
Flux<List<User>> transactionFlux = Flux.fromStream(Stream.generate(() -> userList));
return Flux.zip(interval, transactionFlux).map(Tuple2::getT2);
}
All good till this point. I am able to return the the entire list of users every 3 seconds to the view. No issues at all here.
Now, I want to send the Flue i.e. Flux flux2 = userRepository.findAll() to the view. That means, instead of return getUserData(userList); how can I do return getUserData(flux2(...what should I do here ???... I tried couple of things but I end up making the Blocking list instead of Non-Blocking ...)); ?
Question: How can I achieve this? i.e. How can I send the entire Flux every 3 seconds to my view. I am feeling lost here and clueless. Any relevant help links or solution will be greatly appreciated.
Edit:
As per Nipuna's comments I tried this:
#RequestMapping(value = "/livelistofusersall", method = RequestMethod.GET, produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<List<User>> getUsersall() {
Flux<Long> interval = Flux.interval(Duration.ofSeconds(3));
interval.subscribe((i) -> userRepository.findAll());
Flux<List<User>> transactionFlux = userRepository.findAll().collectList().flatMapMany(Flux::just);
return Flux.zip(interval, transactionFlux).map(Tuple2::getT2);
}
But now at my context path, the list loads but "only once" after a wait of 3 seconds. What I am missing here?
You can use collectList() operator in Flux for this which gives a Mono of List.
userRepository.findAll().collectList().flatMapMany(Flux::just);

JHipster - Insert in the database with the GET method

I have to create an application with Jhipster but i never use it before.
When a user send a GET request to the address http://localhost:8080/api/newmesure/{mac-address}/{value}
I want to insert a new mesure in my database.
First i created 3 entity "Plantes", "Capteurs" and "Mesures" with this format :
Image here : https://i.stack.imgur.com/zJqia.png (I'm not allowed to post)
I activated the JPA Filtering to create a #Query to insert data in my database but i read that was not possible.
In /src/main/java/com/mycompany/myapp/web/rest/MesuresRessources.java :
/**
* REST controller for managing {#link com.mycompany.myapp.domain.Mesures}.
*/
#RestController
#RequestMapping("/api")
public class MesuresResource {
private final Logger log = LoggerFactory.getLogger(MesuresResource.class);
private static final String ENTITY_NAME = "mesures";
#Value("${jhipster.clientApp.name}")
private String applicationName;
private final MesuresService mesuresService;
private final MesuresQueryService mesuresQueryService;
public MesuresResource(MesuresService mesuresService, MesuresQueryService mesuresQueryService) {
this.mesuresService = mesuresService;
this.mesuresQueryService = mesuresQueryService;
}
#GetMapping("/newMesure/{mac}/{value}")
public String newMesure(#PathVariable String mac,#PathVariable int value) {
log.debug("Adresse MAC : "+mac);
log.debug("Valeur : "+value);
#Query("SELECT valeur FROM Mesures WHERE id = 1") //not working
Mesures getValeur(); //not working
return "Mesure ajoutée";
}
}
In /src/main/java/com/mycompany/myapp/domain/Mesures.java :
/**
* A Mesures.
*/
#Entity
#Table(name = "mesures")
public class Mesures implements Serializable {
private static final long serialVersionUID = 1L;
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#Column(name = "valeur")
private Integer valeur;
#ManyToOne(optional = false)
#NotNull
#JsonIgnoreProperties("macs")
private Capteurs mac;
// jhipster-needle-entity-add-field - JHipster will add fields here, do not remove
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public Integer getValeur() {
return valeur;
}
public Mesures valeur(Integer valeur) {
this.valeur = valeur;
return this;
}
public void setValeur(Integer valeur) {
this.valeur = valeur;
}
public Capteurs getMac() {
return mac;
}
public Mesures mac(Capteurs capteurs) {
this.mac = capteurs;
return this;
}
public void setMac(Capteurs capteurs) {
this.mac = capteurs;
}
// jhipster-needle-entity-add-getters-setters - JHipster will add getters and setters here, do not remove
#Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (!(o instanceof Mesures)) {
return false;
}
return id != null && id.equals(((Mesures) o).id);
}
#Override
public int hashCode() {
return 31;
}
#Override
public String toString() {
return "Mesures{" +
"id=" + getId() +
", valeur=" + getValeur() +
"}";
}
}
Louan
Learning java with JHipster is probably not a wise idea, it uses a very rich technology stack which might lose you unless you invest enough time to learn the basics.
There are many things wrong in your code and approach:
You can't use #Query annotation inside the body of method a of your REST controller, it must be used in your #Repository interface, this code can't compile. See https://www.baeldung.com/spring-data-jpa-query for a quick introduction
JPA filtering is not related to inserting into database
In HTTP/REST, GET method is supposed to be idempotent. For making changes in your database you should use POST or PUT methods. See What is idempotency in HTTP methods?
Your entity naming convention is not consistent: use singular for entity classes because each entity object represents one single instance of Mesure. Here you have Plantes (plural), Capteur (singular) and Mesures (plural). For table names, JHipster uses singular but plural is quite common too because a table holds many rows. Of course, this is just a convention and you or your team may decide to apply another (like a prefix for table names) but the key point is to be consistent.

Spring boot JPA and Transactional : cannot reproduce a bug with a unit test

We have a method that adds a new record (credit card) and update an existing record. More specifically, we set the existing credit card as DISABLED and add a new one, created as ACTIVE.
#Service
#Transactional
public class CreditCardServiceImpl {
#Override
public CreditCard saveCard(Map<String, String> data, String cardType, String token, HashMap<String, String> card, String entityId, String fleetId, PspSettings pspSetting)
throws ParseException, ClientProtocolException, IOException, AdyenUnAuthorizedException, org.json.simple.parser.ParseException, AdyenException {
LOG.info("Check if creditCard exists for entityId#{}", entityId);
CreditCard creditCard = creditCardRepository.findByEntityIdAndStatus(entityId, CreditCardStatus.ACTIVE);
if (creditCard == null) {
creditCard = createCreditCard(data, cardType, token, card, entityId, fleetId);
} else {
LOG.info("Credit card#{} found, disable it and create a new one", creditCard.getId());
// disable the older card for this entity
creditCard.setStatus(CreditCardStatus.DISABLED);
creditCardRepository.save(creditCard);
// create a new card for this entity
creditCard = createCreditCard(data, cardType, token, card, entityId, fleetId);
// ***** SMELLLY ***
}
return creditCard;
}
that calls this createCreditCard method:
private CreditCard createCreditCard(Map<String, String> data, String cardType, String token, HashMap card, String entityId, String fleetId) throws ParseException {
LOG.info("Creating new card");
CreditCard creditCard = new CreditCard();
creditCard.setCardSummary(data.get("cardSummary"));
creditCard.setFleetId(fleetId);
if (data.get("expiryDate") != null) {
creditCard.setExpiryDate(formatter.parse(data.get("expiryDate")));
}
creditCard.setCardType(cardType);
creditCard.setToken(token);
creditCard.setEntityId(entityId);
creditCard.setStatus(CreditCardStatus.ACTIVE);
if (card != null) {
creditCard.setHolderName((String) card.get("holderName"));
}
LOG.info("Saving card# {}", creditCard.toString());
creditCardRepository.save(creditCard);
return creditCard;
}
}
We have a bug where we end up with two credit cards in the DB that are identical, both set as ACTIVE.
I suspect it s because we reuse the local creditCard var. (see the *** SMELLY ** line above)
So I think this can easily be fixed by using another local var instead of creditCard.
The troubling issue is that I cannot reproduce this in the unit test.
The below test passes.
#Test
public void disableAdyenTokenTest() {
//save a card first and then check that it exists to disable it
EntityX entityX = new EntityX();
// some stuff
entityX.setFleetId(BASE_FLEET);
entityX = entityService.save(entityX);
LOG.info("----- save credit card for test -----");
creditCardService.save(cc);
Map<String, String> data = new HashMap<String, String>();
String cardType="visa";
String newToken="0000000022";
HashMap card = new HashMap<String, String>();
Date date = new Date();
SimpleDateFormat formater = new SimpleDateFormat("mm/yyyy");
String dateString = formater.format(date);
data.put("expiryDate", dateString);
data.put("cardSummary", "cardSummary");
card.put("holderName", "Joe Louis");
CreditCard ccReturned = creditCardService.saveCard(data, cardType, newToken, card, entityX.getId(), BASE_FLEET, settings);
assertNotNull(ccReturned);
assertEquals(CreditCardStatus.ACTIVE, ccReturned.getStatus());
CreditCard activeCard = creditCardService.findByEntityIdAndStatus(entityX.getId(), CreditCardStatus.ACTIVE);
assertNotNull(activeCard);
CreditCard disabledCard = creditCardService.findByEntityIdAndStatus(entityX.getId(), CreditCardStatus.DISABLED);
assertNotNull(disabledCard);
List<PspTransaction> list = pspTransactionRepo.findAll();
assertNotNull(list);
}
Any idea how i can reproduce this issue in the unit tests?
Thanks a lot for having read this whole post already :)
APPENDIX:
CreditCard entity, with Id as PK.
#Entity
#Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE)
public class CreditCard implements Serializable {
/** The Constant serialVersionUID. */
private static final long serialVersionUID = 1L;
/** The id. */
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
....
UPDATE
The application runs with a MariaDB database whereas the unit tests run on H2 in memory db. Can this be why?

Spring JPA: Locking parent row when inserting one to many child record

We have two tables that have a one to many relationship. When we insert multiple records into the child table across multiple threads (more specifically across multiple REST web requests) we are running into lost update issues due to a race condition.
What we need to be able to do is have JPA recognize that the entity has been updated elsewhere prior to inserting the child record. I've tried using the #Version annotation approach but that doesn't seem to do the trick as the update/insert (I guess...) is happening on another table. I tried adding a version timestamp column on the parent table that is updated on every update but that didn't seem to do the trick either.
I think what I actually need to do is get a reference to the EntityManager directly so that I can issue a lock() command on the record prior to calling save(). I'm just too new to Spring to know if
A) that is indeed the correct approach,
B) if there is a better/easier way to do what we are trying to accomplish, and
C) how to actually do that.
Also, I am aware of the #OneToMany annotation but that didn't seem to do anything.
I've truncated the code below for brevity and I also created a trimmed down version of the code that demonstrates the problem and will hopefully make it easier to see what I am trying to do. In the test if you change the thread pool number to 1 you can see the test pass.
Engagement class:
#Entity
public class Engagement implements Serializable {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "id")
private Long id;
#ElementCollection(fetch = EAGER)
private List<String> assignedUsers;
#Version
private Long version;
private LocalDateTime updatedOn;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public Long getVersion(){return version;}
public void setVersion(Long version){this.version = version;}
public LocalDateTime getUpdatedOn(){
return updatedOn;
}
public void setUpdatedOn(LocalDateTime updatedOn) {
this.updatedOn = updatedOn;
}
public List<String> getAssignedUsers() {
return assignedUsers;
}
public void setAssignedUsers(List<String> assignedUsers) {
this.assignedUsers = assignedUsers;
}
public Engagement() {
}
}
User class:
public final class User {
private final String name;
private final String email;
private final String userId;
private final List<Engagement> engagements;
#ConstructorProperties({"roles", "name", "email", "userId", "engagements"})
User(String name, String email, String userId, List<Engagement> engagements) {
this.name = name;
this.email = email;
this.userId = userId;
this.engagements = engagements;
}
public static User.UserBuilder builder() {
return new User.UserBuilder();
}
public String getName() {
return this.name;
}
public String getEmail() {
return this.email;
}
public String getUserId() {
return this.userId;
}
public List<Engagement> getEngagements() {
return this.engagements;
}
public static final class UserBuilder {
private String name;
private String email;
private String userId;
private List<Engagement> engagements;
UserBuilder() {
}
public User.UserBuilder name(String name) {
this.name = name;
return this;
}
public User.UserBuilder email(String email) {
this.email = email;
return this;
}
public User.UserBuilder userId(String userId) {
this.userId = userId;
return this;
}
public User.UserBuilder engagements(List<Engagement> engagements) {
this.engagements = engagements;
return this;
}
public User build() {
return new User(this.name, this.email, this.userId, this.engagements);
}
public String toString() {
return "User.UserBuilder(name=" + this.name + ", email=" + this.email + ", userId=" + this.userId + ", engagements=" + this.engagements + ")";
}
}
}
Thread test:
#RunWith(SpringJUnit4ClassRunner.class)
#SpringBootTest
public class EngagementTest {
#Mock
UsersAuthService usersService;
#Autowired
EngagementsRepository engagementsRepository;
UsersAuthService authService;
#Before
public void init() {
MockitoAnnotations.initMocks(this);
authService = new UsersAuthServiceImpl(usersService, engagementsRepository);
}
#Test
public void addingMultipleUsersAtOnceSucceeds() throws InterruptedException {
Long engagementId = 1L;
String userId1 = "user1";
String userId2 = "user2";
String userId3 = "user3";
String userId4 = "user4";
String userId5 = "user5";
String auth = "asdf";
User adminUser = User.builder()
.userId("adminUser")
.email("user#user.com")
.name("Admin User")
.build();
Engagement engagement = new Engagement();
engagement.setAssignedUsers(new ArrayList<>());
engagement.getAssignedUsers().add(adminUser.getUserId());
engagementsRepository.save(engagement);
ExecutorService executorService = Executors.newFixedThreadPool(5);//change this to 1 to see the test pass
List<Callable<Engagement>> callableList = Arrays.asList(
addUserThread(engagementId, userId1, auth, adminUser),
addUserThread(engagementId, userId2, auth, adminUser),
addUserThread(engagementId, userId3, auth, adminUser),
addUserThread(engagementId, userId4, auth, adminUser),
addUserThread(engagementId, userId5, auth, adminUser));
executorService.invokeAll(callableList);
Engagement after = engagementsRepository.findById(engagementId);
assertEquals(6, after.getAssignedUsers().size());
}
private Callable<Engagement> addUserThread(Long engagementId, String userId1, String auth, User adminUser) {
return () -> authService.addUserTo(engagementId, userId1, auth, adminUser);
}
}
What's happening here is that you submit the callbacks for execution but never actually wait for their completion before checking the result. You need to use the List<Future<Engagement>> to actually wait for the results to complete before proceeding.
Something like this would do the trick:
executorService.invokeAll(callableList).forEach(it -> {
try {
it.get(500, TimeUnit.MILLISECONDS);
} catch (InterruptedException | ExecutionException | TimeoutException e) {
e.printStackTrace();
}
});
Note that this is not a proper way to deal with the exception case but it causes the code to wait for completion. If you have that in place you see the threads properly rejecting some of the updates with an ObjectOptimisticLockingFailureException:
java.util.concurrent.ExecutionException: org.springframework.orm.ObjectOptimisticLockingFailureException: Object of class [com.example.racecondition.engagement.Engagement] with identifier [1]: optimistic locking failed; nested exception is org.hibernate.StaleObjectStateException: Row was updated or deleted by another transaction (or unsaved-value mapping was incorrect) : [com.example.racecondition.engagement.Engagement#1]
at java.util.concurrent.FutureTask.report(FutureTask.java:122)
at java.util.concurrent.FutureTask.get(FutureTask.java:206)
at com.example.racecondition.EngagementTest.lambda$0(EngagementTest.java:68)
at java.util.ArrayList.forEach(ArrayList.java:1257)
at com.example.racecondition.EngagementTest.addingMultipleUsersAtOnceSucceeds(EngagementTest.java:66)
What's weird about the test case beyond that is that UsersAuthServiceImpl carries an #Transactional but the test case manually instantiates that class, so that there's no transactional proxy in place already. This causes the calls to findById(…) and save(…) from within addToUser(…) to run in two transactions. Tweaking that doesn't change the output though.
I think what I actually need to do is get a reference to the EntityManager directly so that I can issue a lock() command on the record prior to calling save(). I'm just too new to Spring to know if
A) that is indeed the correct approach,
If I understand you correctly you want to basically force a version increment on an entity so that if multiple threads do that one fails.
You can indeed achieve that by locking the entity in question using LockModeType.PESSIMISTIC_FORCE_INCREMENT or LockModeType.OPTIMISTIC_FORCE_INCREMENT.
B) if there is a better/easier way to do what we are trying to accomplish, and
C) how to actually do that.
With Spring Data probably the best way to do that is using the #Lock annotation on the method you use to load the entity.

Resources