Skip to content

Commit 01f9785

Browse files
committed
Added @retryable to avoid deadlocks.
Multiple threads trying to create role's with the same subset of applications under heavy load causes SQLTransactionRollbackException errors. To avoid this we retry the updateRole for max 3 times and also use additional index to prevent full-table scans.
1 parent 934d4cf commit 01f9785

12 files changed

Lines changed: 36 additions & 22 deletions

server/pom.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,11 @@
8989
<artifactId>mariadb-java-client</artifactId>
9090
<version>3.5.3</version>
9191
</dependency>
92+
<dependency>
93+
<groupId>org.springframework.retry</groupId>
94+
<artifactId>spring-retry</artifactId>
95+
<version>2.0.12</version>
96+
</dependency>
9297
<dependency>
9398
<groupId>com.github.spullara.mustache.java</groupId>
9499
<artifactId>compiler</artifactId>

server/src/main/java/access/api/RoleController.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,14 @@
2828
import org.springframework.data.domain.Sort;
2929
import org.springframework.http.MediaType;
3030
import org.springframework.http.ResponseEntity;
31+
import org.springframework.retry.annotation.Backoff;
32+
import org.springframework.retry.annotation.Retryable;
3133
import org.springframework.transaction.annotation.Transactional;
3234
import org.springframework.util.StringUtils;
3335
import org.springframework.validation.annotation.Validated;
3436
import org.springframework.web.bind.annotation.*;
3537

38+
import java.sql.SQLTransactionRollbackException;
3639
import java.util.*;
3740
import java.util.stream.Collectors;
3841

@@ -164,6 +167,11 @@ public ResponseEntity<Role> newRole(@Validated @RequestBody Role role,
164167
}
165168

