Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 47 additions & 34 deletions core/src/main/java/hudson/model/User.java
Original file line number Diff line number Diff line change
Expand Up @@ -1124,40 +1124,53 @@ public static void scanAll() throws IOException {
}
var byName = instance.byName;
var idStrategy = idStrategy();
for (var dir : subdirectories) {
var dirName = dir.getName();
if (!HASHED_DIRNAMES.matcher(dirName).matches()) {
LOGGER.fine(() -> "ignoring unrecognized dir " + dir);
continue;
}
var xml = new XmlFile(XSTREAM, new File(dir, CONFIG_XML));
if (!xml.exists()) {
LOGGER.fine(() -> "ignoring dir " + dir + " with no " + CONFIG_XML);
continue;
}
var user = new User();
try {
xml.unmarshal(user);
} catch (Exception x) {
LOGGER.log(Level.WARNING, "failed to load " + xml, x);
continue;
}
if (user.id == null) {
LOGGER.warning(() -> "ignoring " + xml + " with no <id>");
continue;
}
var expectedFolderName = getUserFolderNameFor(user.id);
if (!dirName.equals(expectedFolderName)) {
LOGGER.warning(() -> "ignoring " + xml + " with <id> " + user.id + " expected to be in " + expectedFolderName);
continue;
}
user.fixUpAfterLoad();
var old = byName.put(idStrategy.keyFor(user.id), user);
if (old != null) {
LOGGER.warning(() -> "entry for " + user.id + " in " + dir + " duplicates one seen earlier for " + old.id);
} else {
LOGGER.fine(() -> "successfully loaded " + user.id + " from " + xml);
}
int concurrency = Integer.getInteger("hudson.model.User.scanConcurrency", Math.min(4, Runtime.getRuntime().availableProcessors()));
var pool = new java.util.concurrent.ForkJoinPool(concurrency);
try {
pool.submit(() -> {
Arrays.stream(subdirectories).parallel().forEach(dir -> {
var dirName = dir.getName();
if (!HASHED_DIRNAMES.matcher(dirName).matches()) {
LOGGER.fine(() -> "ignoring unrecognized dir " + dir);
return;
}
var xml = new XmlFile(XSTREAM, new File(dir, CONFIG_XML));
if (!xml.exists()) {
LOGGER.fine(() -> "ignoring dir " + dir + " with no " + CONFIG_XML);
return;
}
var user = new User();
try {
xml.unmarshal(user);
} catch (Exception x) {
LOGGER.log(Level.WARNING, "failed to load " + xml, x);
return;
}
if (user.id == null) {
LOGGER.warning(() -> "ignoring " + xml + " with no <id>");
return;
}
var expectedFolderName = getUserFolderNameFor(user.id);
if (!dirName.equals(expectedFolderName)) {
LOGGER.warning(() -> "ignoring " + xml + " with <id> " + user.id + " expected to be in " + expectedFolderName);
return;
}
user.fixUpAfterLoad();
var old = byName.put(idStrategy.keyFor(user.id), user);
if (old != null) {
LOGGER.warning(() -> "entry for " + user.id + " in " + dir + " duplicates one seen earlier for " + old.id);
} else {
LOGGER.fine(() -> "successfully loaded " + user.id + " from " + xml);
}
});
}).get();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
LOGGER.log(Level.WARNING, "User directory scanning was interrupted", e);
} catch (ExecutionException e) {
LOGGER.log(Level.WARNING, "User directory scanning failed", e);
} finally {
pool.shutdown();
}
LOGGER.fine(() -> "loaded " + byName.size() + " entries");
}
Expand Down
64 changes: 64 additions & 0 deletions test/src/test/java/hudson/model/UserTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -818,4 +818,68 @@ public UserDetails loadUserByUsername2(String username) throws UsernameNotFoundE
throw new UserMayOrMayNotExistException2(username + " not found");
}
}

@Test
void parallelScanAllWithMalformedConfig() throws Exception {
List<String> validIds = List.of("valid1", "valid2", "valid3", "valid4", "valid5", "valid6", "valid7", "valid8", "valid9", "valid10");
for (String id : validIds) {
User.getById(id, true).save();
}

File malformedDir1 = User.getUserFolderFor("malformed1");
File malformedDir2 = User.getUserFolderFor("malformed2");

assertTrue(malformedDir1.mkdirs());
assertTrue(malformedDir2.mkdirs());

// Write a malformed config.xml (syntactically invalid XML) for malformed1
java.nio.file.Files.writeString(
new File(malformedDir1, "config.xml").toPath(),
"<hudson.model.User>\n <id>malformed1</id>\n <invalid>...\n"
);

// Write a malformed config.xml (syntactically valid but unmarshal-failing XML) for malformed2
java.nio.file.Files.writeString(
new File(malformedDir2, "config.xml").toPath(),
"<hudson.model.Descriptor>\n</hudson.model.Descriptor>\n"
);

try {
// Clear the memory cache of users before scanning to make sure scanAll actually loads them
User.clear();

// Verify they are not in the registry
for (String id : validIds) {
assertNull(User.getById(id, false));
}
assertNull(User.getById("malformed1", false));
assertNull(User.getById("malformed2", false));

// Execute the parallel unmarshaling / scan
User.AllUsers.scanAll();

// Verify that all valid users are successfully unmarshaled and populated in the registry
for (String id : validIds) {
User u = User.getById(id, false);
assertNotNull(u, "User " + id + " should have been loaded");
assertEquals(id, u.getId());
}

// Verify that malformed users are not populated in the registry
assertNull(User.getById("malformed1", false), "Malformed user 1 should not be loaded");
assertNull(User.getById("malformed2", false), "Malformed user 2 should not be loaded");

} finally {
// Clean up files on disk
for (String id : validIds) {
hudson.Util.deleteRecursive(User.getUserFolderFor(id));
}
hudson.Util.deleteRecursive(malformedDir1);
hudson.Util.deleteRecursive(malformedDir2);

// Reset the memory cache again
User.clear();
}
}
}