Author: davide Date: Mon Feb 2 16:07:35 2015 New Revision: 1656506 URL: http://svn.apache.org/r1656506 Log: OAK-2220: Support for atomic counters
- Atomic counter support for non-cluster solution Added: jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/atomic/ jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/atomic/AtomicCounterEditor.java jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/atomic/AtomicCounterEditorProvider.java jackrabbit/oak/trunk/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/atomic/ jackrabbit/oak/trunk/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/atomic/AtomicCounterEditorTest.java jackrabbit/oak/trunk/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/AtomicCounterIT.java jackrabbit/oak/trunk/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/AtomicCounterTest.java Modified: jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/nodetype/NodeTypeConstants.java jackrabbit/oak/trunk/oak-core/src/main/resources/org/apache/jackrabbit/oak/plugins/nodetype/write/builtin_nodetypes.cnd jackrabbit/oak/trunk/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/Jcr.java Added: jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/atomic/AtomicCounterEditor.java URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/atomic/AtomicCounterEditor.java?rev=1656506&view=auto ============================================================================== --- jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/atomic/AtomicCounterEditor.java (added) +++ jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/atomic/AtomicCounterEditor.java Mon Feb 2 16:07:35 2015 @@ -0,0 +1,200 @@ +/* + * 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.oak.plugins.atomic; + +import static com.google.common.base.Preconditions.checkNotNull; +import static org.apache.jackrabbit.JcrConstants.JCR_MIXINTYPES; +import static org.apache.jackrabbit.oak.api.Type.LONG; +import static org.apache.jackrabbit.oak.api.Type.NAMES; +import static org.apache.jackrabbit.oak.plugins.nodetype.NodeTypeConstants.MIX_ATOMIC_COUNTER; + +import java.util.UUID; + +import javax.annotation.Nonnull; + +import org.apache.jackrabbit.oak.api.CommitFailedException; +import org.apache.jackrabbit.oak.api.PropertyState; +import org.apache.jackrabbit.oak.spi.commit.DefaultEditor; +import org.apache.jackrabbit.oak.spi.commit.Editor; +import org.apache.jackrabbit.oak.spi.state.NodeBuilder; +import org.apache.jackrabbit.oak.spi.state.NodeState; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.common.collect.Iterators; + +/** + * <p> + * Manages a node as <em>Atomic Counter</em>: a node which will handle at low level a protected + * property ({@link #PROP_COUNTER}) in an atomic way. This will represent an increment or decrement + * of a counter in the case, for example, of <em>Likes</em> or <em>Voting</em>. + * </p> + * + * <p> + * Whenever you add a {@link NodeTypeConstants#MIX_ATOMIC_COUNTER} mixin to a node it will turn it + * into an atomic counter. Then in order to increment or decrement the {@code oak:counter} property + * you'll need to set the {@code oak:increment} one ({@link #PROP_INCREMENT). Please note that the + * <strong>{@code oak:incremement} will never be saved</strong>, only the {@code oak:counter} will + * be amended accordingly. + * </p> + * + * <p> + * So in order to deal with the counter from a JCR point of view you'll do something as follows + * </p> + * + * <pre> + * Session session = ... + * + * // creating a counter node + * Node counter = session.getRootNode().addNode("mycounter"); + * counter.addMixin("mix:atomicCounter"); // or use the NodeTypeConstants + * session.save(); + * + * // Will output 0. the default value + * System.out.println("counter now: " + counter.getProperty("oak:counter").getLong()); + * + * // incrementing by 5 the counter + * counter.setProperty("oak:increment", 5); + * session.save(); + * + * // Will output 5 + * System.out.println("counter now: " + counter.getProperty("oak:counter").getLong()); + * + * // decreasing by 1 + * counter.setProperty("oak:increment", -1); + * session.save(); + * + * // Will output 4 + * System.out.println("counter now: " + counter.getProperty("oak:counter").getLong()); + * + * session.logout(); + * </pre> + */ +public class AtomicCounterEditor extends DefaultEditor { + /** + * property to be set for incrementing/decrementing the counter + */ + public static final String PROP_INCREMENT = "oak:increment"; + + /** + * property with the consolidated counter + */ + public static final String PROP_COUNTER = "oak:counter"; + + /** + * prefix used internally for tracking the counting requests + */ + public static final String PREFIX_PROP_COUNTER = ":oak-counter-"; + + private static final Logger LOG = LoggerFactory.getLogger(AtomicCounterEditor.class); + private final NodeBuilder builder; + private final String path; + + /** + * instruct whether to update the node on leave. + */ + private boolean update; + + public AtomicCounterEditor(@Nonnull final NodeBuilder builder) { + this("", checkNotNull(builder)); + } + + private AtomicCounterEditor(final String path, final NodeBuilder builder) { + this.builder = checkNotNull(builder); + this.path = path; + } + + private static boolean shallWeProcessProperty(final PropertyState property, + final String path, + final NodeBuilder builder) { + boolean process = false; + PropertyState mixin = checkNotNull(builder).getProperty(JCR_MIXINTYPES); + if (mixin != null && PROP_INCREMENT.equals(property.getName()) && + Iterators.contains(mixin.getValue(NAMES).iterator(), MIX_ATOMIC_COUNTER)) { + if (LONG.equals(property.getType())) { + process = true; + } else { + LOG.warn( + "although the {} property is set is not of the right value: LONG. Not processing node: {}.", + PROP_INCREMENT, path); + } + } + return process; + } + + /** + * <p> + * consolidate the {@link #PREFIX_PROP_COUNTER} properties and sum them into the + * {@link #PROP_COUNTER} + * </p> + * + * <p> + * The passed in {@code NodeBuilder} must have + * {@link org.apache.jackrabbit.JcrConstants#JCR_MIXINTYPES JCR_MIXINTYPES} with + * {@link org.apache.jackrabbit.oak.plugins.nodetype.NodeTypeConstants#MIX_ATOMIC_COUNTER MIX_ATOMIC_COUNTER}. + * If not it will be silently ignored. + * </p> + * + * @param builder the builder to work on. Cannot be null. + */ + public static void consolidateCount(@Nonnull final NodeBuilder builder) { + long count = builder.hasProperty(PROP_COUNTER) + ? builder.getProperty(PROP_COUNTER).getValue(LONG) + : 0; + for (PropertyState p : builder.getProperties()) { + if (p.getName().startsWith(PREFIX_PROP_COUNTER)) { + count += p.getValue(LONG); + builder.removeProperty(p.getName()); + } + } + + builder.setProperty(PROP_COUNTER, count); + } + + private void setUniqueCounter(final long value) { + update = true; + builder.setProperty(PREFIX_PROP_COUNTER + UUID.randomUUID(), value, LONG); + } + + @Override + public void propertyAdded(final PropertyState after) throws CommitFailedException { + if (shallWeProcessProperty(after, path, builder)) { + setUniqueCounter(after.getValue(LONG)); + builder.removeProperty(PROP_INCREMENT); + } + } + + @Override + public Editor childNodeAdded(final String name, final NodeState after) throws CommitFailedException { + return new AtomicCounterEditor(path + '/' + name, builder.getChildNode(name)); + } + + @Override + public Editor childNodeChanged(final String name, + final NodeState before, + final NodeState after) throws CommitFailedException { + return new AtomicCounterEditor(path + '/' + name, builder.getChildNode(name)); + } + + @Override + public void leave(final NodeState before, final NodeState after) throws CommitFailedException { + if (update) { + // TODO here is where the Async check could be done + consolidateCount(builder); + } + } +} Added: jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/atomic/AtomicCounterEditorProvider.java URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/atomic/AtomicCounterEditorProvider.java?rev=1656506&view=auto ============================================================================== --- jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/atomic/AtomicCounterEditorProvider.java (added) +++ jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/atomic/AtomicCounterEditorProvider.java Mon Feb 2 16:07:35 2015 @@ -0,0 +1,41 @@ +/* + * 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.oak.plugins.atomic; + +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Service; +import org.apache.jackrabbit.oak.api.CommitFailedException; +import org.apache.jackrabbit.oak.spi.commit.CommitInfo; +import org.apache.jackrabbit.oak.spi.commit.Editor; +import org.apache.jackrabbit.oak.spi.commit.EditorProvider; +import org.apache.jackrabbit.oak.spi.state.NodeBuilder; +import org.apache.jackrabbit.oak.spi.state.NodeState; + +/** + * Provide an instance of {@link AtomicCounterEditor} + */ +@Component +@Service +public class AtomicCounterEditorProvider implements EditorProvider { + + @Override + public Editor getRootEditor(final NodeState before, final NodeState after, + final NodeBuilder builder, final CommitInfo info) + throws CommitFailedException { + return new AtomicCounterEditor(builder); + } +} Modified: jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/nodetype/NodeTypeConstants.java URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/nodetype/NodeTypeConstants.java?rev=1656506&r1=1656505&r2=1656506&view=diff ============================================================================== --- jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/nodetype/NodeTypeConstants.java (original) +++ jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/nodetype/NodeTypeConstants.java Mon Feb 2 16:07:35 2015 @@ -90,5 +90,9 @@ public interface NodeTypeConstants exten String REP_PRIMARY_TYPE = "rep:primaryType"; String REP_MIXIN_TYPES = "rep:mixinTypes"; String REP_UUID = "rep:uuid"; - + + /** + * mixin to enable the AtomicCounterEditor. + */ + String MIX_ATOMIC_COUNTER = "mix:atomicCounter"; } Modified: jackrabbit/oak/trunk/oak-core/src/main/resources/org/apache/jackrabbit/oak/plugins/nodetype/write/builtin_nodetypes.cnd URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-core/src/main/resources/org/apache/jackrabbit/oak/plugins/nodetype/write/builtin_nodetypes.cnd?rev=1656506&r1=1656505&r2=1656506&view=diff ============================================================================== --- jackrabbit/oak/trunk/oak-core/src/main/resources/org/apache/jackrabbit/oak/plugins/nodetype/write/builtin_nodetypes.cnd (original) +++ jackrabbit/oak/trunk/oak-core/src/main/resources/org/apache/jackrabbit/oak/plugins/nodetype/write/builtin_nodetypes.cnd Mon Feb 2 16:07:35 2015 @@ -244,6 +244,14 @@ [mix:etag] mixin - jcr:etag (STRING) protected autocreated + +/** + * mix:atomicCounter will define a node as a counter which will keep tracks of increasing/decreasing + * consistently across a cluster of oak instances. https://issues.apache.org/jira/browse/OAK-2220 + */ +[mix:atomicCounter] + mixin + - oak:counter (LONG) = '0' protected autocreated //------------------------------------------------------------------------------ // U N S T R U C T U R E D C O N T E N T Added: jackrabbit/oak/trunk/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/atomic/AtomicCounterEditorTest.java URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/atomic/AtomicCounterEditorTest.java?rev=1656506&view=auto ============================================================================== --- jackrabbit/oak/trunk/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/atomic/AtomicCounterEditorTest.java (added) +++ jackrabbit/oak/trunk/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/atomic/AtomicCounterEditorTest.java Mon Feb 2 16:07:35 2015 @@ -0,0 +1,140 @@ +/* + * 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.oak.plugins.atomic; + +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.collect.ImmutableList.of; +import static org.apache.jackrabbit.JcrConstants.JCR_MIXINTYPES; +import static org.apache.jackrabbit.oak.api.Type.LONG; +import static org.apache.jackrabbit.oak.api.Type.NAMES; +import static org.apache.jackrabbit.oak.plugins.atomic.AtomicCounterEditor.PREFIX_PROP_COUNTER; +import static org.apache.jackrabbit.oak.plugins.atomic.AtomicCounterEditor.PROP_COUNTER; +import static org.apache.jackrabbit.oak.plugins.atomic.AtomicCounterEditor.PROP_INCREMENT; +import static org.apache.jackrabbit.oak.plugins.memory.EmptyNodeState.EMPTY_NODE; +import static org.apache.jackrabbit.oak.plugins.nodetype.NodeTypeConstants.MIX_ATOMIC_COUNTER; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +import javax.annotation.Nonnull; + +import org.apache.jackrabbit.oak.api.CommitFailedException; +import org.apache.jackrabbit.oak.api.PropertyState; +import org.apache.jackrabbit.oak.api.Type; +import org.apache.jackrabbit.oak.plugins.memory.PropertyStates; +import org.apache.jackrabbit.oak.spi.commit.Editor; +import org.apache.jackrabbit.oak.spi.state.NodeBuilder; +import org.junit.Ignore; +import org.junit.Test; + +public class AtomicCounterEditorTest { + @Test + @Ignore // FIXME fix test expectations + public void childNodeAdded() throws CommitFailedException { + NodeBuilder builder = EMPTY_NODE.builder(); + + Editor editor = new AtomicCounterEditor(EMPTY_NODE.builder()); + + assertNull("without the mixin we should not process", + editor.childNodeAdded("foo", builder.getNodeState())); + + builder = EMPTY_NODE.builder(); + builder = setMixin(builder); + assertTrue("with the mixin set we should get a proper Editor", + editor.childNodeAdded("foo", builder.getNodeState()) instanceof AtomicCounterEditor); + } + + @Test + public void increment() throws CommitFailedException { + NodeBuilder builder; + Editor editor; + PropertyState property; + + builder = EMPTY_NODE.builder(); + editor = new AtomicCounterEditor(builder); + property = PropertyStates.createProperty(PROP_INCREMENT, 1L, Type.LONG); + editor.propertyAdded(property); + assertNoCounters(builder.getProperties()); + + builder = EMPTY_NODE.builder(); + builder = setMixin(builder); + editor = new AtomicCounterEditor(builder); + property = PropertyStates.createProperty(PROP_INCREMENT, 1L, Type.LONG); + editor.propertyAdded(property); + assertNull("the oak:increment should never be set", builder.getProperty(PROP_INCREMENT)); + assertTotalCounters(builder.getProperties(), 1); + } + + @Test + public void consolidate() throws CommitFailedException { + NodeBuilder builder; + Editor editor; + PropertyState property; + + builder = EMPTY_NODE.builder(); + builder = setMixin(builder); + editor = new AtomicCounterEditor(builder); + property = PropertyStates.createProperty(PROP_INCREMENT, 1L, Type.LONG); + + editor.propertyAdded(property); + assertTotalCounters(builder.getProperties(), 1); + editor.propertyAdded(property); + assertTotalCounters(builder.getProperties(), 2); + AtomicCounterEditor.consolidateCount(builder); + assertNotNull(builder.getProperty(PROP_COUNTER)); + assertEquals(2, builder.getProperty(PROP_COUNTER).getValue(LONG).longValue()); + assertNoCounters(builder.getProperties()); + } + + /** + * that a list of properties does not contains any property with name starting with + * {@link AtomicCounterEditor#PREFIX_PROP_COUNTER} + * + * @param properties + */ + private static void assertNoCounters(@Nonnull final Iterable<? extends PropertyState> properties) { + checkNotNull(properties); + + for (PropertyState p : properties) { + assertFalse("there should be no counter property", + p.getName().startsWith(PREFIX_PROP_COUNTER)); + } + } + + /** + * assert the total amount of {@link AtomicCounterEditor#PREFIX_PROP_COUNTER} + * + * @param properties + */ + private static void assertTotalCounters(@Nonnull final Iterable<? extends PropertyState> properties, + int expected) { + int total = 0; + for (PropertyState p : checkNotNull(properties)) { + if (p.getName().startsWith(PREFIX_PROP_COUNTER)) { + total += p.getValue(LONG); + } + } + + assertEquals("the total amount of :oak-counter properties does not match", expected, total); + } + + private static NodeBuilder setMixin(@Nonnull final NodeBuilder builder) { + return checkNotNull(builder).setProperty(JCR_MIXINTYPES, of(MIX_ATOMIC_COUNTER), NAMES); + } +} Modified: jackrabbit/oak/trunk/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/Jcr.java URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/Jcr.java?rev=1656506&r1=1656505&r2=1656506&view=diff ============================================================================== --- jackrabbit/oak/trunk/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/Jcr.java (original) +++ jackrabbit/oak/trunk/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/Jcr.java Mon Feb 2 16:07:35 2015 @@ -46,6 +46,7 @@ import org.apache.jackrabbit.oak.plugins import org.apache.jackrabbit.oak.plugins.version.VersionEditorProvider; import org.apache.jackrabbit.oak.query.QueryEngineSettings; import org.apache.jackrabbit.oak.security.SecurityProviderImpl; +import org.apache.jackrabbit.oak.plugins.atomic.AtomicCounterEditorProvider; import org.apache.jackrabbit.oak.spi.commit.CommitHook; import org.apache.jackrabbit.oak.spi.commit.CompositeConflictHandler; import org.apache.jackrabbit.oak.spi.commit.Editor; @@ -83,6 +84,7 @@ public class Jcr { with(new NamespaceEditorProvider()); with(new TypeEditorProvider()); with(new ConflictValidatorProvider()); + with(new AtomicCounterEditorProvider()); with(new ReferenceEditorProvider()); with(new ReferenceIndexProvider()); Added: jackrabbit/oak/trunk/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/AtomicCounterIT.java URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/AtomicCounterIT.java?rev=1656506&view=auto ============================================================================== --- jackrabbit/oak/trunk/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/AtomicCounterIT.java (added) +++ jackrabbit/oak/trunk/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/AtomicCounterIT.java Mon Feb 2 16:07:35 2015 @@ -0,0 +1,120 @@ +/* + * 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.oak.jcr; + +import static com.google.common.base.Preconditions.checkNotNull; +import static org.apache.jackrabbit.oak.plugins.atomic.AtomicCounterEditor.PROP_COUNTER; +import static org.apache.jackrabbit.oak.plugins.atomic.AtomicCounterEditor.PROP_INCREMENT; +import static org.apache.jackrabbit.oak.plugins.nodetype.NodeTypeConstants.MIX_ATOMIC_COUNTER; +import static org.junit.Assert.assertEquals; +import static org.junit.Assume.assumeTrue; + +import java.util.List; +import java.util.Random; +import java.util.Set; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.atomic.AtomicLong; + +import javax.annotation.Nonnull; +import javax.jcr.Node; +import javax.jcr.RepositoryException; +import javax.jcr.Session; + +import org.apache.jackrabbit.oak.commons.FixturesHelper; +import org.apache.jackrabbit.oak.commons.FixturesHelper.Fixture; +import org.junit.BeforeClass; +import org.junit.Test; + +import com.google.common.collect.Lists; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFutureTask; + +public class AtomicCounterIT extends AbstractRepositoryTest { + private static final Set<Fixture> FIXTURES = FixturesHelper.getFixtures(); + + public AtomicCounterIT(NodeStoreFixture fixture) { + super(fixture); + } + + @BeforeClass + public static void assumptions() { + assumeTrue(FIXTURES.contains(Fixture.SEGMENT_MK)); + } + + @Test + public void concurrentSegmentIncrements() throws RepositoryException, InterruptedException, + ExecutionException { + // ensuring the run only on allowed fix + assumeTrue(NodeStoreFixture.SEGMENT_MK.equals(fixture)); + + // setting-up + Session session = getAdminSession(); + + try { + Node counter = session.getRootNode().addNode("counter"); + counter.addMixin(MIX_ATOMIC_COUNTER); + session.save(); + + final AtomicLong expected = new AtomicLong(0); + final String counterPath = counter.getPath(); + final Random rnd = new Random(11); + + // ensuring initial state + assertEquals(expected.get(), counter.getProperty(PROP_COUNTER).getLong()); + + List<ListenableFutureTask<Void>> tasks = Lists.newArrayList(); + for (int t = 0; t < 100; t++) { + tasks.add(updateCounter(counterPath, rnd.nextInt(10) + 1, expected)); + } + Futures.allAsList(tasks).get(); + + session.refresh(false); + assertEquals(expected.get(), + session.getNode(counterPath).getProperty(PROP_COUNTER).getLong()); + } finally { + session.logout(); + } + } + + private ListenableFutureTask<Void> updateCounter(@Nonnull final String counterPath, + final long delta, + @Nonnull final AtomicLong expected) { + checkNotNull(counterPath); + checkNotNull(expected); + + ListenableFutureTask<Void> task = ListenableFutureTask.create(new Callable<Void>() { + + @Override + public Void call() throws Exception { + Session session = createAdminSession(); + try { + Node c = session.getNode(counterPath); + c.setProperty(PROP_INCREMENT, delta); + expected.addAndGet(delta); + session.save(); + } finally { + session.logout(); + } + return null; + } + }); + + new Thread(task).start(); + return task; + } +} Added: jackrabbit/oak/trunk/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/AtomicCounterTest.java URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/AtomicCounterTest.java?rev=1656506&view=auto ============================================================================== --- jackrabbit/oak/trunk/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/AtomicCounterTest.java (added) +++ jackrabbit/oak/trunk/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/AtomicCounterTest.java Mon Feb 2 16:07:35 2015 @@ -0,0 +1,127 @@ +/* + * 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.oak.jcr; + +import static com.google.common.base.Preconditions.checkNotNull; +import static org.apache.jackrabbit.oak.plugins.atomic.AtomicCounterEditor.PROP_COUNTER; +import static org.apache.jackrabbit.oak.plugins.atomic.AtomicCounterEditor.PROP_INCREMENT; +import static org.apache.jackrabbit.oak.plugins.nodetype.NodeTypeConstants.MIX_ATOMIC_COUNTER; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import javax.annotation.Nonnull; +import javax.jcr.Node; +import javax.jcr.RepositoryException; +import javax.jcr.Session; + +import org.junit.Test; + +public class AtomicCounterTest extends AbstractRepositoryTest { + public AtomicCounterTest(NodeStoreFixture fixture) { + super(fixture); + } + + @Test + public void incrementRootNode() throws RepositoryException { + + Session session = getAdminSession(); + + try { + Node root = session.getRootNode(); + Node node = root.addNode("normal node"); + session.save(); + + node.setProperty(PROP_INCREMENT, 1L); + session.save(); + + assertTrue("for normal nodes we expect the increment property to be treated as normal", + node.hasProperty(PROP_INCREMENT)); + + node = root.addNode("counterNode"); + node.addMixin(MIX_ATOMIC_COUNTER); + session.save(); + + assertCounter(node, 0); + + node.setProperty(PROP_INCREMENT, 1L); + session.save(); + assertCounter(node, 1); + + // increment again the same node + node.setProperty(PROP_INCREMENT, 1L); + session.save(); + assertCounter(node, 2); + + // decrease the counter by 2 + node.setProperty(PROP_INCREMENT, -2L); + session.save(); + assertCounter(node, 0); + + // increase by 5 + node.setProperty(PROP_INCREMENT, 5L); + session.save(); + assertCounter(node, 5); + } finally { + session.logout(); + } + } + + private static void assertCounter(@Nonnull final Node counter, final long expectedCount) + throws RepositoryException { + checkNotNull(counter); + + assertTrue(counter.hasProperty(PROP_COUNTER)); + assertEquals(expectedCount, counter.getProperty(PROP_COUNTER).getLong()); + assertFalse(counter.hasProperty(PROP_INCREMENT)); + } + + @Test + public void incrementNonRootNode() throws RepositoryException { + Session session = getAdminSession(); + + try { + Node counter = session.getRootNode().addNode("foo").addNode("bar").addNode("counter"); + counter.addMixin(MIX_ATOMIC_COUNTER); + session.save(); + + assertCounter(counter, 0); + + counter.setProperty(PROP_INCREMENT, 1L); + session.save(); + assertCounter(counter, 1); + + // increment again the same node + counter.setProperty(PROP_INCREMENT, 1L); + session.save(); + assertCounter(counter, 2); + + // decrease the counter by 2 + counter.setProperty(PROP_INCREMENT, -2L); + session.save(); + assertCounter(counter, 0); + + // increase by 5 + counter.setProperty(PROP_INCREMENT, 5L); + session.save(); + assertCounter(counter, 5); + } finally { + session.logout(); + } + } + +}