Skip to content

Commit

Permalink
feat: Multi tenant user pool (#57)
Browse files Browse the repository at this point in the history
* fix: get to compile

* fix: updated queries

* fix: self review and fixes

* fix: tests and fixes

* fix: config annotations

* fix: changelog

* fix: changes from postgres plugin

* fix: schema fix

* fix: schema fix

* fix: updated changelog

* fix: updated changelog

* fix: formatting

* fix: update version

* fix: added date
  • Loading branch information
sattvikc authored Jun 2, 2023
1 parent deaa20f commit a42e101
Show file tree
Hide file tree
Showing 42 changed files with 6,703 additions and 1,805 deletions.
664 changes: 664 additions & 0 deletions CHANGELOG.md

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ plugins {
id 'java-library'
}

version = "3.0.0"
version = "4.0.0"

repositories {
mavenCentral()
Expand Down
30 changes: 15 additions & 15 deletions config.yaml
Original file line number Diff line number Diff line change
@@ -1,78 +1,78 @@
mysql_config_version: 0


# (OPTIONAL | Default: 10) integer value. Defines the connection pool size to MySQL.
# (DIFFERENT_ACROSS_TENANTS | OPTIONAL | Default: 10) integer value. Defines the connection pool size to MySQL.
# Please see https://github.com/brettwooldridge/HikariCP/wiki/About-Pool-Sizing
# mysql_connection_pool_size:


# (OPTIONAL | Default: null) string value. Specify the MySQL connection URI in the following
# (DIFFERENT_ACROSS_TENANTS | OPTIONAL | Default: null) string value. Specify the MySQL connection URI in the following
# format: mysql://[user[:[password]]@]host[:port][/dbname][?attr1=val1&attr2=val2...
# Values provided via other configs will override values provided by this config.
# mysql_connection_uri:


# (OPTIONAL | Default: "localhost") string value. Specify the mysql host url here. For example:
# (DIFFERENT_ACROSS_TENANTS | OPTIONAL | Default: "localhost") string value. Specify the mysql host url here. For example:
# - "localhost"
# - "192.168.0.1"
# - "<IP to cloud instance>"
# - "example.com"
# mysql_host:


# (OPTIONAL | Default: 3306) integer value. Specify the port to use when connecting to MySQL instance.
# (DIFFERENT_ACROSS_TENANTS | OPTIONAL | Default: 3306) integer value. Specify the port to use when connecting to MySQL instance.
# mysql_port:


# (OPTIONAL | Default: root) string value. The MySQL user to use to query the database.
# (DIFFERENT_ACROSS_TENANTS | OPTIONAL | Default: root) string value. The MySQL user to use to query the database.
# If the relevant tables are not already created by you, this user should have the
# ability to create new tables. To see the tables needed, visit: https://supertokens.io/docs/community/getting-started/database-setup/mysql
# mysql_user:


# (OPTIONAL | Default: no password) string value. Password for the MySQL user. If you have not set a password
# (DIFFERENT_ACROSS_TENANTS | OPTIONAL | Default: no password) string value. Password for the MySQL user. If you have not set a password
# make this an empty string.
# mysql_password:


# (OPTIONAL | Default: "supertokens") string value. The database name to store SuperTokens related data.
# (DIFFERENT_ACROSS_TENANTS | OPTIONAL | Default: "supertokens") string value. The database name to store SuperTokens related data.
# mysql_database_name:

# (OPTIONAL | Default: "") string value. A prefix to add to all table names managed by SuperTokens. An "_" will be
# (DIFFERENT_ACROSS_TENANTS | OPTIONAL | Default: "") string value. A prefix to add to all table names managed by SuperTokens. An "_" will be
# added between this prefix and the actual table name if the prefix is defined
# mysql_table_names_prefix:


# (OPTIONAL | Default: "key_value") string value. Specify the name of the table that will store secret keys
# (DIFFERENT_ACROSS_TENANTS | OPTIONAL | Default: "key_value") string value. Specify the name of the table that will store secret keys
# and app info necessary for the functioning sessions.
# mysql_key_value_table_name:


# (OPTIONAL | Default: "session_info") string value. Specify the name of the table that will store the
# (DIFFERENT_ACROSS_TENANTS | OPTIONAL | Default: "session_info") string value. Specify the name of the table that will store the
# session info for users.
# mysql_session_info_table_name:


# (OPTIONAL | Default: "emailpassword_users") string value. Specify the name of the table that will store the
# (DIFFERENT_ACROSS_TENANTS | OPTIONAL | Default: "emailpassword_users") string value. Specify the name of the table that will store the
# user information, along with their email and hashed password.
# mysql_emailpassword_users_table_name:


# (OPTIONAL | Default: "emailpassword_pswd_reset_tokens") string value. Specify the name of the table that will
# (DIFFERENT_ACROSS_TENANTS | OPTIONAL | Default: "emailpassword_pswd_reset_tokens") string value. Specify the name of the table that will
# store the password reset tokens for users.
# mysql_emailpassword_pswd_reset_tokens_table_name:


# (OPTIONAL | Default: "emailverification_tokens") string value. Specify the name of the table that will
# (DIFFERENT_ACROSS_TENANTS | OPTIONAL | Default: "emailverification_tokens") string value. Specify the name of the table that will
# store the email verification tokens for users.
# mysql_emailverification_tokens_table_name:


# (OPTIONAL | Default: "emailverification_verified_emails") string value. Specify the name of the table that will
# (DIFFERENT_ACROSS_TENANTS | OPTIONAL | Default: "emailverification_verified_emails") string value. Specify the name of the table that will
# store the verified email addresses.
# mysql_emailverification_verified_emails_table_name:


# (OPTIONAL | Default: "thirdparty_users") string value. Specify the name of the table that will
# (DIFFERENT_ACROSS_TENANTS | OPTIONAL | Default: "thirdparty_users") string value. Specify the name of the table that will
# store the thirdparty recipe users.
# mysql_thirdparty_users_table_name
30 changes: 15 additions & 15 deletions devConfig.yaml
Original file line number Diff line number Diff line change
@@ -1,74 +1,74 @@
mysql_config_version: 0


# (OPTIONAL | Default: 10) integer value. Defines the connection pool size to MySQL.
# (DIFFERENT_ACROSS_TENANTS | OPTIONAL | Default: 10) integer value. Defines the connection pool size to MySQL.
# Please see https://github.com/brettwooldridge/HikariCP/wiki/About-Pool-Sizing
# mysql_connection_pool_size:


# (OPTIONAL | Default: null) string value. Specify the MySQL connection URI in the following
# (DIFFERENT_ACROSS_TENANTS | OPTIONAL | Default: null) string value. Specify the MySQL connection URI in the following
# format: mysql://[user[:[password]]@]host[:port][/dbname][?attr1=val1&attr2=val2...
# Values provided via other configs will override values provided by this config.
# mysql_connection_uri:


# (OPTIONAL | Default: "localhost") string value. Specify the mysql host url here. For example:
# (DIFFERENT_ACROSS_TENANTS | OPTIONAL | Default: "localhost") string value. Specify the mysql host url here. For example:
# - "localhost"
# - "192.168.0.1"
# - "<IP to cloud instance>"
# - "example.com"
# mysql_host:


# (OPTIONAL | Default: 3306) integer value. Specify the port to use when connecting to MySQL instance.
# (DIFFERENT_ACROSS_TENANTS | OPTIONAL | Default: 3306) integer value. Specify the port to use when connecting to MySQL instance.
# mysql_port:


# (OPTIONAL | Default: root) string value. The MySQL user to use to query the database.
# (DIFFERENT_ACROSS_TENANTS | OPTIONAL | Default: root) string value. The MySQL user to use to query the database.
# If the relevant tables are not already created by you, this user should have the
# ability to create new tables. To see the tables needed, visit: TODO
mysql_user: "root"


# (OPTIONAL | Default: no password) string value. Password for the MySQL instance. If you do not have a password
# (DIFFERENT_ACROSS_TENANTS | OPTIONAL | Default: no password) string value. Password for the MySQL instance. If you do not have a password
# make this an empty string.
mysql_password: "root"


# (OPTIONAL | Default: "supertokens") string value. The database name to store SuperTokens related data.
# (DIFFERENT_ACROSS_TENANTS | OPTIONAL | Default: "supertokens") string value. The database name to store SuperTokens related data.
# mysql_database_name:


# (OPTIONAL | Default: "") string value. A prefix to add to all table names managed by SuperTokens. An "_" will be
# (DIFFERENT_ACROSS_TENANTS | OPTIONAL | Default: "") string value. A prefix to add to all table names managed by SuperTokens. An "_" will be
# added between this prefix and the actual table name if the prefix is defined
# mysql_table_names_prefix:


# (OPTIONAL | Default: "key_value") string value. Specify the name of the table that will store secret keys
# (DIFFERENT_ACROSS_TENANTS | OPTIONAL | Default: "key_value") string value. Specify the name of the table that will store secret keys
# and app info necessary for the functioning sessions.
# mysql_key_value_table_name:


# (OPTIONAL | Default: "session_info") string value. Specify the name of the table that will store the
# (DIFFERENT_ACROSS_TENANTS | OPTIONAL | Default: "session_info") string value. Specify the name of the table that will store the
# session info for users.
# mysql_session_info_table_name:

# (OPTIONAL | Default: "emailpassword_users") string value. Specify the name of the table that will store the
# (DIFFERENT_ACROSS_TENANTS | OPTIONAL | Default: "emailpassword_users") string value. Specify the name of the table that will store the
# user information, along with their email and hashed password.
# mysql_emailpassword_users_table_name:

# (OPTIONAL | Default: "emailpassword_pswd_reset_tokens") string value. Specify the name of the table that will
# (DIFFERENT_ACROSS_TENANTS | OPTIONAL | Default: "emailpassword_pswd_reset_tokens") string value. Specify the name of the table that will
# store the password reset tokens for users.
# mysql_emailpassword_pswd_reset_tokens_table_name:

# (OPTIONAL | Default: "emailverification_tokens") string value. Specify the name of the table that will
# (DIFFERENT_ACROSS_TENANTS | OPTIONAL | Default: "emailverification_tokens") string value. Specify the name of the table that will
# store the email verification tokens for users.
# mysql_emailverification_tokens_table_name:

# (OPTIONAL | Default: "emailverification_verified_emails") string value. Specify the name of the table that will
# (DIFFERENT_ACROSS_TENANTS | OPTIONAL | Default: "emailverification_verified_emails") string value. Specify the name of the table that will
# store the verified email addresses.
# mysql_emailverification_verified_emails_table_name:

# (OPTIONAL | Default: "thirdparty_users") string value. Specify the name of the table that will
# (DIFFERENT_ACROSS_TENANTS | OPTIONAL | Default: "thirdparty_users") string value. Specify the name of the table that will
# store the thirdparty recipe users.
# mysql_thirdparty_users_table_name
Binary file removed jar/mysql-plugin-3.0.0.jar
Binary file not shown.
Binary file added jar/mysql-plugin-4.0.0.jar
Binary file not shown.
2 changes: 1 addition & 1 deletion pluginInterfaceSupported.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"_comment": "contains a list of plugin interfaces branch names that this core supports",
"versions": [
"2.23"
"3.0"
]
}
62 changes: 47 additions & 15 deletions src/main/java/io/supertokens/storage/mysql/ConnectionPool.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@

import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;
import io.supertokens.pluginInterface.exceptions.QuitProgramFromPluginException;
import io.supertokens.pluginInterface.exceptions.DbInitException;
import io.supertokens.storage.mysql.config.Config;
import io.supertokens.storage.mysql.config.MySQLConfig;
import io.supertokens.storage.mysql.output.Logging;
Expand All @@ -33,9 +33,18 @@
public class ConnectionPool extends ResourceDistributor.SingletonResource {

private static final String RESOURCE_KEY = "io.supertokens.storage.mysql.ConnectionPool";
private static HikariDataSource hikariDataSource = null;
private HikariDataSource hikariDataSource = null;

private final Start start;

private ConnectionPool(Start start) {
this.start = start;
}

private synchronized void initialiseHikariDataSource() throws SQLException {
if (this.hikariDataSource != null) {
return;
}
if (!start.enabled) {
throw new RuntimeException("Connection refused"); // emulates exception thrown by Hikari
}
Expand Down Expand Up @@ -71,15 +80,20 @@ private ConnectionPool(Start start) {
config.setPassword(userConfig.getPassword());
}
config.setMaximumPoolSize(userConfig.getConnectionPoolSize());
config.setConnectionTimeout(5000);
config.addDataSourceProperty("cachePrepStmts", "true");
config.addDataSourceProperty("prepStmtCacheSize", "250");
config.addDataSourceProperty("prepStmtCacheSqlLimit", "2048");
// TODO: set maxLifetimeValue to lesser than 10 mins so that the following error doesnt happen:
// io.supertokens.storage.mysql.HikariLoggingAppender.doAppend(HikariLoggingAppender.java:117) | SuperTokens
// - Failed to validate connection org.mariadb.jdbc.MariaDbConnection@79af83ae (Connection.setNetworkTimeout
// cannot be called on a closed connection). Possibly consider using a shorter maxLifetime value.
config.setPoolName("SuperTokens");
hikariDataSource = new HikariDataSource(config);
config.setPoolName(start.getUserPoolId() + "~" + start.getConnectionPoolId());
try {
hikariDataSource = new HikariDataSource(config);
} catch (Exception e) {
throw new SQLException(e);
}
}

private static int getTimeToWaitToInit(Start start) {
Expand All @@ -106,12 +120,16 @@ private static ConnectionPool getInstance(Start start) {
return (ConnectionPool) start.getResourceDistributor().getResource(RESOURCE_KEY);
}

public static void initPool(Start start) {
if (getInstance(start) != null) {
static boolean isAlreadyInitialised(Start start) {
return getInstance(start) != null && getInstance(start).hikariDataSource != null;
}

static void initPool(Start start, boolean shouldWait) throws DbInitException, SQLException {
if (isAlreadyInitialised(start)) {
return;
}
if (Thread.currentThread() != start.mainThread) {
throw new QuitProgramFromPluginException("Should not come here");
throw new DbInitException("Should not come here");
}
Logging.info(start, "Setting up MySQL connection pool.", true);
boolean longMessagePrinted = false;
Expand All @@ -120,15 +138,20 @@ public static void initPool(Start start) {
+ "you have"
+ " specified the correct values for ('mysql_host' and 'mysql_port') or for 'mysql_connection_uri'";
try {
ConnectionPool con = new ConnectionPool(start);
start.getResourceDistributor().setResource(RESOURCE_KEY, con);
while (true) {
try {
start.getResourceDistributor().setResource(RESOURCE_KEY, new ConnectionPool(start));
con.initialiseHikariDataSource();
break;
} catch (Exception e) {
if (!shouldWait) {
throw new DbInitException(e);
}
if (e.getMessage().contains("Connection refused")) {
start.handleKillSignalForWhenItHappens();
if (System.currentTimeMillis() > maxTryTime) {
throw new QuitProgramFromPluginException(errorMessage);
throw new DbInitException(errorMessage);
}
if (!longMessagePrinted) {
longMessagePrinted = true;
Expand All @@ -145,7 +168,7 @@ public static void initPool(Start start) {
}
Thread.sleep(getRetryIntervalIfInitFails(start));
} catch (InterruptedException ex) {
throw new QuitProgramFromPluginException(errorMessage);
throw new DbInitException(errorMessage);
}
} else {
throw e;
Expand All @@ -159,19 +182,28 @@ public static void initPool(Start start) {

public static Connection getConnection(Start start) throws SQLException {
if (getInstance(start) == null) {
throw new QuitProgramFromPluginException("Please call initPool before getConnection");
throw new IllegalStateException("Please call initPool before getConnection");
}
if (!start.enabled) {
throw new SQLException("Storage layer disabled");
}
return ConnectionPool.hikariDataSource.getConnection();
if (getInstance(start).hikariDataSource == null) {
getInstance(start).initialiseHikariDataSource();
}
return getInstance(start).hikariDataSource.getConnection();
}

public static void close(Start start) {
static void close(Start start) {
if (getInstance(start) == null) {
return;
}
ConnectionPool.hikariDataSource.close();
ConnectionPool.hikariDataSource = null;
if (getInstance(start).hikariDataSource != null) {
try {
getInstance(start).hikariDataSource.close();
} finally {
// we mark it as null so that next time it's being initialised, it will be initialised again
getInstance(start).hikariDataSource = null;
}
}
}
}
Loading

0 comments on commit a42e101

Please sign in to comment.