From e5dcad6d2684b497059a95011a16a671fce199e4 Mon Sep 17 00:00:00 2001 From: Sattvik Chakravarthy Date: Thu, 27 Nov 2025 12:59:16 +0530 Subject: [PATCH] fix: change to read committed --- .../storage/postgresql/LockFailure.java | 27 ++++++++++ .../supertokens/storage/postgresql/Start.java | 3 +- .../postgresql/queries/GeneralQueries.java | 6 ++- .../queries/MultitenancyQueries.java | 4 ++ .../queries/UserMetadataQueries.java | 23 ++++---- .../storage/postgresql/queries/Utils.java | 53 +++++++++++++++++++ 6 files changed, 104 insertions(+), 12 deletions(-) create mode 100644 src/main/java/io/supertokens/storage/postgresql/LockFailure.java create mode 100644 src/main/java/io/supertokens/storage/postgresql/queries/Utils.java diff --git a/src/main/java/io/supertokens/storage/postgresql/LockFailure.java b/src/main/java/io/supertokens/storage/postgresql/LockFailure.java new file mode 100644 index 00000000..70cfcd5f --- /dev/null +++ b/src/main/java/io/supertokens/storage/postgresql/LockFailure.java @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2020, 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.postgresql; + +public class LockFailure extends Exception { + public LockFailure() { + super("Failed to acquire advisory lock"); + } + + public LockFailure(String message) { + super(message); + } +} diff --git a/src/main/java/io/supertokens/storage/postgresql/Start.java b/src/main/java/io/supertokens/storage/postgresql/Start.java index abf6cdce..9aa2519b 100644 --- a/src/main/java/io/supertokens/storage/postgresql/Start.java +++ b/src/main/java/io/supertokens/storage/postgresql/Start.java @@ -342,7 +342,7 @@ public void initStorage(boolean shouldWait, List tenantIdentif @Override public T startTransaction(TransactionLogic logic) throws StorageTransactionLogicException, StorageQueryException { - return startTransaction(logic, TransactionIsolationLevel.SERIALIZABLE); + return startTransaction(logic, TransactionIsolationLevel.READ_COMMITTED); } @Override @@ -384,6 +384,7 @@ public T startTransaction(TransactionLogic logic, TransactionIsolationLev // We could get here if the new logic hits a false negative, // e.g., in case someone renamed constraints/tables boolean isDeadlockException = actualException instanceof SQLTransactionRollbackException + || actualException instanceof LockFailure || exceptionMessage.toLowerCase().contains("concurrent update") || exceptionMessage.toLowerCase().contains("concurrent delete") || exceptionMessage.toLowerCase().contains("the transaction might succeed if retried") || diff --git a/src/main/java/io/supertokens/storage/postgresql/queries/GeneralQueries.java b/src/main/java/io/supertokens/storage/postgresql/queries/GeneralQueries.java index 679d0936..695f174a 100644 --- a/src/main/java/io/supertokens/storage/postgresql/queries/GeneralQueries.java +++ b/src/main/java/io/supertokens/storage/postgresql/queries/GeneralQueries.java @@ -875,8 +875,12 @@ public static KeyValueInfo getKeyValue(Start start, TenantIdentifier tenantIdent public static KeyValueInfo getKeyValue_Transaction(Start start, Connection con, TenantIdentifier tenantIdentifier, String key) throws SQLException, StorageQueryException { + + io.supertokens.storage.postgresql.queries.Utils.takeAdvisoryLock( + con, tenantIdentifier.getAppId() + "~" + tenantIdentifier.getTenantId() + "~" + key); + String QUERY = "SELECT value, created_at_time FROM " + getConfig(start).getKeyValueTable() - + " WHERE app_id = ? AND tenant_id = ? AND name = ? FOR UPDATE"; + + " WHERE app_id = ? AND tenant_id = ? AND name = ?"; return execute(con, QUERY, pst -> { pst.setString(1, tenantIdentifier.getAppId()); diff --git a/src/main/java/io/supertokens/storage/postgresql/queries/MultitenancyQueries.java b/src/main/java/io/supertokens/storage/postgresql/queries/MultitenancyQueries.java index b0c7ffe1..f18eadd0 100644 --- a/src/main/java/io/supertokens/storage/postgresql/queries/MultitenancyQueries.java +++ b/src/main/java/io/supertokens/storage/postgresql/queries/MultitenancyQueries.java @@ -243,6 +243,10 @@ public static void overwriteTenantConfig(Start start, TenantConfig tenantConfig) Connection sqlCon = (Connection) con.getConnection(); { try { + { + io.supertokens.storage.postgresql.queries.Utils.takeAdvisoryLock( + sqlCon, tenantConfig.tenantIdentifier.getConnectionUriDomain() + "~" + tenantConfig.tenantIdentifier.getAppId() + "~" + tenantConfig.tenantIdentifier.getTenantId()); + } { String QUERY = "DELETE FROM " + getConfig(start).getTenantConfigsTable() + " WHERE connection_uri_domain = ? AND app_id = ? AND tenant_id = ?;"; diff --git a/src/main/java/io/supertokens/storage/postgresql/queries/UserMetadataQueries.java b/src/main/java/io/supertokens/storage/postgresql/queries/UserMetadataQueries.java index b620d884..3900991e 100644 --- a/src/main/java/io/supertokens/storage/postgresql/queries/UserMetadataQueries.java +++ b/src/main/java/io/supertokens/storage/postgresql/queries/UserMetadataQueries.java @@ -16,25 +16,27 @@ package io.supertokens.storage.postgresql.queries; +import java.sql.Connection; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + import com.google.gson.JsonObject; import com.google.gson.JsonParser; + import io.supertokens.pluginInterface.exceptions.StorageQueryException; import io.supertokens.pluginInterface.exceptions.StorageTransactionLogicException; import io.supertokens.pluginInterface.multitenancy.AppIdentifier; import io.supertokens.storage.postgresql.PreparedStatementValueSetter; +import static io.supertokens.storage.postgresql.QueryExecutorTemplate.execute; +import static io.supertokens.storage.postgresql.QueryExecutorTemplate.executeBatch; +import static io.supertokens.storage.postgresql.QueryExecutorTemplate.update; import io.supertokens.storage.postgresql.Start; import io.supertokens.storage.postgresql.config.Config; -import io.supertokens.storage.postgresql.utils.Utils; - -import java.sql.Connection; -import java.sql.SQLException; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -import static io.supertokens.storage.postgresql.QueryExecutorTemplate.*; import static io.supertokens.storage.postgresql.config.Config.getConfig; +import io.supertokens.storage.postgresql.utils.Utils; public class UserMetadataQueries { @@ -121,6 +123,7 @@ public static void setMultipleUsersMetadatas_Transaction(Start start, Connection public static JsonObject getUserMetadata_Transaction(Start start, Connection con, AppIdentifier appIdentifier, String userId) throws SQLException, StorageQueryException { + io.supertokens.storage.postgresql.queries.Utils.takeAdvisoryLock(con, appIdentifier.getAppId() + "~" + userId); String QUERY = "SELECT user_metadata FROM " + getConfig(start).getUserMetadataTable() + " WHERE app_id = ? AND user_id = ? FOR UPDATE"; return execute(con, QUERY, pst -> { diff --git a/src/main/java/io/supertokens/storage/postgresql/queries/Utils.java b/src/main/java/io/supertokens/storage/postgresql/queries/Utils.java new file mode 100644 index 00000000..6c343a79 --- /dev/null +++ b/src/main/java/io/supertokens/storage/postgresql/queries/Utils.java @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2020, 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.postgresql.queries; + +import java.sql.Connection; +import java.sql.SQLException; + +import io.supertokens.pluginInterface.exceptions.StorageQueryException; +import io.supertokens.storage.postgresql.LockFailure; +import static io.supertokens.storage.postgresql.QueryExecutorTemplate.execute; + +public class Utils { + + /** + * Acquires a PostgreSQL advisory lock using two string keys. + * Uses pg_try_advisory_xact_lock which is transaction-scoped (automatically released on commit/rollback). + * + * @param con The database connection (must be within a transaction) + * @param key Key for the lock (e.g., appId) + * @throws SQLException If a database error occurs + * @throws StorageQueryException If a query error occurs + * @throws LockFailure If the lock could not be acquired + */ + public static void takeAdvisoryLock(Connection con, String key) + throws SQLException, StorageQueryException { + String LOCK_QUERY = "SELECT pg_try_advisory_xact_lock(hashtext(?))"; + boolean lockAcquired = execute(con, LOCK_QUERY, pst -> { + pst.setString(1, key); + }, result -> { + if (result.next()) { + return result.getBoolean(1); + } + return false; + }); + if (!lockAcquired) { + throw new StorageQueryException(new LockFailure()); + } + } +}