Skip to content

Commit de13b74

Browse files
authored
fix: bulk migration stuck in processing (#263)
* fix: add error handling for multiple email verification * fix: proper checking of batchupdateexception * chore: changelog and build version update * fix: remove accidental ;
1 parent 9dfab66 commit de13b74

File tree

4 files changed

+83
-58
lines changed

4 files changed

+83
-58
lines changed

CHANGELOG.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,12 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

88
## [Unreleased]
99

10-
## [9.0.2]
10+
## [9.0.3]
11+
12+
- Fixes BatchUpdateException checks and error handling to prevent bulk import users stuck in `PROCESSING` state
1113

14+
## [9.0.2]
15+
1216
- Fixes `AuthRecipe#getUserByAccountInfo` to consider the tenantId instead of the appId when fetching the webauthn user
1317
- Changes dependency structure to avoid multiple dependency declarations for the same library
1418

build.gradle

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ plugins {
22
id 'java-library'
33
}
44

5-
version = "9.0.2"
5+
version = "9.0.3"
66

77
repositories {
88
mavenCentral()

src/main/java/io/supertokens/storage/postgresql/Start.java

Lines changed: 76 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -1046,10 +1046,9 @@ public void signUpMultipleViaBulkImport_Transaction(TransactionConnection connec
10461046
try {
10471047
Connection sqlConnection = (Connection) connection.getConnection();
10481048
EmailPasswordQueries.signUpMultipleForBulkImport_Transaction(this, sqlConnection, users);
1049-
} catch (StorageQueryException | SQLException | StorageTransactionLogicException e) {
1049+
} catch (StorageQueryException | StorageTransactionLogicException e) {
10501050
Throwable actual = e.getCause();
1051-
if (actual instanceof BatchUpdateException) {
1052-
BatchUpdateException batchUpdateException = (BatchUpdateException) actual;
1051+
if (actual instanceof BatchUpdateException batchUpdateException) {
10531052
Map<String, Exception> errorByPosition = new HashMap<>();
10541053
SQLException nextException = batchUpdateException.getNextException();
10551054
while (nextException != null) {
@@ -1271,6 +1270,15 @@ public void updateIsEmailVerified_Transaction(AppIdentifier appIdentifier, Trans
12711270
}
12721271
}
12731272

1273+
/**
1274+
* Update the isEmailVerified column for multiple users in the email verification table. This method is used in the
1275+
* Bulk Migration process.
1276+
* Important note: this method expects a Map of email to userId, but if there is an error in the batch processing,
1277+
* it will throw a BulkImportBatchInsertException with a map of userid to Exception, based on the position of the
1278+
* erroneous item in the batch.
1279+
* This means, that the underlying map implementation must be one that preserves iteration order (LinkedHashMap
1280+
* is a good choice) and this is the responsibility of the caller to ensure that the passed map is such.
1281+
*/
12741282
@Override
12751283
public void updateMultipleIsEmailVerified_Transaction(AppIdentifier appIdentifier, TransactionConnection con,
12761284
Map<String, String> emailToUserId, boolean isEmailVerified)
@@ -1280,23 +1288,35 @@ public void updateMultipleIsEmailVerified_Transaction(AppIdentifier appIdentifie
12801288
EmailVerificationQueries.updateMultipleUsersIsEmailVerified_Transaction(this, sqlCon, appIdentifier,
12811289
emailToUserId, isEmailVerified);
12821290
} catch (SQLException e) {
1283-
if (e instanceof PSQLException) {
1284-
PostgreSQLConfig config = Config.getConfig(this);
1285-
ServerErrorMessage serverMessage = ((PSQLException) e).getServerErrorMessage();
1291+
if (e instanceof BatchUpdateException batchUpdateException) {
1292+
SQLException nextException = batchUpdateException.getNextException();
1293+
Map<String, Exception> errorByPosition = new HashMap<>();
1294+
while (nextException != null) {
12861295

1287-
if (isForeignKeyConstraintError(serverMessage, config.getEmailVerificationTable(), "app_id")) {
1288-
throw new TenantOrAppNotFoundException(appIdentifier);
1289-
}
1290-
}
1296+
if (nextException instanceof PSQLException) {
1297+
PostgreSQLConfig config = Config.getConfig(this);
1298+
ServerErrorMessage serverMessage = ((PSQLException) nextException).getServerErrorMessage();
12911299

1292-
boolean isPSQLPrimKeyError = e instanceof PSQLException && isPrimaryKeyError(
1293-
((PSQLException) e).getServerErrorMessage(),
1294-
Config.getConfig(this).getEmailVerificationTable());
1300+
int position = getErroneousEntryPosition(batchUpdateException);
1301+
String userid = ((Map.Entry<String, String>) emailToUserId.entrySet().toArray()[position]).getKey();
1302+
if (isNullConstraintError(serverMessage, config.getEmailVerificationTable(), "email")) {
1303+
errorByPosition.put(userid, new NullPointerException("email is null"));
1304+
} else if (isPrimaryKeyError(serverMessage, config.getEmailVerificationTable())) {
1305+
errorByPosition.put(userid,
1306+
new DuplicateEmailException());
1307+
}
1308+
if (isForeignKeyConstraintError(serverMessage, config.getEmailVerificationTable(),
1309+
"app_id")) {
1310+
throw new TenantOrAppNotFoundException(appIdentifier);
1311+
}
1312+
}
12951313

1296-
if (!isEmailVerified || !isPSQLPrimKeyError) {
1314+
nextException = nextException.getNextException();
1315+
}
1316+
throw new StorageQueryException(
1317+
new BulkImportBatchInsertException("emailverification errors", errorByPosition));
1318+
}
12971319
throw new StorageQueryException(e);
1298-
}
1299-
// we do not throw an error since the email is already verified
13001320
}
13011321
}
13021322

@@ -1499,9 +1519,7 @@ public void importThirdPartyUsers_Transaction(TransactionConnection con,
14991519
Connection sqlCon = (Connection) con.getConnection();
15001520
ThirdPartyQueries.importUser_Transaction(this, sqlCon, usersToImport);
15011521
} catch (SQLException e) {
1502-
Throwable actual = e.getCause();
1503-
if (actual instanceof BatchUpdateException) {
1504-
BatchUpdateException batchUpdateException = (BatchUpdateException) actual;
1522+
if (e instanceof BatchUpdateException batchUpdateException) {
15051523
Map<String, Exception> errorByPosition = new HashMap<>();
15061524
SQLException nextException = batchUpdateException.getNextException();
15071525
while (nextException != null) {
@@ -1769,6 +1787,13 @@ private boolean isForeignKeyConstraintError(ServerErrorMessage serverMessage, St
17691787
&& serverMessage.getConstraint().equals(tableName + "_" + columnName + "_fkey");
17701788
}
17711789

1790+
private boolean isNullConstraintError(ServerErrorMessage serverMessage, String tableName, String columnName) {
1791+
String[] tableNameParts = tableName.split("\\.");
1792+
tableName = tableNameParts[tableNameParts.length - 1];
1793+
return serverMessage.getSQLState().equals("23502")
1794+
&& serverMessage.getMessage().contains("null value in column \"" + columnName + "\" of relation \"" + tableName + "\" violates not-null constraint");
1795+
}
1796+
17721797
private boolean isPrimaryKeyError(ServerErrorMessage serverMessage, String tableName) {
17731798
String[] tableNameParts = tableName.split("\\.");
17741799
tableName = tableNameParts[tableNameParts.length - 1];
@@ -2098,50 +2123,46 @@ public void importPasswordlessUsers_Transaction(TransactionConnection con,
20982123
Connection sqlCon = (Connection) con.getConnection();
20992124
PasswordlessQueries.importUsers_Transaction(sqlCon, this, users);
21002125
} catch (SQLException e) {
2101-
if (e instanceof BatchUpdateException) {
2102-
Throwable actual = e.getCause();
2103-
if (actual instanceof BatchUpdateException) {
2104-
BatchUpdateException batchUpdateException = (BatchUpdateException) actual;
2105-
Map<String, Exception> errorByPosition = new HashMap<>();
2106-
SQLException nextException = batchUpdateException.getNextException();
2107-
while (nextException != null) {
2126+
if (e instanceof BatchUpdateException batchUpdateException) {
2127+
Map<String, Exception> errorByPosition = new HashMap<>();
2128+
SQLException nextException = batchUpdateException.getNextException();
2129+
while (nextException != null) {
21082130

2109-
if (nextException instanceof PSQLException) {
2110-
PostgreSQLConfig config = Config.getConfig(this);
2111-
ServerErrorMessage serverMessage = ((PSQLException) nextException).getServerErrorMessage();
2131+
if (nextException instanceof PSQLException) {
2132+
PostgreSQLConfig config = Config.getConfig(this);
2133+
ServerErrorMessage serverMessage = ((PSQLException) nextException).getServerErrorMessage();
21122134

2113-
int position = getErroneousEntryPosition(batchUpdateException);
2135+
int position = getErroneousEntryPosition(batchUpdateException);
21142136

2115-
if (isPrimaryKeyError(serverMessage, config.getPasswordlessUsersTable())
2116-
|| isPrimaryKeyError(serverMessage, config.getUsersTable())
2117-
|| isPrimaryKeyError(serverMessage, config.getPasswordlessUserToTenantTable())
2118-
|| isPrimaryKeyError(serverMessage, config.getAppIdToUserIdTable())) {
2119-
errorByPosition.put(users.get(position).userId, new DuplicateUserIdException());
2120-
}
2121-
if (isUniqueConstraintError(serverMessage, config.getPasswordlessUserToTenantTable(),
2122-
"email")) {
2123-
errorByPosition.put(users.get(position).userId, new DuplicateEmailException());
2137+
if (isPrimaryKeyError(serverMessage, config.getPasswordlessUsersTable())
2138+
|| isPrimaryKeyError(serverMessage, config.getUsersTable())
2139+
|| isPrimaryKeyError(serverMessage, config.getPasswordlessUserToTenantTable())
2140+
|| isPrimaryKeyError(serverMessage, config.getAppIdToUserIdTable())) {
2141+
errorByPosition.put(users.get(position).userId, new DuplicateUserIdException());
2142+
}
2143+
if (isUniqueConstraintError(serverMessage, config.getPasswordlessUserToTenantTable(),
2144+
"email")) {
2145+
errorByPosition.put(users.get(position).userId, new DuplicateEmailException());
21242146

2125-
} else if (isUniqueConstraintError(serverMessage, config.getPasswordlessUserToTenantTable(),
2126-
"phone_number")) {
2127-
errorByPosition.put(users.get(position).userId, new DuplicatePhoneNumberException());
2147+
} else if (isUniqueConstraintError(serverMessage, config.getPasswordlessUserToTenantTable(),
2148+
"phone_number")) {
2149+
errorByPosition.put(users.get(position).userId, new DuplicatePhoneNumberException());
21282150

2129-
} else if (isForeignKeyConstraintError(serverMessage, config.getAppIdToUserIdTable(),
2130-
"app_id")) {
2131-
throw new TenantOrAppNotFoundException(users.get(position).tenantIdentifier.toAppIdentifier());
2151+
} else if (isForeignKeyConstraintError(serverMessage, config.getAppIdToUserIdTable(),
2152+
"app_id")) {
2153+
throw new TenantOrAppNotFoundException(users.get(position).tenantIdentifier.toAppIdentifier());
21322154

2133-
} else if (isForeignKeyConstraintError(serverMessage, config.getUsersTable(),
2134-
"tenant_id")) {
2135-
throw new TenantOrAppNotFoundException(users.get(position).tenantIdentifier.toAppIdentifier());
2136-
}
2155+
} else if (isForeignKeyConstraintError(serverMessage, config.getUsersTable(),
2156+
"tenant_id")) {
2157+
throw new TenantOrAppNotFoundException(users.get(position).tenantIdentifier.toAppIdentifier());
21372158
}
2138-
nextException = nextException.getNextException();
21392159
}
2140-
throw new StorageQueryException(
2141-
new BulkImportBatchInsertException("passwordless errors", errorByPosition));
2160+
nextException = nextException.getNextException();
21422161
}
2143-
throw new StorageQueryException(e);
2162+
throw new StorageQueryException(
2163+
new BulkImportBatchInsertException("passwordless errors", errorByPosition));
21442164
}
2165+
throw new StorageQueryException(e);
21452166
}
21462167
}
21472168

@@ -3627,8 +3648,7 @@ public void addBulkImportUsers(AppIdentifier appIdentifier, List<BulkImportUser>
36273648
try {
36283649
BulkImportQueries.insertBulkImportUsers_Transaction(this, (Connection) con.getConnection(), appIdentifier, users);
36293650
} catch (SQLException e) {
3630-
if (e instanceof BatchUpdateException) {
3631-
BatchUpdateException batchUpdateException = (BatchUpdateException) e;
3651+
if (e instanceof BatchUpdateException batchUpdateException) {
36323652
SQLException nextException = batchUpdateException.getNextException();
36333653
if(nextException instanceof PSQLException){
36343654
ServerErrorMessage serverErrorMessage = ((PSQLException) nextException).getServerErrorMessage();
@@ -3756,6 +3776,7 @@ public List<String> deleteBulkImportUsers(AppIdentifier appIdentifier, @Nonnull
37563776
try {
37573777
return BulkImportQueries.deleteBulkImportUsers(this, appIdentifier, bulkImportUserIds);
37583778
} catch (SQLException e) {
3779+
Logging.error(this, "Error deleting bulk import users", true, e);
37593780
throw new StorageQueryException(e);
37603781
}
37613782
}

src/main/java/io/supertokens/storage/postgresql/queries/EmailPasswordQueries.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -346,7 +346,7 @@ public static AuthRecipeUserInfo signUp(Start start, TenantIdentifier tenantIden
346346
}
347347

348348
public static void signUpMultipleForBulkImport_Transaction(Start start, Connection sqlCon, List<EmailPasswordImportUser> usersToSignUp)
349-
throws StorageQueryException, StorageTransactionLogicException, SQLException {
349+
throws StorageQueryException, StorageTransactionLogicException {
350350
try {
351351
String app_id_to_user_id_QUERY = "INSERT INTO " + getConfig(start).getAppIdToUserIdTable()
352352
+ "(app_id, user_id, primary_or_recipe_user_id, recipe_id)" + " VALUES(?, ?, ?, ?)";

0 commit comments

Comments
 (0)