/* * (C) Copyright 2014 Nuxeo SA (http://nuxeo.com/) and others. * * Licensed 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. * * Contributors: * Florent Guillaume */ package org.nuxeo.ecm.core; import static org.junit.Assert.assertEquals; import static org.junit.Assume.assumeTrue; import java.util.concurrent.CountDownLatch; import java.util.concurrent.CyclicBarrier; import java.util.concurrent.TimeUnit; import javax.inject.Inject; import org.junit.Before; import org.junit.Ignore; import org.junit.Test; import org.junit.runner.RunWith; import org.nuxeo.ecm.core.api.CoreInstance; import org.nuxeo.ecm.core.api.CoreSession; import org.nuxeo.ecm.core.api.DocumentModel; import org.nuxeo.ecm.core.api.DocumentModelList; import org.nuxeo.ecm.core.api.security.ACE; import org.nuxeo.ecm.core.api.security.impl.ACLImpl; import org.nuxeo.ecm.core.api.security.impl.ACPImpl; import org.nuxeo.ecm.core.event.EventService; import org.nuxeo.ecm.core.query.sql.NXQL; import org.nuxeo.ecm.core.test.CoreFeature; import org.nuxeo.ecm.core.test.annotations.Granularity; import org.nuxeo.ecm.core.test.annotations.RepositoryConfig; import org.nuxeo.runtime.test.runner.Features; import org.nuxeo.runtime.test.runner.FeaturesRunner; import org.nuxeo.runtime.transaction.TransactionHelper; /** * Tests read ACLs behavior in a transactional setting. */ @RunWith(FeaturesRunner.class) @Features(CoreFeature.class) @RepositoryConfig(cleanup = Granularity.METHOD) public class TestSQLRepositoryReadAcls { @Inject protected CoreFeature coreFeature; @Inject protected EventService eventService; @Inject protected CoreSession session; @Before public void setUp() { assumeTrue(coreFeature.getStorageConfiguration().isVCS()); } protected void waitForAsyncCompletion() { nextTransaction(); eventService.waitForAsyncCompletion(); } protected void nextTransaction() { if (TransactionHelper.isTransactionActiveOrMarkedRollback()) { TransactionHelper.commitOrRollbackTransaction(); TransactionHelper.startTransaction(); } } @Test public void testParallelPrepareUserReadAcls() throws Throwable { assumeTrue(!coreFeature.getStorageConfiguration().isVCSOracle()); // NXP-18684 doParallelPrepareUserReadAcls(0); } // fails on SQL Server sometimes (expected:<1> but was:<0>) @Ignore @Test public void testParallelPrepareUserReadAclsMany() throws Throwable { for (int i = 0; i < 100; i++) { // log.debug("try " + i); doParallelPrepareUserReadAcls(i); } } protected void doParallelPrepareUserReadAcls(int i) throws Throwable { String repositoryName = session.getRepositoryName(); // set ACP on root ACPImpl acp = new ACPImpl(); ACLImpl acl = new ACLImpl(); String username = "user" + i; acl.add(new ACE("Administrator", "Everything", true)); acl.add(new ACE(username, "Everything", true)); acp.addACL(acl); String name = "doc" + i; DocumentModel doc = session.createDocumentModel("/", name, "File"); doc = session.createDocument(doc); doc.setACP(acp, true); session.save(); waitForAsyncCompletion(); CyclicBarrier barrier = new CyclicBarrier(2); CountDownLatch firstReady = new CountDownLatch(1); PrepareUserReadAclsJob r1 = new PrepareUserReadAclsJob(name, username, repositoryName, firstReady, barrier); PrepareUserReadAclsJob r2 = new PrepareUserReadAclsJob(name, username, repositoryName, null, barrier); Thread t1 = null; Thread t2 = null; try { t1 = new Thread(r1, "t1"); t2 = new Thread(r2, "t2"); t1.start(); if (firstReady.await(60, TimeUnit.SECONDS)) { t2.start(); t1.join(); t1 = null; t2.join(); t2 = null; if (r1.throwable != null) { throw r1.throwable; } if (r2.throwable != null) { throw r2.throwable; } } // else timed out } finally { // error condition recovery if (t1 != null) { t1.interrupt(); } if (t2 != null) { t2.interrupt(); } } // after both threads have run, check that we don't see // duplicate documents try (CoreSession session = CoreInstance.openCoreSession(repositoryName, username)) { checkOneDoc(session, name); // failed for PostgreSQL } } protected static void checkOneDoc(CoreSession session, String name) { String query = "SELECT * FROM File WHERE ecm:isProxy = 0 AND ecm:name = '" + name + "'"; DocumentModelList res = session.query(query, NXQL.NXQL, null, 0, 0, false); assertEquals(1, res.size()); } protected static class PrepareUserReadAclsJob implements Runnable { private String name; private String username; private String repositoryName; public CountDownLatch ready; public CyclicBarrier barrier; public Throwable throwable; public PrepareUserReadAclsJob(String name, String username, String repositoryName, CountDownLatch ready, CyclicBarrier barrier) { this.name = name; this.username = username; this.repositoryName = repositoryName; this.ready = ready; this.barrier = barrier; } @Override public void run() { TransactionHelper.startTransaction(); try (CoreSession session = CoreInstance.openCoreSession(repositoryName, username)) { if (ready != null) { ready.countDown(); ready = null; } barrier.await(30, TimeUnit.SECONDS); // (throws on timeout) barrier = null; checkOneDoc(session, name); // fails for Oracle } catch (Throwable t) { t.printStackTrace(); throwable = t; } finally { TransactionHelper.commitOrRollbackTransaction(); // error recovery // still count down as main thread is awaiting us if (ready != null) { ready.countDown(); } // break barrier for other thread if (barrier != null) { barrier.reset(); // break barrier } } } } }