166169
@PutMapping("")
170+
@Retryable(
171+
value = { SQLTransactionRollbackException.class },
172+
maxAttempts = 3,
173+
backoff = @Backoff(delay = 1000)
174+
)
167175
public ResponseEntity<Role> updateRole(@Validated @RequestBody Role role,
168176
@Parameter(hidden = true) User user) {
169177
LOG.debug(String.format("PUT /roles/ for user %s", user.getEduPersonPrincipalName()));

server/src/main/java/access/api/RoleOperations.java

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -41,13 +41,14 @@ public void syncRoleApplicationUsages(Role role) {
4141
.map(applicationUsageFromClient -> {
4242
Application application = applicationUsageFromClient.getApplication();
4343
Application applicationFromDB = applicationRepository
44-
.findByManageIdAndManageType(application.getManageId(), application.getManageType())
44+
.findByManageIdAndManageTypeOrderById(application.getManageId(), application.getManageType())
4545
.orElseGet(() -> applicationRepository.save(application));
46-
ApplicationUsage applicationUsageFromDB = applicationUsageRepository.findByRoleIdAndApplicationManageIdAndApplicationManageType(
47-
role.getId(),
48-
applicationFromDB.getManageId(),
49-
applicationFromDB.getManageType()
50-
).orElseGet(() -> new ApplicationUsage(applicationFromDB, applicationUsageFromClient.getLandingPage()));
46+
ApplicationUsage applicationUsageFromDB = applicationUsageRepository
47+
.findByRoleIdAndApplicationManageIdAndApplicationManageTypeOrderByApplicationId(
48+
role.getId(),
49+
applicationFromDB.getManageId(),
50+
applicationFromDB.getManageType()
51+
).orElseGet(() -> new ApplicationUsage(applicationFromDB, applicationUsageFromClient.getLandingPage()));
5152
applicationUsageFromDB.setLandingPage(applicationUsageFromClient.getLandingPage());
5253
return applicationUsageFromDB;
5354
})

server/src/main/java/access/repository/ApplicationRepository.java

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,6 @@
22

33
import access.manage.EntityType;
44
import access.model.Application;
5-
import org.springframework.data.domain.Page;
6-
import org.springframework.data.domain.Pageable;
75
import org.springframework.data.jpa.repository.JpaRepository;
86
import org.springframework.data.jpa.repository.Query;
97
import org.springframework.stereotype.Repository;
@@ -15,7 +13,7 @@
1513
@Repository
1614
public interface ApplicationRepository extends JpaRepository<Application, Long> {
1715

18-
Optional<Application> findByManageIdAndManageType(String manageId, EntityType manageType);
16+
Optional<Application> findByManageIdAndManageTypeOrderById(String manageId, EntityType manageType);
1917

2018
@Query(value = """
2119
SELECT count(r.id) as role_count, apps.manage_id as manage_id FROM roles r

server/src/main/java/access/repository/ApplicationUsageRepository.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
@Repository
1111
public interface ApplicationUsageRepository extends JpaRepository<ApplicationUsage, Long> {
1212

13-
Optional<ApplicationUsage> findByRoleIdAndApplicationManageIdAndApplicationManageType(
13+
Optional<ApplicationUsage> findByRoleIdAndApplicationManageIdAndApplicationManageTypeOrderByApplicationId(
1414
Long roleId, String manageId, EntityType manageType);
1515

1616
}

server/src/main/java/access/seed/PerformanceSeed.java

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212
import org.springframework.beans.factory.annotation.Value;
1313
import org.springframework.stereotype.Component;
1414

15-
import javax.sql.DataSource;
1615
import java.time.Instant;
1716
import java.time.temporal.ChronoUnit;
1817
import java.util.*;
@@ -178,7 +177,7 @@ private Role createRole(List<Map<String, Object>> providers, String institutionG
178177
private Application application(ManageIdentifier manageIdentifier) {
179178
String manageId = manageIdentifier.manageId();
180179
EntityType entityType = manageIdentifier.manageType();
181-
return applicationRepository.findByManageIdAndManageType(manageId, entityType).
180+
return applicationRepository.findByManageIdAndManageTypeOrderById(manageId, entityType).
182181
orElseGet(() -> applicationRepository.save(new Application(manageId, entityType)));
183182
}
184183

server/src/main/java/access/teams/TeamsController.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ public ResponseEntity<Map<String, Integer>> migrateTeam(@RequestBody Team team)
9595
Set<ApplicationUsage> applicationUsages = team.getApplications().stream()
9696
.map(applicationFromTeams -> {
9797
Optional<Application> optionalApplication = applicationRepository
98-
.findByManageIdAndManageType(applicationFromTeams.getManageId(), applicationFromTeams.getManageType());
98+
.findByManageIdAndManageTypeOrderById(applicationFromTeams.getManageId(), applicationFromTeams.getManageType());
9999
Application applicationFromDB = optionalApplication
100100
.orElseGet(() -> {
101101
// ID is also serialized from database teams object, need to prevent StaleObjectStateException
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
ALTER TABLE `applications`
2+
ADD INDEX `manage_id_index` (`manage_id`),
3+
ADD INDEX `manage_type_index` (`manage_type`);

server/src/test/java/access/AbstractTest.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -560,7 +560,7 @@ protected UnaryOperator<Map<String, Object>> institutionalAdminEntitlementOperat
560560
}
561561

562562
protected Set<ApplicationUsage> application(String manageId, EntityType entityType) {
563-
Application application = applicationRepository.findByManageIdAndManageType(manageId, entityType).
563+
Application application = applicationRepository.findByManageIdAndManageTypeOrderById(manageId, entityType).
564564
orElseGet(() -> applicationRepository.save(new Application(manageId, entityType)));
565565
return Set.of(new ApplicationUsage(application, "http://landingpage.com"));
566566
}
@@ -608,7 +608,7 @@ private void doSeed() {
608608
new Application("3", EntityType.SAML20_SP),
609609
new Application("6", EntityType.OIDC10_RP))
610610
.stream()
611-
.map(app -> applicationRepository.findByManageIdAndManageType(app.getManageId(), app.getManageType()).
611+
.map(app -> applicationRepository.findByManageIdAndManageTypeOrderById(app.getManageId(), app.getManageType()).
612612
orElseGet(() -> applicationRepository.save(new Application(app.getManageId(), app.getManageType()))))
613613
.collect(Collectors.toSet());
614614
Set<ApplicationUsage> applicationUsages = applications.stream().map(application -> new ApplicationUsage(application, "https://landingpage.com")).collect(Collectors.toSet());

server/src/test/java/access/api/RoleControllerTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ void createInvalidLandingPage() throws Exception {
122122
stubForManageProvisioning(List.of());
123123
stubForManageProvidersAllowedByIdP(ORGANISATION_GUID);
124124

125-
Application application = applicationRepository.findByManageIdAndManageType("1", EntityType.SAML20_SP).
125+
Application application = applicationRepository.findByManageIdAndManageTypeOrderById("1", EntityType.SAML20_SP).
126126
orElseGet(() -> applicationRepository.save(new Application("1", EntityType.SAML20_SP)));
127127
Set<ApplicationUsage> applications = Set.of(new ApplicationUsage(application, "bogus"));
128128
AccessCookieFilter accessCookieFilter = openIDConnectFlow("/api/v1/users/login", INSTITUTION_ADMIN_SUB);

0 commit comments

Comments
 (0)