/* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You under the Apache License, Version 2.0 * (the "License"); 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 org.apache.jackrabbit.core.security.user; import java.io.File; import java.io.PrintStream; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Random; import java.util.concurrent.atomic.AtomicLong; import javax.jcr.InvalidItemStateException; import javax.jcr.Node; import javax.jcr.RepositoryException; import javax.jcr.SimpleCredentials; import org.apache.commons.io.FileUtils; import org.apache.jackrabbit.api.JackrabbitSession; import org.apache.jackrabbit.api.security.user.Authorizable; import org.apache.jackrabbit.api.security.user.Group; import org.apache.jackrabbit.api.security.user.User; import org.apache.jackrabbit.api.security.user.UserManager; import org.apache.jackrabbit.core.RepositoryImpl; import org.apache.jackrabbit.core.config.RepositoryConfig; import org.apache.jackrabbit.test.JUnitTest; /** * Performance test for JCR-3892. */ public class MembershipCachePerfTest extends JUnitTest { private static final String TEST_USER_PREFIX = "MembershipCacheTestUser-"; private static final String TEST_GROUP_PREFIX = "MembershipCacheTestGroup-"; private static final String REPO_HOME = new File("target", MembershipCachePerfTest.class.getSimpleName()).getPath(); private static final int NUM_USERS = 10000; private static final int NUM_USERS_PER_GROUP = 5000; private static final int NUM_GROUPS = 300; private static final int NUM_READERS = 8; private static final int NUM_WRITERS = 8; private static final int TIME_TEST = 20000; private static final int TIME_RAMP_UP = 1000; private RepositoryImpl repo; private JackrabbitSession session; private UserManager userMgr; private MembershipCache cache; @Override protected void setUp() throws Exception { super.setUp(); FileUtils.deleteDirectory(new File(REPO_HOME)); RepositoryConfig config = RepositoryConfig.create( getClass().getResourceAsStream("repository-membersplit.xml"), REPO_HOME); repo = RepositoryImpl.create(config); session = createSession(); userMgr = session.getUserManager(); cache = ((UserManagerImpl) userMgr).getMembershipCache(); boolean autoSave = userMgr.isAutoSave(); userMgr.autoSave(false); // create test users and groups System.out.printf("Creating %d users...\n", NUM_USERS); List<User> users = new ArrayList<User>(); for (int i = 0; i < NUM_USERS; i++) { users.add(userMgr.createUser(TEST_USER_PREFIX + i, "secret")); } System.out.printf("Creating %d groups...\n", NUM_GROUPS); for (int i = 0; i < NUM_GROUPS; i++) { Group g = userMgr.createGroup(TEST_GROUP_PREFIX + i); for (int j=0; j<NUM_USERS_PER_GROUP; j++) { g.addMember(users.get(j)); } session.save(); System.out.printf(".").flush(); } userMgr.autoSave(autoSave); logger.info("Initial cache size: " + cache.getSize()); } @Override protected void tearDown() throws Exception { boolean autoSave = userMgr.isAutoSave(); userMgr.autoSave(false); for (int i = 0; i < NUM_USERS; i++) { userMgr.getAuthorizable(TEST_USER_PREFIX + i).remove(); } for (int i = 0; i < NUM_GROUPS; i++) { userMgr.getAuthorizable(TEST_GROUP_PREFIX + i).remove(); } session.save(); userMgr.autoSave(autoSave); userMgr = null; cache = null; session.logout(); repo.shutdown(); repo = null; FileUtils.deleteDirectory(new File(REPO_HOME)); super.tearDown(); } public void testInvalidationPerformance() throws Exception { List<Exception> exceptions = Collections.synchronizedList(new ArrayList<Exception>()); List<Reader> readers = new ArrayList<Reader>(); Stats readerStats = new Stats(); for (int i = 0; i < NUM_READERS; i++) { Reader r = new Reader(createSession(), readerStats, exceptions); readers.add(r); } List<Writer> writers = new ArrayList<Writer>(); Stats writerStats = new Stats(); for (int i = 0; i < NUM_WRITERS; i++) { Writer w = new Writer(createSession(), writerStats, exceptions); writers.add(w); } Node test = session.getRootNode().addNode("test", "nt:unstructured"); session.save(); for (Reader r : readers) { r.start(); } // invalidate stats after ramp-up Thread.sleep(TIME_RAMP_UP); cache.clear(); readerStats.clear(); // start writers for (Writer w : writers) { w.start(); } long endTime = System.currentTimeMillis() + TIME_TEST; while (System.currentTimeMillis() < endTime) { Thread.sleep(1000); System.out.printf("running...current cache size: %d\n", cache.getSize()); } for (Reader r : readers) { r.setRunning(false); } for (Writer w : writers) { w.setRunning(false); } for (Reader r : readers) { r.join(); } for (Writer w : writers) { w.join(); } test.remove(); session.save(); System.out.printf("-----------------------------------------------\n"); System.out.printf("Test time: %d, Ramp-up time %d\n", TIME_TEST, TIME_RAMP_UP); System.out.printf("Number of users: %d\n", NUM_USERS); System.out.printf("Avg number of users/group: %d\n", NUM_USERS_PER_GROUP); System.out.printf("Number of groups: %d\n", NUM_GROUPS); System.out.printf("Number of readers: %d\n", NUM_READERS); System.out.printf("Number of writers: %d\n", NUM_WRITERS); System.out.printf("Cache size: %d\n", cache.getSize()); System.out.printf("Time to get memberships:\n"); readerStats.printResults(System.out); System.out.printf("-----------------------------------------------\n"); System.out.printf("Time to alter memberships:\n"); writerStats.printResults(System.out); System.out.printf("-----------------------------------------------\n"); for (Exception e : exceptions) { throw e; } logger.info("cache size: " + cache.getSize()); } private JackrabbitSession createSession() throws RepositoryException { return (JackrabbitSession) repo.login( new SimpleCredentials("admin", "admin".toCharArray())); } private static final class Reader extends Thread { private final JackrabbitSession session; private final UserManager userMgr; private final Stats stats; private final Random random = new Random(); private final List<Exception> exceptions; private boolean running = true; public Reader(JackrabbitSession s, Stats stats, List<Exception> exceptions) throws RepositoryException { this.session = s; this.userMgr = s.getUserManager(); this.stats = stats; this.exceptions = exceptions; } public void setRunning(boolean running) { this.running = running; } public void run() { try { while (running) { int idx = random.nextInt(NUM_USERS); Authorizable user = userMgr.getAuthorizable(TEST_USER_PREFIX + idx); long time = System.nanoTime(); user.memberOf(); stats.logTime(System.nanoTime() - time); } } catch (RepositoryException e) { exceptions.add(e); } finally { session.logout(); } } } private static final class Writer extends Thread { private final JackrabbitSession session; private final UserManager userMgr; private final Stats stats; private final Random random = new Random(); private final List<Exception> exceptions; private boolean running = true; public Writer(JackrabbitSession s, Stats stats, List<Exception> exceptions) throws RepositoryException { this.session = s; this.stats = stats; this.userMgr = s.getUserManager(); userMgr.autoSave(false); this.exceptions = exceptions; } public void setRunning(boolean running) { this.running = running; } public void run() { try { while (running) { int userIdx = random.nextInt(NUM_USERS); int groupIdx = random.nextInt(NUM_GROUPS); User user = (User) userMgr.getAuthorizable(TEST_USER_PREFIX + userIdx); Group group = (Group) userMgr.getAuthorizable(TEST_GROUP_PREFIX + groupIdx); do { long time = System.nanoTime(); try { if (group.isDeclaredMember(user)) { group.removeMember(user); } else { group.addMember(user); } session.save(); stats.logTime(System.nanoTime() - time); break; } catch (InvalidItemStateException e) { // concurrent writing...try again session.refresh(false); } } while (running); Thread.sleep(10); } } catch (RepositoryException e) { exceptions.add(e); } catch (InterruptedException e) { exceptions.add(e); } finally { session.logout(); } } } private static final class Stats { private AtomicLong[] buckets = new AtomicLong[20]; public Stats() { for (int i = 0; i < buckets.length; i++) { buckets[i] = new AtomicLong(); } } void logTime(long nanos) { if (nanos == 0) { buckets[0].incrementAndGet(); } else { buckets[(int) Math.log10(nanos)].incrementAndGet(); } } void clear() { for (AtomicLong b: buckets) { b.set(0); } } @Override public String toString() { StringBuilder sb = new StringBuilder(); String separator = ""; for (AtomicLong bucket : buckets) { sb.append(separator); sb.append(bucket.get()); separator = ","; } return sb.toString(); } public void printResults(PrintStream out) { long total = 0; long last = 0; for (int power = 0; power<buckets.length; power++) { long value = buckets[power].get(); total += value; if (value > 0) { last = power; } } if (last == 0) { last = buckets.length - 1; } String[] units = {"ns", "10ns", "100ns", "1us", "10us", "100us", "1ms", "10ms", "100ms"}; for (int power = 0; power<=last; power++) { long value = buckets[power].get(); String unit = power < units.length ? units[power] : Math.pow(10, power-units.length) + "s"; out.printf("%-6s: %2.2f%% (%d)\n", unit, 100.0 * (double) value / (double) total, value); } } } }