From a48321c4dc6688559f99a9e3a9b4f9ff30e65e90 Mon Sep 17 00:00:00 2001 From: Sattvik Chakravarthy Date: Fri, 19 Apr 2024 12:24:55 +0530 Subject: [PATCH] fix: crash fixes (#117) --- CHANGELOG.md | 4 + build.gradle | 2 +- pluginInterfaceSupported.json | 2 +- .../storage/mysql/ConnectionPool.java | 32 +- .../io/supertokens/storage/mysql/Start.java | 36 +- .../storage/mysql/queries/GeneralQueries.java | 190 +++--- .../mysql/queries/MultitenancyQueries.java | 92 +-- .../supertokens/storage/mysql/test/Utils.java | 2 + .../TestForNoCrashDuringStartup.java | 563 ++++++++++++++++++ 9 files changed, 785 insertions(+), 138 deletions(-) create mode 100644 src/test/java/io/supertokens/storage/mysql/test/multitenancy/TestForNoCrashDuringStartup.java diff --git a/CHANGELOG.md b/CHANGELOG.md index 4eda62f..c8b22e1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +## [7.0.1] - 2024-04-17 + +- Fixes issues with partial failures during tenant creation + ## [7.0.0] - 2024-03-13 - Replace `TotpNotEnabledError` with `UnknownUserIdTotpError`. diff --git a/build.gradle b/build.gradle index 0bf0706..b8b5788 100644 --- a/build.gradle +++ b/build.gradle @@ -2,7 +2,7 @@ plugins { id 'java-library' } -version = "7.0.0" +version = "7.0.1" repositories { mavenCentral() diff --git a/pluginInterfaceSupported.json b/pluginInterfaceSupported.json index f9d5be7..476e2b8 100644 --- a/pluginInterfaceSupported.json +++ b/pluginInterfaceSupported.json @@ -1,6 +1,6 @@ { "_comment": "contains a list of plugin interfaces branch names that this core supports", "versions": [ - "6.0" + "6.1" ] } \ No newline at end of file diff --git a/src/main/java/io/supertokens/storage/mysql/ConnectionPool.java b/src/main/java/io/supertokens/storage/mysql/ConnectionPool.java index 7f496a5..8f5bd10 100644 --- a/src/main/java/io/supertokens/storage/mysql/ConnectionPool.java +++ b/src/main/java/io/supertokens/storage/mysql/ConnectionPool.java @@ -20,6 +20,7 @@ import com.zaxxer.hikari.HikariConfig; import com.zaxxer.hikari.HikariDataSource; import io.supertokens.pluginInterface.exceptions.DbInitException; +import io.supertokens.pluginInterface.exceptions.StorageQueryException; import io.supertokens.storage.mysql.config.Config; import io.supertokens.storage.mysql.config.MySQLConfig; import io.supertokens.storage.mysql.output.Logging; @@ -36,12 +37,14 @@ public class ConnectionPool extends ResourceDistributor.SingletonResource { private HikariDataSource hikariDataSource = null; private final Start start; + private PostConnectCallback postConnectCallback; - private ConnectionPool(Start start) { + private ConnectionPool(Start start, PostConnectCallback postConnectCallback) { this.start = start; + this.postConnectCallback = postConnectCallback; } - private synchronized void initialiseHikariDataSource() throws SQLException { + private synchronized void initialiseHikariDataSource() throws SQLException, StorageQueryException { if (this.hikariDataSource != null) { return; } @@ -98,6 +101,19 @@ private synchronized void initialiseHikariDataSource() throws SQLException { } catch (Exception e) { throw new SQLException(e); } + + try { + try (Connection con = hikariDataSource.getConnection()) { + this.postConnectCallback.apply(con); + } + } catch (StorageQueryException e) { + // if an exception happens here, we want to set the hikariDataSource to null once again so that + // whenever the getConnection is called again, we want to re-attempt creation of tables and tenant + // entries for this storage + hikariDataSource.close(); + hikariDataSource = null; + throw e; + } } private static int getTimeToWaitToInit(Start start) { @@ -132,7 +148,8 @@ static boolean isAlreadyInitialised(Start start) { return getInstance(start) != null && getInstance(start).hikariDataSource != null; } - static void initPool(Start start, boolean shouldWait) throws DbInitException, SQLException { + static void initPool(Start start, boolean shouldWait, PostConnectCallback postConnectCallback) + throws DbInitException, SQLException, StorageQueryException { if (isAlreadyInitialised(start)) { return; } @@ -143,7 +160,7 @@ static void initPool(Start start, boolean shouldWait) throws DbInitException, SQ + "you have" + " specified the correct values for ('mysql_host' and 'mysql_port') or for 'mysql_connection_uri'"; try { - ConnectionPool con = new ConnectionPool(start); + ConnectionPool con = new ConnectionPool(start, postConnectCallback); start.getResourceDistributor().setResource(RESOURCE_KEY, con); while (true) { try { @@ -185,7 +202,7 @@ static void initPool(Start start, boolean shouldWait) throws DbInitException, SQ } } - public static Connection getConnection(Start start) throws SQLException { + public static Connection getConnection(Start start) throws SQLException, StorageQueryException { if (getInstance(start) == null) { throw new IllegalStateException("Please call initPool before getConnection"); } @@ -212,4 +229,9 @@ static void close(Start start) { } } } + + @FunctionalInterface + public static interface PostConnectCallback { + void apply(Connection connection) throws StorageQueryException; + } } diff --git a/src/main/java/io/supertokens/storage/mysql/Start.java b/src/main/java/io/supertokens/storage/mysql/Start.java index a90fe27..2ac5ff5 100644 --- a/src/main/java/io/supertokens/storage/mysql/Start.java +++ b/src/main/java/io/supertokens/storage/mysql/Start.java @@ -218,7 +218,7 @@ public void stopLogging() { } @Override - public void initStorage(boolean shouldWait) throws DbInitException { + public void initStorage(boolean shouldWait, List tenantIdentifiers) throws DbInitException { if (ConnectionPool.isAlreadyInitialised(this)) { return; } @@ -228,8 +228,20 @@ public void initStorage(boolean shouldWait) throws DbInitException { mainThread = Thread.currentThread(); } try { - ConnectionPool.initPool(this, shouldWait); - GeneralQueries.createTablesIfNotExists(this); + ConnectionPool.initPool(this, shouldWait, (con) -> { + try { + GeneralQueries.createTablesIfNotExists(this, con); + } catch (SQLException e) { + throw new StorageQueryException(e); + } + for (TenantIdentifier tenantIdentifier : tenantIdentifiers) { + try { + this.addTenantIdInTargetStorage_Transaction(con, tenantIdentifier); + } catch (DuplicateTenantException e) { + // ignore + } + } + }); } catch (Exception e) { throw new DbInitException(e); } @@ -434,7 +446,7 @@ public void deleteAllInformation() throws StorageQueryException { } ProcessState.getInstance(this).clear(); try { - initStorage(false); + initStorage(false, new ArrayList<>()); enabled = true; // Allow get connection to work, to delete the data GeneralQueries.deleteAllTables(this); @@ -2227,6 +2239,22 @@ public void addTenantIdInTargetStorage(TenantIdentifier tenantIdentifier) } } + public void addTenantIdInTargetStorage_Transaction(Connection con, TenantIdentifier tenantIdentifier) + throws DuplicateTenantException, StorageQueryException { + try { + MultitenancyQueries.addTenantIdInTargetStorage_Transaction(this, con, tenantIdentifier); + } catch (SQLException e) { + if (e instanceof SQLIntegrityConstraintViolationException) { + String errorMessage = e.getMessage(); + if (isPrimaryKeyError(errorMessage, Config.getConfig(this).getTenantsTable())) { + throw new DuplicateTenantException(); + } + } + throw new StorageQueryException(e); + } + } + + @Override public void overwriteTenantConfig(TenantConfig tenantConfig) throws TenantOrAppNotFoundException, StorageQueryException, DuplicateThirdPartyIdException, diff --git a/src/main/java/io/supertokens/storage/mysql/queries/GeneralQueries.java b/src/main/java/io/supertokens/storage/mysql/queries/GeneralQueries.java index 66461f3..8df206c 100644 --- a/src/main/java/io/supertokens/storage/mysql/queries/GeneralQueries.java +++ b/src/main/java/io/supertokens/storage/mysql/queries/GeneralQueries.java @@ -61,13 +61,17 @@ public class GeneralQueries { - private static boolean doesTableExists(Start start, String tableName) { + private static boolean doesTableExists(Start start, Connection con, String tableName) + throws SQLException, StorageQueryException { try { String QUERY = "SELECT 1 FROM " + tableName + " LIMIT 1"; - execute(start, QUERY, NO_OP_SETTER, result -> null); + execute(con, QUERY, NO_OP_SETTER, result -> null); return true; } catch (SQLException | StorageQueryException e) { - return false; + if (e.getMessage().contains(tableName) && e.getMessage().contains("doesn't exist")) { + return false; + } + throw e; } } @@ -197,217 +201,217 @@ static String getQueryToCreateUserIdIndexForAppIdToUserIdTable(Start start) { + "(user_id);"; } - public static void createTablesIfNotExists(Start start) throws SQLException, StorageQueryException { - if (!doesTableExists(start, Config.getConfig(start).getAppsTable())) { + public static void createTablesIfNotExists(Start start, Connection con) throws SQLException, StorageQueryException { + if (!doesTableExists(start, con, Config.getConfig(start).getAppsTable())) { getInstance(start).addState(CREATING_NEW_TABLE, null); - update(start, getQueryToCreateAppsTable(start), NO_OP_SETTER); + update(con, getQueryToCreateAppsTable(start), NO_OP_SETTER); } - if (!doesTableExists(start, Config.getConfig(start).getTenantsTable())) { + if (!doesTableExists(start, con, Config.getConfig(start).getTenantsTable())) { getInstance(start).addState(CREATING_NEW_TABLE, null); - update(start, getQueryToCreateTenantsTable(start), NO_OP_SETTER); + update(con, getQueryToCreateTenantsTable(start), NO_OP_SETTER); } - if (!doesTableExists(start, Config.getConfig(start).getKeyValueTable())) { + if (!doesTableExists(start, con, Config.getConfig(start).getKeyValueTable())) { getInstance(start).addState(CREATING_NEW_TABLE, null); - update(start, getQueryToCreateKeyValueTable(start), NO_OP_SETTER); + update(con, getQueryToCreateKeyValueTable(start), NO_OP_SETTER); } - if (!doesTableExists(start, Config.getConfig(start).getAppIdToUserIdTable())) { + if (!doesTableExists(start, con, Config.getConfig(start).getAppIdToUserIdTable())) { getInstance(start).addState(CREATING_NEW_TABLE, null); - update(start, getQueryToCreateAppIdToUserIdTable(start), NO_OP_SETTER); + update(con, getQueryToCreateAppIdToUserIdTable(start), NO_OP_SETTER); - update(start, getQueryToCreatePrimaryUserIdIndexForAppIdToUserIdTable(start), NO_OP_SETTER); - update(start, getQueryToCreateUserIdIndexForAppIdToUserIdTable(start), NO_OP_SETTER); + update(con, getQueryToCreatePrimaryUserIdIndexForAppIdToUserIdTable(start), NO_OP_SETTER); + update(con, getQueryToCreateUserIdIndexForAppIdToUserIdTable(start), NO_OP_SETTER); } - if (!doesTableExists(start, Config.getConfig(start).getUsersTable())) { + if (!doesTableExists(start, con, Config.getConfig(start).getUsersTable())) { getInstance(start).addState(CREATING_NEW_TABLE, null); - update(start, getQueryToCreateUsersTable(start), NO_OP_SETTER); + update(con, getQueryToCreateUsersTable(start), NO_OP_SETTER); // index - update(start, getQueryToCreateUserPaginationIndex1(start), NO_OP_SETTER); - update(start, getQueryToCreateUserPaginationIndex3(start), NO_OP_SETTER); - update(start, getQueryToCreatePrimaryUserId(start), NO_OP_SETTER); - update(start, getQueryToCreateRecipeIdIndex(start), NO_OP_SETTER); + update(con, getQueryToCreateUserPaginationIndex1(start), NO_OP_SETTER); + update(con, getQueryToCreateUserPaginationIndex3(start), NO_OP_SETTER); + update(con, getQueryToCreatePrimaryUserId(start), NO_OP_SETTER); + update(con, getQueryToCreateRecipeIdIndex(start), NO_OP_SETTER); } - if (!doesTableExists(start, Config.getConfig(start).getUserLastActiveTable())) { + if (!doesTableExists(start, con, Config.getConfig(start).getUserLastActiveTable())) { getInstance(start).addState(CREATING_NEW_TABLE, null); - update(start, ActiveUsersQueries.getQueryToCreateUserLastActiveTable(start), NO_OP_SETTER); + update(con, ActiveUsersQueries.getQueryToCreateUserLastActiveTable(start), NO_OP_SETTER); } - if (!doesTableExists(start, Config.getConfig(start).getAccessTokenSigningKeysTable())) { + if (!doesTableExists(start, con, Config.getConfig(start).getAccessTokenSigningKeysTable())) { getInstance(start).addState(CREATING_NEW_TABLE, null); - update(start, getQueryToCreateAccessTokenSigningKeysTable(start), NO_OP_SETTER); + update(con, getQueryToCreateAccessTokenSigningKeysTable(start), NO_OP_SETTER); } - if (!doesTableExists(start, Config.getConfig(start).getSessionInfoTable())) { + if (!doesTableExists(start, con, Config.getConfig(start).getSessionInfoTable())) { getInstance(start).addState(CREATING_NEW_TABLE, null); - update(start, getQueryToCreateSessionInfoTable(start), NO_OP_SETTER); + update(con, getQueryToCreateSessionInfoTable(start), NO_OP_SETTER); // index - update(start, getQueryToCreateSessionExpiryIndex(start), NO_OP_SETTER); + update(con, getQueryToCreateSessionExpiryIndex(start), NO_OP_SETTER); } - if (!doesTableExists(start, Config.getConfig(start).getTenantConfigsTable())) { + if (!doesTableExists(start, con, Config.getConfig(start).getTenantConfigsTable())) { getInstance(start).addState(CREATING_NEW_TABLE, null); - update(start, MultitenancyQueries.getQueryToCreateTenantConfigsTable(start), NO_OP_SETTER); + update(con, MultitenancyQueries.getQueryToCreateTenantConfigsTable(start), NO_OP_SETTER); } - if (!doesTableExists(start, Config.getConfig(start).getTenantFirstFactorsTable())) { + if (!doesTableExists(start, con, Config.getConfig(start).getTenantFirstFactorsTable())) { getInstance(start).addState(CREATING_NEW_TABLE, null); - update(start, MultitenancyQueries.getQueryToCreateFirstFactorsTable(start), NO_OP_SETTER); + update(con, MultitenancyQueries.getQueryToCreateFirstFactorsTable(start), NO_OP_SETTER); } - if (!doesTableExists(start, Config.getConfig(start).getTenantRequiredSecondaryFactorsTable())) { + if (!doesTableExists(start, con, Config.getConfig(start).getTenantRequiredSecondaryFactorsTable())) { getInstance(start).addState(CREATING_NEW_TABLE, null); - update(start, MultitenancyQueries.getQueryToCreateRequiredSecondaryFactorsTable(start), NO_OP_SETTER); + update(con, MultitenancyQueries.getQueryToCreateRequiredSecondaryFactorsTable(start), NO_OP_SETTER); } - if (!doesTableExists(start, Config.getConfig(start).getTenantThirdPartyProvidersTable())) { + if (!doesTableExists(start, con, Config.getConfig(start).getTenantThirdPartyProvidersTable())) { getInstance(start).addState(CREATING_NEW_TABLE, null); - update(start, MultitenancyQueries.getQueryToCreateTenantThirdPartyProvidersTable(start), + update(con, MultitenancyQueries.getQueryToCreateTenantThirdPartyProvidersTable(start), NO_OP_SETTER); } - if (!doesTableExists(start, Config.getConfig(start).getTenantThirdPartyProviderClientsTable())) { + if (!doesTableExists(start, con, Config.getConfig(start).getTenantThirdPartyProviderClientsTable())) { getInstance(start).addState(CREATING_NEW_TABLE, null); - update(start, MultitenancyQueries.getQueryToCreateTenantThirdPartyProviderClientsTable(start), + update(con, MultitenancyQueries.getQueryToCreateTenantThirdPartyProviderClientsTable(start), NO_OP_SETTER); } - if (!doesTableExists(start, Config.getConfig(start).getEmailPasswordUsersTable())) { + if (!doesTableExists(start, con, Config.getConfig(start).getEmailPasswordUsersTable())) { getInstance(start).addState(CREATING_NEW_TABLE, null); - update(start, EmailPasswordQueries.getQueryToCreateUsersTable(start), NO_OP_SETTER); + update(con, EmailPasswordQueries.getQueryToCreateUsersTable(start), NO_OP_SETTER); } - if (!doesTableExists(start, Config.getConfig(start).getEmailPasswordUserToTenantTable())) { + if (!doesTableExists(start, con, Config.getConfig(start).getEmailPasswordUserToTenantTable())) { getInstance(start).addState(CREATING_NEW_TABLE, null); - update(start, EmailPasswordQueries.getQueryToCreateEmailPasswordUserToTenantTable(start), + update(con, EmailPasswordQueries.getQueryToCreateEmailPasswordUserToTenantTable(start), NO_OP_SETTER); } - if (!doesTableExists(start, Config.getConfig(start).getPasswordResetTokensTable())) { + if (!doesTableExists(start, con, Config.getConfig(start).getPasswordResetTokensTable())) { getInstance(start).addState(CREATING_NEW_TABLE, null); - update(start, getQueryToCreatePasswordResetTokensTable(start), NO_OP_SETTER); + update(con, getQueryToCreatePasswordResetTokensTable(start), NO_OP_SETTER); // index - update(start, getQueryToCreatePasswordResetTokenExpiryIndex(start), NO_OP_SETTER); + update(con, getQueryToCreatePasswordResetTokenExpiryIndex(start), NO_OP_SETTER); } - if (!doesTableExists(start, Config.getConfig(start).getEmailVerificationTable())) { + if (!doesTableExists(start, con, Config.getConfig(start).getEmailVerificationTable())) { getInstance(start).addState(CREATING_NEW_TABLE, null); - update(start, getQueryToCreateEmailVerificationTable(start), NO_OP_SETTER); + update(con, getQueryToCreateEmailVerificationTable(start), NO_OP_SETTER); } - if (!doesTableExists(start, Config.getConfig(start).getEmailVerificationTokensTable())) { + if (!doesTableExists(start, con, Config.getConfig(start).getEmailVerificationTokensTable())) { getInstance(start).addState(CREATING_NEW_TABLE, null); - update(start, getQueryToCreateEmailVerificationTokensTable(start), NO_OP_SETTER); + update(con, getQueryToCreateEmailVerificationTokensTable(start), NO_OP_SETTER); // index - update(start, getQueryToCreateEmailVerificationTokenExpiryIndex(start), NO_OP_SETTER); + update(con, getQueryToCreateEmailVerificationTokenExpiryIndex(start), NO_OP_SETTER); } - if (!doesTableExists(start, Config.getConfig(start).getThirdPartyUsersTable())) { + if (!doesTableExists(start, con, Config.getConfig(start).getThirdPartyUsersTable())) { getInstance(start).addState(CREATING_NEW_TABLE, null); - update(start, ThirdPartyQueries.getQueryToCreateUsersTable(start), NO_OP_SETTER); + update(con, ThirdPartyQueries.getQueryToCreateUsersTable(start), NO_OP_SETTER); // index - update(start, ThirdPartyQueries.getQueryToThirdPartyUserEmailIndex(start), NO_OP_SETTER); - update(start, ThirdPartyQueries.getQueryToThirdPartyUserIdIndex(start), NO_OP_SETTER); + update(con, ThirdPartyQueries.getQueryToThirdPartyUserEmailIndex(start), NO_OP_SETTER); + update(con, ThirdPartyQueries.getQueryToThirdPartyUserIdIndex(start), NO_OP_SETTER); } - if (!doesTableExists(start, Config.getConfig(start).getThirdPartyUserToTenantTable())) { + if (!doesTableExists(start, con, Config.getConfig(start).getThirdPartyUserToTenantTable())) { getInstance(start).addState(CREATING_NEW_TABLE, null); - update(start, ThirdPartyQueries.getQueryToCreateThirdPartyUserToTenantTable(start), NO_OP_SETTER); + update(con, ThirdPartyQueries.getQueryToCreateThirdPartyUserToTenantTable(start), NO_OP_SETTER); } - if (!doesTableExists(start, Config.getConfig(start).getJWTSigningKeysTable())) { + if (!doesTableExists(start, con, Config.getConfig(start).getJWTSigningKeysTable())) { getInstance(start).addState(CREATING_NEW_TABLE, null); - update(start, getQueryToCreateJWTSigningTable(start), NO_OP_SETTER); + update(con, getQueryToCreateJWTSigningTable(start), NO_OP_SETTER); } - if (!doesTableExists(start, Config.getConfig(start).getPasswordlessUsersTable())) { + if (!doesTableExists(start, con, Config.getConfig(start).getPasswordlessUsersTable())) { getInstance(start).addState(CREATING_NEW_TABLE, null); - update(start, PasswordlessQueries.getQueryToCreateUsersTable(start), NO_OP_SETTER); + update(con, PasswordlessQueries.getQueryToCreateUsersTable(start), NO_OP_SETTER); } - if (!doesTableExists(start, Config.getConfig(start).getPasswordlessUserToTenantTable())) { + if (!doesTableExists(start, con, Config.getConfig(start).getPasswordlessUserToTenantTable())) { getInstance(start).addState(CREATING_NEW_TABLE, null); - update(start, PasswordlessQueries.getQueryToCreatePasswordlessUserToTenantTable(start), + update(con, PasswordlessQueries.getQueryToCreatePasswordlessUserToTenantTable(start), NO_OP_SETTER); } - if (!doesTableExists(start, Config.getConfig(start).getPasswordlessDevicesTable())) { + if (!doesTableExists(start, con, Config.getConfig(start).getPasswordlessDevicesTable())) { getInstance(start).addState(CREATING_NEW_TABLE, null); - update(start, getQueryToCreateDevicesTable(start), NO_OP_SETTER); + update(con, getQueryToCreateDevicesTable(start), NO_OP_SETTER); // index - update(start, getQueryToCreateDeviceEmailIndex(start), NO_OP_SETTER); - update(start, getQueryToCreateDevicePhoneNumberIndex(start), NO_OP_SETTER); + update(con, getQueryToCreateDeviceEmailIndex(start), NO_OP_SETTER); + update(con, getQueryToCreateDevicePhoneNumberIndex(start), NO_OP_SETTER); } - if (!doesTableExists(start, Config.getConfig(start).getPasswordlessCodesTable())) { + if (!doesTableExists(start, con, Config.getConfig(start).getPasswordlessCodesTable())) { getInstance(start).addState(CREATING_NEW_TABLE, null); - update(start, getQueryToCreateCodesTable(start), NO_OP_SETTER); + update(con, getQueryToCreateCodesTable(start), NO_OP_SETTER); // index - update(start, getQueryToCreateCodeCreatedAtIndex(start), NO_OP_SETTER); + update(con, getQueryToCreateCodeCreatedAtIndex(start), NO_OP_SETTER); } - if (!doesTableExists(start, Config.getConfig(start).getUserMetadataTable())) { + if (!doesTableExists(start, con, Config.getConfig(start).getUserMetadataTable())) { getInstance(start).addState(CREATING_NEW_TABLE, null); - update(start, getQueryToCreateUserMetadataTable(start), NO_OP_SETTER); + update(con, getQueryToCreateUserMetadataTable(start), NO_OP_SETTER); } - if (!doesTableExists(start, Config.getConfig(start).getRolesTable())) { + if (!doesTableExists(start, con, Config.getConfig(start).getRolesTable())) { getInstance(start).addState(CREATING_NEW_TABLE, null); - update(start, UserRolesQueries.getQueryToCreateRolesTable(start), NO_OP_SETTER); + update(con, UserRolesQueries.getQueryToCreateRolesTable(start), NO_OP_SETTER); } - if (!doesTableExists(start, Config.getConfig(start).getUserRolesPermissionsTable())) { + if (!doesTableExists(start, con, Config.getConfig(start).getUserRolesPermissionsTable())) { getInstance(start).addState(CREATING_NEW_TABLE, null); - update(start, UserRolesQueries.getQueryToCreateRolePermissionsTable(start), NO_OP_SETTER); + update(con, UserRolesQueries.getQueryToCreateRolePermissionsTable(start), NO_OP_SETTER); // index - update(start, UserRolesQueries.getQueryToCreateRolePermissionsPermissionIndex(start), NO_OP_SETTER); + update(con, UserRolesQueries.getQueryToCreateRolePermissionsPermissionIndex(start), NO_OP_SETTER); } - if (!doesTableExists(start, Config.getConfig(start).getUserRolesTable())) { + if (!doesTableExists(start, con, Config.getConfig(start).getUserRolesTable())) { getInstance(start).addState(CREATING_NEW_TABLE, null); - update(start, UserRolesQueries.getQueryToCreateUserRolesTable(start), NO_OP_SETTER); + update(con, UserRolesQueries.getQueryToCreateUserRolesTable(start), NO_OP_SETTER); // index - update(start, UserRolesQueries.getQueryToCreateUserRolesRoleIndex(start), NO_OP_SETTER); + update(con, UserRolesQueries.getQueryToCreateUserRolesRoleIndex(start), NO_OP_SETTER); } - if (!doesTableExists(start, Config.getConfig(start).getUserIdMappingTable())) { + if (!doesTableExists(start, con, Config.getConfig(start).getUserIdMappingTable())) { getInstance(start).addState(CREATING_NEW_TABLE, null); - update(start, UserIdMappingQueries.getQueryToCreateUserIdMappingTable(start), NO_OP_SETTER); + update(con, UserIdMappingQueries.getQueryToCreateUserIdMappingTable(start), NO_OP_SETTER); } - if (!doesTableExists(start, Config.getConfig(start).getDashboardUsersTable())) { + if (!doesTableExists(start, con, Config.getConfig(start).getDashboardUsersTable())) { getInstance(start).addState(CREATING_NEW_TABLE, null); - update(start, DashboardQueries.getQueryToCreateDashboardUsersTable(start), NO_OP_SETTER); + update(con, DashboardQueries.getQueryToCreateDashboardUsersTable(start), NO_OP_SETTER); } - if (!doesTableExists(start, Config.getConfig(start).getDashboardSessionsTable())) { + if (!doesTableExists(start, con, Config.getConfig(start).getDashboardSessionsTable())) { getInstance(start).addState(CREATING_NEW_TABLE, null); - update(start, DashboardQueries.getQueryToCreateDashboardUserSessionsTable(start), NO_OP_SETTER); + update(con, DashboardQueries.getQueryToCreateDashboardUserSessionsTable(start), NO_OP_SETTER); // index - update(start, DashboardQueries.getQueryToCreateDashboardUserSessionsExpiryIndex(start), + update(con, DashboardQueries.getQueryToCreateDashboardUserSessionsExpiryIndex(start), NO_OP_SETTER); } - if (!doesTableExists(start, Config.getConfig(start).getTotpUsersTable())) { + if (!doesTableExists(start, con, Config.getConfig(start).getTotpUsersTable())) { getInstance(start).addState(CREATING_NEW_TABLE, null); - update(start, TOTPQueries.getQueryToCreateUsersTable(start), NO_OP_SETTER); + update(con, TOTPQueries.getQueryToCreateUsersTable(start), NO_OP_SETTER); } - if (!doesTableExists(start, Config.getConfig(start).getTotpUserDevicesTable())) { + if (!doesTableExists(start, con, Config.getConfig(start).getTotpUserDevicesTable())) { getInstance(start).addState(CREATING_NEW_TABLE, null); - update(start, TOTPQueries.getQueryToCreateUserDevicesTable(start), NO_OP_SETTER); + update(con, TOTPQueries.getQueryToCreateUserDevicesTable(start), NO_OP_SETTER); } - if (!doesTableExists(start, Config.getConfig(start).getTotpUsedCodesTable())) { + if (!doesTableExists(start, con, Config.getConfig(start).getTotpUsedCodesTable())) { getInstance(start).addState(CREATING_NEW_TABLE, null); - update(start, TOTPQueries.getQueryToCreateUsedCodesTable(start), NO_OP_SETTER); + update(con, TOTPQueries.getQueryToCreateUsedCodesTable(start), NO_OP_SETTER); // index: - update(start, TOTPQueries.getQueryToCreateUsedCodesExpiryTimeIndex(start), NO_OP_SETTER); + update(con, TOTPQueries.getQueryToCreateUsedCodesExpiryTimeIndex(start), NO_OP_SETTER); } } @@ -1514,7 +1518,7 @@ public static Map> getTenantIdsForUserIds(Start start, } @TestOnly - public static String[] getAllTablesInTheDatabase(Start start) throws SQLException { + public static String[] getAllTablesInTheDatabase(Start start) throws SQLException, StorageQueryException { if (!Start.isTesting) { throw new UnsupportedOperationException(); } diff --git a/src/main/java/io/supertokens/storage/mysql/queries/MultitenancyQueries.java b/src/main/java/io/supertokens/storage/mysql/queries/MultitenancyQueries.java index 53923d1..179e587 100644 --- a/src/main/java/io/supertokens/storage/mysql/queries/MultitenancyQueries.java +++ b/src/main/java/io/supertokens/storage/mysql/queries/MultitenancyQueries.java @@ -37,10 +37,13 @@ import java.sql.SQLIntegrityConstraintViolationException; import java.util.HashMap; +import static io.supertokens.storage.mysql.QueryExecutorTemplate.execute; import static io.supertokens.storage.mysql.QueryExecutorTemplate.update; import static io.supertokens.storage.mysql.config.Config.getConfig; public class MultitenancyQueries { + public static boolean simulateErrorInAddingTenantIdInTargetStorage_forTesting = false; + static String getQueryToCreateTenantConfigsTable(Start start) { String tenantConfigsTable = Config.getConfig(start).getTenantConfigsTable(); // @formatter:off @@ -272,41 +275,8 @@ public static void addTenantIdInTargetStorage(Start start, TenantIdentifier tena { start.startTransaction(con -> { Connection sqlCon = (Connection) con.getConnection(); - long currentTime = System.currentTimeMillis(); try { - { - // ON CONFLICT DO NOTHING - String QUERY = "INSERT INTO " + getConfig(start).getAppsTable() - + "(app_id, created_at_time) " - + "SELECT ?, ? WHERE NOT EXISTS (" - + " SELECT app_id FROM " + getConfig(start).getAppsTable() - + " WHERE app_id = ?" - + ")"; - update(sqlCon, QUERY, pst -> { - pst.setString(1, tenantIdentifier.getAppId()); - pst.setLong(2, currentTime); - pst.setString(3, tenantIdentifier.getAppId()); - }); - } - - { - // ON CONFLICT DO NOTHING - String QUERY = "INSERT INTO " + getConfig(start).getTenantsTable() - + "(app_id, tenant_id, created_at_time)" - + "SELECT ?, ?, ? WHERE NOT EXISTS (" - + " SELECT app_id, tenant_id FROM " + getConfig(start).getTenantsTable() - + " WHERE app_id = ? AND tenant_id = ?" - + ")"; - - update(sqlCon, QUERY, pst -> { - pst.setString(1, tenantIdentifier.getAppId()); - pst.setString(2, tenantIdentifier.getTenantId()); - pst.setLong(3, currentTime); - pst.setString(4, tenantIdentifier.getAppId()); - pst.setString(5, tenantIdentifier.getTenantId()); - }); - } - + addTenantIdInTargetStorage_Transaction(start, sqlCon, tenantIdentifier); sqlCon.commit(); } catch (SQLException throwables) { throw new StorageTransactionLogicException(throwables); @@ -316,6 +286,60 @@ public static void addTenantIdInTargetStorage(Start start, TenantIdentifier tena } } + public static void addTenantIdInTargetStorage_Transaction(Start start, Connection con, + TenantIdentifier tenantIdentifier) throws + SQLException, StorageQueryException { + + if (Start.isTesting && simulateErrorInAddingTenantIdInTargetStorage_forTesting) { + String QUERY = "SELECT 1 FROM " + getConfig(start).getAppsTable() + " WHERE app_id = ?"; + int val = execute(con, QUERY, pst -> { + pst.setString(1, tenantIdentifier.getAppId()); + }, rs -> { + if (rs.next()) { + return rs.getInt(1); + } + return -1; + }); + if (val == -1) { + throw new SQLException("Simulated error in addTenantIdInTargetStorage"); + } + } + + long currentTime = System.currentTimeMillis(); + { + // ON CONFLICT DO NOTHING + String QUERY = "INSERT INTO " + getConfig(start).getAppsTable() + + "(app_id, created_at_time) " + + "SELECT ?, ? WHERE NOT EXISTS (" + + " SELECT app_id FROM " + getConfig(start).getAppsTable() + + " WHERE app_id = ?" + + ")"; + update(con, QUERY, pst -> { + pst.setString(1, tenantIdentifier.getAppId()); + pst.setLong(2, currentTime); + pst.setString(3, tenantIdentifier.getAppId()); + }); + } + + { + // ON CONFLICT DO NOTHING + String QUERY = "INSERT INTO " + getConfig(start).getTenantsTable() + + "(app_id, tenant_id, created_at_time)" + + "SELECT ?, ?, ? WHERE NOT EXISTS (" + + " SELECT app_id, tenant_id FROM " + getConfig(start).getTenantsTable() + + " WHERE app_id = ? AND tenant_id = ?" + + ")"; + + update(con, QUERY, pst -> { + pst.setString(1, tenantIdentifier.getAppId()); + pst.setString(2, tenantIdentifier.getTenantId()); + pst.setLong(3, currentTime); + pst.setString(4, tenantIdentifier.getAppId()); + pst.setString(5, tenantIdentifier.getTenantId()); + }); + } + } + public static void deleteTenantIdInTargetStorage(Start start, TenantIdentifier tenantIdentifier) throws StorageQueryException { try { diff --git a/src/test/java/io/supertokens/storage/mysql/test/Utils.java b/src/test/java/io/supertokens/storage/mysql/test/Utils.java index 72361bf..3f1d42b 100644 --- a/src/test/java/io/supertokens/storage/mysql/test/Utils.java +++ b/src/test/java/io/supertokens/storage/mysql/test/Utils.java @@ -20,6 +20,7 @@ import io.supertokens.Main; import io.supertokens.pluginInterface.PluginInterfaceTesting; import io.supertokens.storage.mysql.Start; +import io.supertokens.storage.mysql.queries.MultitenancyQueries; import io.supertokens.storageLayer.StorageLayer; import org.apache.tomcat.util.http.fileupload.FileUtils; import org.junit.rules.TestRule; @@ -76,6 +77,7 @@ public static void reset() { Start.isTesting = true; Start.setEnableForDeadlockTesting(false); Main.makeConsolePrintSilent = true; + MultitenancyQueries.simulateErrorInAddingTenantIdInTargetStorage_forTesting = false; String installDir = "../"; try { // if the default config is not the same as the current config, we must reset the storage layer diff --git a/src/test/java/io/supertokens/storage/mysql/test/multitenancy/TestForNoCrashDuringStartup.java b/src/test/java/io/supertokens/storage/mysql/test/multitenancy/TestForNoCrashDuringStartup.java new file mode 100644 index 0000000..b2ee097 --- /dev/null +++ b/src/test/java/io/supertokens/storage/mysql/test/multitenancy/TestForNoCrashDuringStartup.java @@ -0,0 +1,563 @@ +/* + * Copyright (c) 2024, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package io.supertokens.storage.mysql.test.multitenancy; + +import com.google.gson.JsonObject; +import io.supertokens.Main; +import io.supertokens.ProcessState; +import io.supertokens.featureflag.EE_FEATURES; +import io.supertokens.featureflag.FeatureFlagTestContent; +import io.supertokens.featureflag.exceptions.FeatureNotEnabledException; +import io.supertokens.multitenancy.Multitenancy; +import io.supertokens.multitenancy.MultitenancyHelper; +import io.supertokens.multitenancy.exception.BadPermissionException; +import io.supertokens.multitenancy.exception.CannotModifyBaseConfigException; +import io.supertokens.pluginInterface.exceptions.InvalidConfigException; +import io.supertokens.pluginInterface.exceptions.StorageQueryException; +import io.supertokens.pluginInterface.multitenancy.*; +import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException; +import io.supertokens.session.Session; +import io.supertokens.session.accessToken.AccessToken; +import io.supertokens.storage.mysql.Start; +import io.supertokens.storage.mysql.queries.MultitenancyQueries; +import io.supertokens.storage.mysql.test.TestingProcessManager; +import io.supertokens.storage.mysql.test.Utils; +import io.supertokens.storage.mysql.test.httpRequest.HttpRequestForTesting; +import io.supertokens.storage.mysql.test.httpRequest.HttpResponseException; +import io.supertokens.storageLayer.StorageLayer; +import io.supertokens.thirdparty.InvalidProviderConfigException; +import io.supertokens.utils.SemVer; +import org.junit.After; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.Test; + +import java.io.IOException; + +import static org.junit.Assert.*; +import static io.supertokens.storage.mysql.QueryExecutorTemplate.update; + +public class TestForNoCrashDuringStartup { + TestingProcessManager.TestingProcess process; + + @AfterClass + public static void afterTesting() { + Utils.afterTesting(); + } + + @After + public void afterEach() throws InterruptedException { + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Before + public void beforeEach() throws InterruptedException, InvalidProviderConfigException, + StorageQueryException, FeatureNotEnabledException, TenantOrAppNotFoundException, IOException, + InvalidConfigException, CannotModifyBaseConfigException, BadPermissionException { + Utils.reset(); + + String[] args = {"../"}; + + this.process = TestingProcessManager.start(args); + FeatureFlagTestContent.getInstance(process.getProcess()) + .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{EE_FEATURES.MULTI_TENANCY}); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + } + + @Test + public void testThatCUDRecoversWhenItFailsToAddEntryDuringCreation() throws Exception { + JsonObject coreConfig = new JsonObject(); + StorageLayer.getStorage(new TenantIdentifier(null, null, null), process.getProcess()) + .modifyConfigToAddANewUserPoolForTesting(coreConfig, 1); + + TenantIdentifier tenantIdentifier = new TenantIdentifier("127.0.0.1", null, null); + + MultitenancyQueries.simulateErrorInAddingTenantIdInTargetStorage_forTesting = true; + try { + Multitenancy.addNewOrUpdateAppOrTenant(process.getProcess(), new TenantConfig( + tenantIdentifier, + new EmailPasswordConfig(true), + new ThirdPartyConfig(true, null), + new PasswordlessConfig(true), + null, null, + coreConfig + ), false); + fail(); + } catch (StorageQueryException e) { + // ignore + assertTrue(e.getMessage().contains("Simulated error")); + } + + TenantConfig[] allTenants = MultitenancyHelper.getInstance(process.getProcess()).getAllTenants(); + assertEquals(2, allTenants.length); // should have the new CUD + + try { + tpSignInUpAndGetResponse(new TenantIdentifier("127.0.0.1", null, null), "google", "googleid1", "test@example.com", process.getProcess(), SemVer.v5_0); + fail(); + } catch (HttpResponseException e) { + // ignore + assertTrue(e.getMessage().contains("Internal Error")); // retried creating tenant entry + } + + MultitenancyQueries.simulateErrorInAddingTenantIdInTargetStorage_forTesting = false; + + // this should succeed now + tpSignInUpAndGetResponse(new TenantIdentifier("127.0.0.1", null, null), "google", "googleid1", "test@example.com", process.getProcess(), SemVer.v5_0); + } + + @Test + public void testThatCUDRecoversWhenTheDbIsDownDuringCreationButDbComesUpLater() throws Exception { + Start start = ((Start) StorageLayer.getBaseStorage(process.getProcess())); + try { + update(start, "DROP DATABASE st5000;", pst -> {}); + } catch (Exception e) { + // ignore + } + + JsonObject coreConfig = new JsonObject(); + StorageLayer.getStorage(new TenantIdentifier(null, null, null), process.getProcess()) + .modifyConfigToAddANewUserPoolForTesting(coreConfig, 5000); + + TenantIdentifier tenantIdentifier = new TenantIdentifier("127.0.0.1", null, null); + + try { + Multitenancy.addNewOrUpdateAppOrTenant(process.getProcess(), new TenantConfig( + tenantIdentifier, + new EmailPasswordConfig(true), + new ThirdPartyConfig(true, null), + new PasswordlessConfig(true), + null, null, + coreConfig + ), false); + fail(); + } catch (StorageQueryException e) { + // ignore + assertTrue(e.getMessage().contains("Unknown database 'st5000'")); + } + + TenantConfig[] allTenants = MultitenancyHelper.getInstance(process.getProcess()).getAllTenants(); + assertEquals(2, allTenants.length); // should have the new CUD + + try { + tpSignInUpAndGetResponse(new TenantIdentifier("127.0.0.1", null, null), "google", "googleid1", "test@example.com", process.getProcess(), SemVer.v5_0); + fail(); + } catch (HttpResponseException e) { + // ignore + assertTrue(e.getMessage().contains("Internal Error")); // db is still down + } + + update(start, "CREATE DATABASE st5000;", pst -> {}); + + // this should succeed now + tpSignInUpAndGetResponse(new TenantIdentifier("127.0.0.1", null, null), "google", "googleid1", "test@example.com", process.getProcess(), SemVer.v5_0); + } + + @Test + public void testThatAppRecoversAfterAppCreationFailedToAddEntry() throws Exception { + JsonObject coreConfig = new JsonObject(); + + TenantIdentifier tenantIdentifier = new TenantIdentifier(null, "a1", null); + + MultitenancyQueries.simulateErrorInAddingTenantIdInTargetStorage_forTesting = true; + try { + Multitenancy.addNewOrUpdateAppOrTenant(process.getProcess(), new TenantConfig( + tenantIdentifier, + new EmailPasswordConfig(true), + new ThirdPartyConfig(true, null), + new PasswordlessConfig(true), + null, null, + coreConfig + ), false); + fail(); + } catch (StorageQueryException e) { + // ignore + assertTrue(e.getMessage().contains("Simulated error")); + } + + TenantConfig[] allTenants = MultitenancyHelper.getInstance(process.getProcess()).getAllTenants(); + assertEquals(2, allTenants.length); // should have the new CUD + + try { + tpSignInUpAndGetResponse(new TenantIdentifier(null, "a1", null), "google", "googleid1", "test@example.com", + process.getProcess(), SemVer.v5_0); + fail(); + } catch (HttpResponseException e) { + // ignore + assertTrue(e.getMessage().contains("AppId or tenantId not found")); // retried creating tenant entry + } + + MultitenancyQueries.simulateErrorInAddingTenantIdInTargetStorage_forTesting = false; + + Multitenancy.addNewOrUpdateAppOrTenant(process.getProcess(), new TenantConfig( + tenantIdentifier, + new EmailPasswordConfig(true), + new ThirdPartyConfig(true, null), + new PasswordlessConfig(true), + null, null, + coreConfig + ), false); + + // this should succeed now + tpSignInUpAndGetResponse(new TenantIdentifier(null, "a1", null), "google", "googleid1", "test@example.com", + process.getProcess(), SemVer.v5_0); + } + + @Test + public void testThatCoreDoesNotCrashDuringStartupWhenCUDCreationFailedToAddEntryInTargetStorage() throws Exception { + JsonObject coreConfig = new JsonObject(); + StorageLayer.getStorage(new TenantIdentifier(null, null, null), process.getProcess()) + .modifyConfigToAddANewUserPoolForTesting(coreConfig, 1); + + TenantIdentifier tenantIdentifier = new TenantIdentifier("127.0.0.1", null, null); + + MultitenancyQueries.simulateErrorInAddingTenantIdInTargetStorage_forTesting = true; + try { + Multitenancy.addNewOrUpdateAppOrTenant(process.getProcess(), new TenantConfig( + tenantIdentifier, + new EmailPasswordConfig(true), + new ThirdPartyConfig(true, null), + new PasswordlessConfig(true), + null, null, + coreConfig + ), false); + fail(); + } catch (StorageQueryException e) { + // ignore + assertTrue(e.getMessage().contains("Simulated error")); + } + + TenantConfig[] allTenants = MultitenancyHelper.getInstance(process.getProcess()).getAllTenants(); + assertEquals(2, allTenants.length); // should have the new CUD + + process.kill(false); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + + String[] args = {"../"}; + + this.process = TestingProcessManager.start(args); + FeatureFlagTestContent.getInstance(process.getProcess()) + .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{EE_FEATURES.MULTI_TENANCY}); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + allTenants = MultitenancyHelper.getInstance(process.getProcess()).getAllTenants(); + assertEquals(2, allTenants.length); // should have the new CUD + + MultitenancyQueries.simulateErrorInAddingTenantIdInTargetStorage_forTesting = false; + + // this should succeed now + tpSignInUpAndGetResponse(new TenantIdentifier("127.0.0.1", null, null), "google", "googleid1", "test@example.com", process.getProcess(), SemVer.v5_0); + } + + @Test + public void testThatCoreDoesNotCrashDuringStartupWhenTenantEntryIsInconsistentInTheBaseTenant() throws Exception { + JsonObject coreConfig = new JsonObject(); + StorageLayer.getStorage(new TenantIdentifier(null, null, null), process.getProcess()) + .modifyConfigToAddANewUserPoolForTesting(coreConfig, 1); + + TenantIdentifier tenantIdentifier = new TenantIdentifier("127.0.0.1", null, null); + + MultitenancyQueries.simulateErrorInAddingTenantIdInTargetStorage_forTesting = true; + try { + Multitenancy.addNewOrUpdateAppOrTenant(process.getProcess(), new TenantConfig( + tenantIdentifier, + new EmailPasswordConfig(true), + new ThirdPartyConfig(true, null), + new PasswordlessConfig(true), + null, null, + coreConfig + ), false); + fail(); + } catch (StorageQueryException e) { + // ignore + assertTrue(e.getMessage().contains("Simulated error")); + } + + TenantConfig[] allTenants = MultitenancyHelper.getInstance(process.getProcess()).getAllTenants(); + assertEquals(2, allTenants.length); // should have the new CUD + + Start start = (Start) StorageLayer.getBaseStorage(process.getProcess()); + update(start, "DELETE FROM apps;", pst -> {}); + + process.kill(false); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + + String[] args = {"../"}; + + this.process = TestingProcessManager.start(args); + FeatureFlagTestContent.getInstance(process.getProcess()) + .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{EE_FEATURES.MULTI_TENANCY}); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + MultitenancyQueries.simulateErrorInAddingTenantIdInTargetStorage_forTesting = false; + + // this should succeed now + tpSignInUpAndGetResponse(new TenantIdentifier("127.0.0.1", null, null), "google", "googleid1", "test@example.com", process.getProcess(), SemVer.v5_0); + + Session.createNewSession(process.getProcess(), "userid", new JsonObject(), new JsonObject()); + } + + @Test + public void testThatCoreDoesNotCrashDuringStartupWhenAppCreationFailedToAddEntryInTheBaseTenantStorage() throws Exception { + JsonObject coreConfig = new JsonObject(); + + TenantIdentifier tenantIdentifier = new TenantIdentifier(null, "a1", null); + + MultitenancyQueries.simulateErrorInAddingTenantIdInTargetStorage_forTesting = true; + try { + Multitenancy.addNewOrUpdateAppOrTenant(process.getProcess(), new TenantConfig( + tenantIdentifier, + new EmailPasswordConfig(true), + new ThirdPartyConfig(true, null), + new PasswordlessConfig(true), + null, null, + coreConfig + ), false); + fail(); + } catch (StorageQueryException e) { + // ignore + assertTrue(e.getMessage().contains("Simulated error")); + } + + TenantConfig[] allTenants = MultitenancyHelper.getInstance(process.getProcess()).getAllTenants(); + assertEquals(2, allTenants.length); // should have the new CUD + + Start start = (Start) StorageLayer.getBaseStorage(process.getProcess()); + update(start, "DELETE FROM apps WHERE app_id = ?;", pst -> { + pst.setString(1, "a1"); + }); + + process.kill(false); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + + MultitenancyQueries.simulateErrorInAddingTenantIdInTargetStorage_forTesting = false; + + String[] args = {"../"}; + + this.process = TestingProcessManager.start(args); + FeatureFlagTestContent.getInstance(process.getProcess()) + .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{EE_FEATURES.MULTI_TENANCY}); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + // this should succeed now + tpSignInUpAndGetResponse(new TenantIdentifier("127.0.0.1", null, null), "google", "googleid1", "test@example.com", process.getProcess(), SemVer.v5_0); + + Session.createNewSession( + new TenantIdentifier(null, "a1", null), + StorageLayer.getBaseStorage(process.getProcess()), + process.getProcess(), "userid", new JsonObject(), new JsonObject(), true, + AccessToken.getLatestVersion(), false); + } + + @Test + public void testThatCoreDoesNotCrashDuringStartupWhenCUDCreationFailedToAddTenantEntryInTargetStorageWithLoadOnlyCUDConfig() throws Exception { + JsonObject coreConfig = new JsonObject(); + + TenantIdentifier tenantIdentifier = new TenantIdentifier("127.0.0.1", null, null); + + MultitenancyQueries.simulateErrorInAddingTenantIdInTargetStorage_forTesting = true; + try { + StorageLayer.getStorage(new TenantIdentifier(null, null, null), process.getProcess()) + .modifyConfigToAddANewUserPoolForTesting(coreConfig, 1); + Multitenancy.addNewOrUpdateAppOrTenant(process.getProcess(), new TenantConfig( + tenantIdentifier, + new EmailPasswordConfig(true), + new ThirdPartyConfig(true, null), + new PasswordlessConfig(true), + null, null, + coreConfig + ), false); + fail(); + } catch (StorageQueryException e) { + // ignore + assertTrue(e.getMessage().contains("Simulated error")); + } + + try { + StorageLayer.getStorage(new TenantIdentifier(null, null, null), process.getProcess()) + .modifyConfigToAddANewUserPoolForTesting(coreConfig, 2); + Multitenancy.addNewOrUpdateAppOrTenant(process.getProcess(), new TenantConfig( + new TenantIdentifier("localhost", null, null), + new EmailPasswordConfig(true), + new ThirdPartyConfig(true, null), + new PasswordlessConfig(true), + null, null, + coreConfig + ), false); + fail(); + } catch (StorageQueryException e) { + // ignore + assertTrue(e.getMessage().contains("Simulated error")); + } + + try { + StorageLayer.getStorage(new TenantIdentifier(null, null, null), process.getProcess()) + .modifyConfigToAddANewUserPoolForTesting(coreConfig, 3); + Multitenancy.addNewOrUpdateAppOrTenant(process.getProcess(), new TenantConfig( + new TenantIdentifier("cud2", null, null), + new EmailPasswordConfig(true), + new ThirdPartyConfig(true, null), + new PasswordlessConfig(true), + null, null, + coreConfig + ), false); + fail(); + } catch (StorageQueryException e) { + // ignore + assertTrue(e.getMessage().contains("Simulated error")); + } + + TenantConfig[] allTenants = MultitenancyHelper.getInstance(process.getProcess()).getAllTenants(); + assertEquals(4, allTenants.length); // should have the new CUD + + process.kill(false); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + + MultitenancyQueries.simulateErrorInAddingTenantIdInTargetStorage_forTesting = false; + + String[] args = {"../"}; + + this.process = TestingProcessManager.start(args, false); + Utils.setValueInConfig("supertokens_saas_load_only_cud", "127.0.0.1:3567"); + FeatureFlagTestContent.getInstance(process.getProcess()) + .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{EE_FEATURES.MULTI_TENANCY}); + this.process.startProcess(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + allTenants = MultitenancyHelper.getInstance(process.getProcess()).getAllTenants(); + assertEquals(2, allTenants.length); // should have the new CUD + + // this should succeed now + tpSignInUpAndGetResponse(new TenantIdentifier("127.0.0.1", null, null), "google", "googleid1", "test@example.com", process.getProcess(), SemVer.v5_0); + + try { + tpSignInUpAndGetResponse(new TenantIdentifier("localhost", null, null), "google", "googleid1", "test@example.com", process.getProcess(), SemVer.v5_0); + fail(); + } catch (HttpResponseException e) { + // ignore + } + + process.kill(false); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + + this.process = TestingProcessManager.start(args, false); + Utils.setValueInConfig("supertokens_saas_load_only_cud", "localhost:3567"); + FeatureFlagTestContent.getInstance(process.getProcess()) + .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{EE_FEATURES.MULTI_TENANCY}); + this.process.startProcess(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + allTenants = MultitenancyHelper.getInstance(process.getProcess()).getAllTenants(); + assertEquals(2, allTenants.length); // should have the new CUD + + // this should succeed now + tpSignInUpAndGetResponse(new TenantIdentifier("localhost", null, null), "google", "googleid1", "test@example.com", process.getProcess(), SemVer.v5_0); + try { + tpSignInUpAndGetResponse(new TenantIdentifier("127.0.0.1", null, null), "google", "googleid1", "test@example.com", process.getProcess(), SemVer.v5_0); + fail(); + } catch (HttpResponseException e) { + // ignore + } + + process.kill(false); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + + this.process = TestingProcessManager.start(args, false); + Utils.setValueInConfig("supertokens_saas_load_only_cud", null); + FeatureFlagTestContent.getInstance(process.getProcess()) + .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{EE_FEATURES.MULTI_TENANCY}); + this.process.startProcess(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + } + + @Test + public void testThatTenantComesToLifeOnceTheTargetDbIsUpAfterCoreRestart() throws Exception { + Start start = ((Start) StorageLayer.getBaseStorage(process.getProcess())); + try { + update(start, "DROP DATABASE st5000;", pst -> {}); + } catch (Exception e) { + // ignore + } + + JsonObject coreConfig = new JsonObject(); + StorageLayer.getStorage(new TenantIdentifier(null, null, null), process.getProcess()) + .modifyConfigToAddANewUserPoolForTesting(coreConfig, 5000); + + TenantIdentifier tenantIdentifier = new TenantIdentifier("127.0.0.1", null, null); + + try { + Multitenancy.addNewOrUpdateAppOrTenant(process.getProcess(), new TenantConfig( + tenantIdentifier, + new EmailPasswordConfig(true), + new ThirdPartyConfig(true, null), + new PasswordlessConfig(true), + null, null, + coreConfig + ), false); + fail(); + } catch (StorageQueryException e) { + // ignore + assertTrue(e.getMessage().contains("Unknown database 'st5000'")); + } + + TenantConfig[] allTenants = MultitenancyHelper.getInstance(process.getProcess()).getAllTenants(); + assertEquals(2, allTenants.length); // should have the new CUD + + try { + tpSignInUpAndGetResponse(new TenantIdentifier("127.0.0.1", null, null), "google", "googleid1", "test@example.com", process.getProcess(), SemVer.v5_0); + fail(); + } catch (HttpResponseException e) { + // ignore + assertTrue(e.getMessage().contains("Internal Error")); // db is still down + } + + process.kill(false); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + + String[] args = {"../"}; + this.process = TestingProcessManager.start(args); + FeatureFlagTestContent.getInstance(process.getProcess()) + .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{EE_FEATURES.MULTI_TENANCY}); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + // the process should start successfully even though the db is down + + start = ((Start) StorageLayer.getBaseStorage(process.getProcess())); + update(start, "CREATE DATABASE st5000;", pst -> {}); + + // this should succeed now + tpSignInUpAndGetResponse(new TenantIdentifier("127.0.0.1", null, null), "google", "googleid1", "test@example.com", process.getProcess(), SemVer.v5_0); + } + + public static JsonObject tpSignInUpAndGetResponse(TenantIdentifier tenantIdentifier, String thirdPartyId, String thirdPartyUserId, String email, Main main, SemVer version) + throws HttpResponseException, IOException { + JsonObject emailObject = new JsonObject(); + emailObject.addProperty("id", email); + emailObject.addProperty("isVerified", false); + + JsonObject signUpRequestBody = new JsonObject(); + signUpRequestBody.addProperty("thirdPartyId", thirdPartyId); + signUpRequestBody.addProperty("thirdPartyUserId", thirdPartyUserId); + signUpRequestBody.add("email", emailObject); + + JsonObject response = HttpRequestForTesting.sendJsonPOSTRequest(main, "", + HttpRequestForTesting.getMultitenantUrl(tenantIdentifier, "/recipe/signinup"), signUpRequestBody, + 1000, 1000, null, + version.get(), "thirdparty"); + return response; + } +} \ No newline at end of file