This is an automated email from the ASF dual-hosted git repository.
roryqi pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/gravitino.git
The following commit(s) were added to refs/heads/main by this push:
new fdb03ed448 [#13964] fix(idp-basic): Make service-admin bootstrap
idempotent under races (#13965)
fdb03ed448 is described below
commit fdb03ed44844b636f3d77a5652c968e98e36c20a
Author: MaSai <[email protected]>
AuthorDate: Tue Oct 20 15:04:35 2026 +0800
[#13964] fix(idp-basic): Make service-admin bootstrap idempotent under
races (#13965)
### What changes were proposed in this pull request?
Make `IdpUserGroupManager.initializeConfiguredServiceAdmins` idempotent
under multi-node races: keep the existence pre-check, and treat
`AlreadyExistsException` on insert as success when another node already
created the same configured service admin.
- Only the bootstrap insert path is changed; `addUser` / normal create
APIs still surface already-exists errors.
- Add a concurrent bootstrap unit test.
### Why are the changes needed?
When multiple Gravitino nodes start together, each may observe a missing
service admin and attempt insert. One wins; the other hits the unique
constraint and previously failed Jetty/`gravitino-web` startup with
`AlreadyExistsException`.
Fix: #13964
### Does this PR introduce _any_ user-facing change?
No. Bootstrap becomes more tolerant of concurrent startups; user-facing
IdP create APIs are unchanged.
### How was this patch tested?
- `./gradlew :plugins:idp-basic:test --tests
org.apache.gravitino.idp.TestIdpUserGroupManager -PskipITs`
Co-authored-by: Cursor <[email protected]>
---
.../apache/gravitino/idp/IdpUserGroupManager.java | 15 ++++++++-
.../gravitino/idp/TestIdpUserGroupManager.java | 37 ++++++++++++++++++++++
2 files changed, 51 insertions(+), 1 deletion(-)
diff --git
a/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/IdpUserGroupManager.java
b/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/IdpUserGroupManager.java
index 0e5fb53fbb..0090f79fc3 100644
---
a/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/IdpUserGroupManager.java
+++
b/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/IdpUserGroupManager.java
@@ -29,6 +29,7 @@ import org.apache.commons.lang3.StringUtils;
import org.apache.gravitino.Config;
import org.apache.gravitino.Configs;
import org.apache.gravitino.auth.AuthConstants;
+import org.apache.gravitino.exceptions.AlreadyExistsException;
import org.apache.gravitino.exceptions.NotFoundException;
import org.apache.gravitino.exceptions.UnauthorizedException;
import org.apache.gravitino.idp.basic.IdpCredentialValidator;
@@ -45,6 +46,8 @@ import
org.apache.gravitino.idp.storage.service.IdpGroupMetaService;
import org.apache.gravitino.idp.storage.service.IdpUserMetaService;
import org.apache.gravitino.storage.IdGenerator;
import org.apache.gravitino.storage.relational.utils.POConverters;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
/**
* Manager for built-in IdP users and groups. It mirrors {@link
@@ -52,6 +55,8 @@ import
org.apache.gravitino.storage.relational.utils.POConverters;
*/
public class IdpUserGroupManager implements Closeable {
+ private static final Logger LOG =
LoggerFactory.getLogger(IdpUserGroupManager.class);
+
private static final IdpUserMetaService USER_SERVICE =
IdpUserMetaService.getInstance();
private static final IdpGroupMetaService GROUP_SERVICE =
IdpGroupMetaService.getInstance();
@@ -116,7 +121,15 @@ public class IdpUserGroupManager implements Closeable {
IdpCredentialValidator.validatePassword(initialAdminPassword);
String passwordHash = passwordHasher.hash(initialAdminPassword);
for (String serviceAdmin : missingServiceAdmins) {
- USER_SERVICE.insertIdpUser(newUserPO(serviceAdmin, passwordHash, true));
+ try {
+ USER_SERVICE.insertIdpUser(newUserPO(serviceAdmin, passwordHash,
true));
+ } catch (AlreadyExistsException e) {
+ // Another node may have created the same service admin between the
existence check and
+ // insert. Treat the unique-constraint conflict as success so
bootstrap stays idempotent.
+ LOG.info(
+ "Configured service admin {} already exists; skipping create
during bootstrap",
+ serviceAdmin);
+ }
}
}
diff --git
a/plugins/idp-basic/src/test/java/org/apache/gravitino/idp/TestIdpUserGroupManager.java
b/plugins/idp-basic/src/test/java/org/apache/gravitino/idp/TestIdpUserGroupManager.java
index 1afa1b5e14..81457fb454 100644
---
a/plugins/idp-basic/src/test/java/org/apache/gravitino/idp/TestIdpUserGroupManager.java
+++
b/plugins/idp-basic/src/test/java/org/apache/gravitino/idp/TestIdpUserGroupManager.java
@@ -35,7 +35,14 @@ import com.google.common.collect.Lists;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
+import java.util.ArrayList;
import java.util.Comparator;
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
import java.util.stream.Stream;
import org.apache.gravitino.Config;
import org.apache.gravitino.auth.AuthConstants;
@@ -386,6 +393,36 @@ public class TestIdpUserGroupManager {
"initAdminSame2", manager.authenticate("initAdminSame2",
VALID_PASSWORD).name());
}
+ @Test
+ public void
testInitializeConfiguredServiceAdminsIgnoresConcurrentCreateRace() throws
Exception {
+ loadServiceAdminConfig(BASIC_AUTHENTICATOR, "initAdminRace1");
+
+ int threads = 8;
+ ExecutorService executor = Executors.newFixedThreadPool(threads);
+ CountDownLatch start = new CountDownLatch(1);
+ List<Future<?>> futures = new ArrayList<>();
+ try {
+ for (int i = 0; i < threads; i++) {
+ futures.add(
+ executor.submit(
+ () -> {
+ start.await();
+ manager.initializeConfiguredServiceAdmins(config,
VALID_PASSWORD);
+ return null;
+ }));
+ }
+ start.countDown();
+ for (Future<?> future : futures) {
+ future.get(30, TimeUnit.SECONDS);
+ }
+ } finally {
+ executor.shutdownNow();
+ }
+
+ Assertions.assertEquals(
+ "initAdminRace1", manager.authenticate("initAdminRace1",
VALID_PASSWORD).name());
+ }
+
private static Config createH2Config(Path h2Path) {
Config backendConfig = new Config(false) {};
backendConfig.set(ENTITY_STORE, RELATIONAL_ENTITY_STORE);