Index: oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/DocumentNodeStore.java =================================================================== --- oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/DocumentNodeStore.java (revision 1785837) +++ oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/DocumentNodeStore.java (working copy) @@ -142,6 +142,8 @@ private static final PerfLogger PERFLOG = new PerfLogger( LoggerFactory.getLogger(DocumentNodeStore.class.getName() + ".perf")); + public static final FormatVersion VERSION = FormatVersion.V1_8; + /** * Do not cache more than this number of children for a document. */ @@ -492,6 +494,7 @@ } else { readOnlyMode = false; } + checkVersion(s, readOnlyMode); this.executor = builder.getExecutor(); this.clock = builder.getClock(); @@ -2236,7 +2239,42 @@ //-----------------------------< internal >--------------------------------- - void pushJournalEntry(Revision r) { + /** + * Checks if this node store can operate on the data in the given document + * store. + * + * @param store the document store. + * @param readOnlyMode whether this node store is in read-only mode. + * @throws DocumentStoreException if the versions are incompatible given the + * access mode (read-write vs. read-only). + */ + private static void checkVersion(DocumentStore store, boolean readOnlyMode) + throws DocumentStoreException { + FormatVersion storeVersion = FormatVersion.versionOf(store); + if (!VERSION.canRead(storeVersion)) { + throw new DocumentStoreException("Cannot open DocumentNodeStore. " + + "Existing data in DocumentStore was written with more " + + "recent version. Store version: " + storeVersion + + ", this version: " + VERSION); + } + if (!readOnlyMode) { + if (storeVersion == FormatVersion.V0) { + // no version present. set to current version + VERSION.writeTo(store); + LOG.info("FormatVersion is now {}", VERSION); + } else if (!VERSION.equals(storeVersion)) { + // version does not match. fail the check and + // require a manual upgrade first + throw new DocumentStoreException("Cannot open DocumentNodeStore " + + "in read-write mode. Existing data in DocumentStore " + + "was written with older version. Store version: " + + storeVersion + ", this version: " + VERSION + ". Use " + + "the oak-run tool with the unlockUpgrade command first."); + } + } + } + + private void pushJournalEntry(Revision r) { if (!changes.hasChanges()) { LOG.debug("Not pushing journal as there are no changes"); } else if (store.create(JOURNAL, singletonList(changes.asUpdateOp(r)))) { Index: oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/FormatVersion.java =================================================================== --- oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/FormatVersion.java (nonexistent) +++ oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/FormatVersion.java (working copy) @@ -0,0 +1,263 @@ +/* + * 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.document; + +import java.util.List; + +import javax.annotation.Nonnull; + +import com.google.common.collect.ComparisonChain; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Lists; + +import static com.google.common.base.Preconditions.checkNotNull; +import static org.apache.jackrabbit.oak.plugins.document.Collection.SETTINGS; + +/** + * The format version currently in use by the DocumentNodeStore and written + * to the underlying DocumentStore. A version {@link #canRead} the current or + * older versions. + */ +public final class FormatVersion implements Comparable { + + /** + * A dummy version when none is available. + */ + static final FormatVersion V0 = new FormatVersion(0, 0, 0); + + /** + * Format version for Oak 1.0. + */ + static final FormatVersion V1_0 = new FormatVersion(1, 0, 0); + + /** + * Format version for Oak 1.2. + *

+ * Changes introduced with this version: + *

+ */ + static final FormatVersion V1_2 = new FormatVersion(1, 2, 0); + + /** + * Format version for Oak 1.4. + *

+ * Changes introduced with this version: + *

+ */ + static final FormatVersion V1_4 = new FormatVersion(1, 4, 0); + + /** + * Format version for Oak 1.6. + *

+ * Changes introduced with this version: + *

+ */ + static final FormatVersion V1_6 = new FormatVersion(1, 6, 0); + + /** + * Format version for Oak 1.8. + *

+ * Changes introduced with this version: + *

+ */ + static final FormatVersion V1_8 = new FormatVersion(1, 8, 0); + + /** + * The ID of the document in the settings collection that contains the + * version information. + */ + private static final String VERSION_ID = "version"; + + /** + * @return well known format versions. + */ + public static Iterable values() { + return ImmutableList.of(V0, V1_0, V1_2, V1_4, V1_6, V1_8); + } + + /** + * Name of the version property. + */ + private static final String PROP_VERSION = "_v"; + + private final int major, minor, micro; + + private FormatVersion(int major, int minor, int micro) { + this.major = major; + this.minor = minor; + this.micro = micro; + } + + /** + * Returns {@code true} if {@code this} version can read data written by the + * {@code other} version. + * + * @param other the version the data was written in. + * @return {@code true} if this version can read, {@code false} otherwise. + */ + public boolean canRead(FormatVersion other) { + return compareTo(checkNotNull(other)) >= 0; + } + + /** + * Reads the version from the given store. + * + * @param store the store to read from. + * @return the format version of the store. + * @throws DocumentStoreException if an error occurs while reading from the + * store. + */ + @Nonnull + public static FormatVersion versionOf(@Nonnull DocumentStore store) + throws DocumentStoreException { + checkNotNull(store); + FormatVersion v = V0; + Document d = store.find(SETTINGS, VERSION_ID); + if (d != null) { + Object p = d.get(PROP_VERSION); + if (p != null) { + try { + v = valueOf(p.toString()); + } catch (IllegalArgumentException e) { + throw new DocumentStoreException(e); + } + } + } + return v; + } + + /** + * Writes this version to the given document store. + * + * @param store the document store. + * @return {@code true} if the version in the store was updated, + * {@code false} otherwise. This method will also return {@code false} + * if the version in the store equals this version and now update was + * required. + * @throws DocumentStoreException if the write operation fails. Reasons + * include: 1) an attempt to downgrade the existing version, 2) there + * are active cluster nodes using an existing version, 3) the version + * was changed concurrently. + */ + public boolean writeTo(@Nonnull DocumentStore store) + throws DocumentStoreException { + checkNotNull(store); + FormatVersion v = versionOf(store); + if (v == this) { + // already on this version + return false; + } + if (!canRead(v)) { + // never downgrade + throw unableToWrite("Version " + this + " cannot read " + v); + } + List active = Lists.newArrayList(); + for (ClusterNodeInfoDocument d : ClusterNodeInfoDocument.all(store)) { + if (d.isActive()) { + active.add(d.getClusterId()); + } + } + if (!active.isEmpty() && v != V0) { + throw unableToWrite("There are active cluster nodes: " + active); + } + if (v == V0) { + UpdateOp op = new UpdateOp(VERSION_ID, true); + op.set(PROP_VERSION, toString()); + if (!store.create(SETTINGS, Lists.newArrayList(op))) { + throw unableToWrite("Version was updated concurrently"); + } + } else { + UpdateOp op = new UpdateOp(VERSION_ID, false); + op.equals(PROP_VERSION, v.toString()); + op.set(PROP_VERSION, toString()); + if (store.findAndUpdate(SETTINGS, op) == null) { + throw unableToWrite("Version was updated concurrently"); + } + } + return true; + } + + /** + * Returns a format version for the given String representation. This method + * either returns one of the well known versions or an entirely new version + * if the version is not well known. + * + * @param s the String representation of a format version. + * @return the parsed format version. + * @throws IllegalArgumentException if the string is malformed. + */ + public static FormatVersion valueOf(String s) + throws IllegalArgumentException { + String[] parts = s.split("\\."); + if (parts.length != 3) { + throw new IllegalArgumentException(s); + } + int[] elements = new int[parts.length]; + for (int i = 0; i < parts.length; i++) { + try { + elements[i] = Integer.parseInt(parts[i]); + } catch (NumberFormatException e) { + throw new IllegalArgumentException(s); + } + } + FormatVersion v = new FormatVersion(elements[0], elements[1], elements[2]); + for (FormatVersion known : values()) { + if (v.equals(known)) { + v = known; + break; + } + } + return v; + } + + @Override + public String toString() { + return major + "." + minor + "." + micro; + } + + @Override + public boolean equals(Object obj) { + return obj instanceof FormatVersion + && compareTo((FormatVersion) obj) == 0; + } + + @Override + public int compareTo(@Nonnull FormatVersion other) { + checkNotNull(other); + return ComparisonChain.start() + .compare(major, other.major) + .compare(minor, other.minor) + .compare(micro, other.micro) + .result(); + } + + private static DocumentStoreException unableToWrite(String reason) { + return new DocumentStoreException( + "Unable to write format version. " + reason); + } +} Property changes on: oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/FormatVersion.java ___________________________________________________________________ Added: svn:eol-style ## -0,0 +1 ## +native \ No newline at end of property Index: oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/document/DocumentNodeStoreTest.java =================================================================== --- oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/document/DocumentNodeStoreTest.java (revision 1785837) +++ oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/document/DocumentNodeStoreTest.java (working copy) @@ -19,6 +19,7 @@ import static java.util.concurrent.TimeUnit.SECONDS; import static org.apache.jackrabbit.oak.api.CommitFailedException.CONSTRAINT; import static org.apache.jackrabbit.oak.plugins.document.Collection.NODES; +import static org.apache.jackrabbit.oak.plugins.document.Collection.SETTINGS; import static org.apache.jackrabbit.oak.plugins.document.NodeDocument.MODIFIED_IN_SECS; import static org.apache.jackrabbit.oak.plugins.document.NodeDocument.MODIFIED_IN_SECS_RESOLUTION; import static org.apache.jackrabbit.oak.plugins.document.NodeDocument.NUM_REVS_THRESHOLD; @@ -2942,6 +2943,50 @@ assertNotNull(doc); } + @Test + public void readWriteOldVersion() throws Exception { + DocumentStore store = new MemoryDocumentStore(); + FormatVersion.V1_0.writeTo(store); + try { + new DocumentMK.Builder().setDocumentStore(store).getNodeStore(); + fail("must fail with " + DocumentStoreException.class.getSimpleName()); + } catch (Exception e) { + // expected + } + } + + @Test + public void readOnlyOldVersion() throws Exception { + DocumentStore store = new MemoryDocumentStore(); + FormatVersion.V1_0.writeTo(store); + // initialize store with root node + Revision r = Revision.newRevision(1); + UpdateOp op = new UpdateOp(Utils.getIdFromPath("/"), true); + NodeDocument.setModified(op, r); + NodeDocument.setDeleted(op, r, false); + NodeDocument.setRevision(op, r, "c"); + NodeDocument.setLastRev(op, r); + store.create(NODES, Lists.newArrayList(op)); + // initialize checkpoints document + op = new UpdateOp("checkpoint", true); + store.create(SETTINGS, Lists.newArrayList(op)); + // now try to open in read-only mode with more recent version + builderProvider.newBuilder().setReadOnlyMode().setDocumentStore(store).getNodeStore(); + } + + @Test + public void readMoreRecentVersion() throws Exception { + DocumentStore store = new MemoryDocumentStore(); + FormatVersion futureVersion = FormatVersion.valueOf("999.9.9"); + futureVersion.writeTo(store); + try { + new DocumentMK.Builder().setDocumentStore(store).getNodeStore(); + fail("must fail with " + DocumentStoreException.class.getSimpleName()); + } catch (DocumentStoreException e) { + // expected + } + } + private static class WriteCountingStore extends MemoryDocumentStore { private final ThreadLocal createMulti = new ThreadLocal<>(); int count; Index: oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/document/FormatVersionTest.java =================================================================== --- oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/document/FormatVersionTest.java (nonexistent) +++ oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/document/FormatVersionTest.java (working copy) @@ -0,0 +1,162 @@ +/* + * 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.document; + +import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; + +import com.google.common.collect.ImmutableList; + +import org.apache.jackrabbit.oak.plugins.document.memory.MemoryDocumentStore; +import org.junit.Test; + +import static org.apache.jackrabbit.oak.plugins.document.Collection.SETTINGS; +import static org.apache.jackrabbit.oak.plugins.document.FormatVersion.V0; +import static org.apache.jackrabbit.oak.plugins.document.FormatVersion.V1_0; +import static org.apache.jackrabbit.oak.plugins.document.FormatVersion.V1_2; +import static org.apache.jackrabbit.oak.plugins.document.FormatVersion.V1_4; +import static org.apache.jackrabbit.oak.plugins.document.FormatVersion.V1_6; +import static org.apache.jackrabbit.oak.plugins.document.FormatVersion.V1_8; +import static org.apache.jackrabbit.oak.plugins.document.FormatVersion.valueOf; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; + +public class FormatVersionTest { + + @Test + public void canRead() { + assertTrue(V1_8.canRead(V1_8)); + assertTrue(V1_8.canRead(V1_6)); + assertTrue(V1_8.canRead(V1_4)); + assertTrue(V1_8.canRead(V1_2)); + assertTrue(V1_8.canRead(V1_0)); + assertTrue(V1_8.canRead(V0)); + assertFalse(V1_6.canRead(V1_8)); + assertTrue(V1_6.canRead(V1_6)); + assertTrue(V1_6.canRead(V1_4)); + assertTrue(V1_6.canRead(V1_2)); + assertTrue(V1_6.canRead(V1_0)); + assertTrue(V1_6.canRead(V0)); + assertFalse(V1_4.canRead(V1_8)); + assertFalse(V1_4.canRead(V1_6)); + assertTrue(V1_4.canRead(V1_4)); + assertTrue(V1_4.canRead(V1_2)); + assertTrue(V1_4.canRead(V1_0)); + assertTrue(V1_4.canRead(V0)); + assertFalse(V1_2.canRead(V1_8)); + assertFalse(V1_2.canRead(V1_6)); + assertFalse(V1_2.canRead(V1_4)); + assertTrue(V1_2.canRead(V1_2)); + assertTrue(V1_2.canRead(V1_0)); + assertTrue(V1_2.canRead(V0)); + assertFalse(V1_0.canRead(V1_8)); + assertFalse(V1_0.canRead(V1_6)); + assertFalse(V1_0.canRead(V1_4)); + assertFalse(V1_0.canRead(V1_2)); + assertTrue(V1_0.canRead(V1_0)); + assertTrue(V1_0.canRead(V0)); + } + + @Test + public void toStringValueOf() { + for (FormatVersion v : FormatVersion.values()) { + String s = v.toString(); + assertSame(v, valueOf(s)); + } + } + + @Test + public void valueOfUnknown() { + String s = "0.9.7"; + FormatVersion v = valueOf(s); + assertEquals(s, v.toString()); + } + + @Test + public void versionOf() throws Exception { + DocumentStore store = new MemoryDocumentStore(); + FormatVersion v = FormatVersion.versionOf(store); + assertSame(V0, v); + } + + @Test + public void writeTo() throws Exception { + DocumentStore store = new MemoryDocumentStore(); + // must not write dummy version + assertFalse(V0.writeTo(store)); + // upgrade + for (FormatVersion v : ImmutableList.of(V1_0, V1_2, V1_4, V1_6, V1_8)) { + assertTrue(v.writeTo(store)); + assertSame(v, FormatVersion.versionOf(store)); + } + } + + @Test(expected = DocumentStoreException.class) + public void downgrade() throws Exception { + DocumentStore store = new MemoryDocumentStore(); + assertTrue(V1_4.writeTo(store)); + // must not downgrade + V1_2.writeTo(store); + } + + @Test(expected = DocumentStoreException.class) + public void activeClusterNodes() throws Exception { + DocumentStore store = new MemoryDocumentStore(); + V1_0.writeTo(store); + ClusterNodeInfo info = ClusterNodeInfo.getInstance(store, 1); + info.renewLease(); + V1_2.writeTo(store); + } + + @Test(expected = DocumentStoreException.class) + public void concurrentUpdate1() throws Exception { + DocumentStore store = new MemoryDocumentStore() { + private final AtomicBoolean once = new AtomicBoolean(false); + @Override + public T findAndUpdate(Collection collection, + UpdateOp update) { + if (collection == SETTINGS + && !once.getAndSet(true)) { + V1_2.writeTo(this); + } + return super.findAndUpdate(collection, update); + } + }; + V1_0.writeTo(store); + V1_2.writeTo(store); + } + + @Test(expected = DocumentStoreException.class) + public void concurrentUpdate2() throws Exception { + DocumentStore store = new MemoryDocumentStore() { + private final AtomicBoolean once = new AtomicBoolean(false); + + @Override + public boolean create(Collection collection, + List updateOps) { + if (collection == SETTINGS + && !once.getAndSet(true)) { + V1_0.writeTo(this); + } + return super.create(collection, updateOps); + } + }; + V1_0.writeTo(store); + } +} Property changes on: oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/document/FormatVersionTest.java ___________________________________________________________________ Added: svn:eol-style ## -0,0 +1 ## +native \ No newline at end of property Index: oak-run/src/main/java/org/apache/jackrabbit/oak/run/Mode.java =================================================================== --- oak-run/src/main/java/org/apache/jackrabbit/oak/run/Mode.java (revision 1785837) +++ oak-run/src/main/java/org/apache/jackrabbit/oak/run/Mode.java (working copy) @@ -30,6 +30,7 @@ COMPACT("compact", new CompactCommand()), SERVER("server", new ServerCommand()), UPGRADE("upgrade", new UpgradeCommand()), + UNLOCKUPGRADE("unlockUpgrade", new UnlockUpgradeCommand()), SCALABILITY("scalability", new ScalabilityCommand()), EXPLORE("explore", new ExploreCommand()), CHECKPOINTS("checkpoints", new CheckpointsCommand()), Index: oak-run/src/main/java/org/apache/jackrabbit/oak/run/UnlockUpgradeCommand.java =================================================================== --- oak-run/src/main/java/org/apache/jackrabbit/oak/run/UnlockUpgradeCommand.java (nonexistent) +++ oak-run/src/main/java/org/apache/jackrabbit/oak/run/UnlockUpgradeCommand.java (working copy) @@ -0,0 +1,106 @@ +/* + * 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.run; + +import java.util.List; + +import javax.sql.DataSource; + +import com.mongodb.MongoClientURI; + +import org.apache.jackrabbit.oak.plugins.document.DocumentMK; +import org.apache.jackrabbit.oak.plugins.document.DocumentStore; +import org.apache.jackrabbit.oak.plugins.document.DocumentStoreException; +import org.apache.jackrabbit.oak.plugins.document.mongo.MongoDocumentStore; +import org.apache.jackrabbit.oak.plugins.document.rdb.RDBDataSourceFactory; +import org.apache.jackrabbit.oak.plugins.document.rdb.RDBDocumentStore; +import org.apache.jackrabbit.oak.plugins.document.util.MongoConnection; + +import joptsimple.OptionParser; +import joptsimple.OptionSet; +import joptsimple.OptionSpec; + +import static com.mongodb.MongoURI.MONGODB_PREFIX; +import static java.util.Arrays.asList; +import static org.apache.jackrabbit.oak.plugins.document.DocumentNodeStore.VERSION; + +/** + * Unlocks the DocumentStore for an upgrade to the current DocumentNodeStore + * version. + */ +class UnlockUpgradeCommand implements Command { + + @Override + public void execute(String... args) throws Exception { + OptionParser parser = new OptionParser(); + // RDB specific options + OptionSpec rdbjdbcuser = parser.accepts("rdbjdbcuser", "RDB JDBC user").withOptionalArg().defaultsTo(""); + OptionSpec rdbjdbcpasswd = parser.accepts("rdbjdbcpasswd", "RDB JDBC password").withOptionalArg().defaultsTo(""); + + OptionSpec nonOption = parser.nonOptions("unlockUpgrade { | }"); + OptionSpec help = parser.acceptsAll(asList("h", "?", "help"), "show help").forHelp(); + + OptionSet options = parser.parse(args); + List nonOptions = nonOption.values(options); + + if (options.has(help)) { + parser.printHelpOn(System.out); + System.exit(0); + } + + if (nonOptions.isEmpty()) { + parser.printHelpOn(System.err); + System.exit(1); + } + + int code = 1; + DocumentStore store = null; + try { + String uri = nonOptions.get(0); + if (uri.startsWith(MONGODB_PREFIX)) { + MongoClientURI clientURI = new MongoClientURI(uri); + if (clientURI.getDatabase() == null) { + System.err.println("Database missing in MongoDB URI: " + clientURI.getURI()); + } else { + MongoConnection mongo = new MongoConnection(clientURI.getURI()); + store = new MongoDocumentStore(mongo.getDB(), new DocumentMK.Builder()); + } + } else if (uri.startsWith("jdbc")) { + DataSource ds = RDBDataSourceFactory.forJdbcUrl(uri, + rdbjdbcuser.value(options), rdbjdbcpasswd.value(options)); + store = new RDBDocumentStore(ds, new DocumentMK.Builder()); + } else { + System.err.println("Unrecognized URI: " + uri); + } + + if (store != null && VERSION.writeTo(store)) { + System.out.println("Format version set to " + VERSION); + // success + code = 0; + } + } catch (DocumentStoreException e) { + System.err.println(e.getMessage()); + } finally { + if (store != null) { + store.dispose(); + } + } + System.exit(code); + } +} Property changes on: oak-run/src/main/java/org/apache/jackrabbit/oak/run/UnlockUpgradeCommand.java ___________________________________________________________________ Added: svn:eol-style ## -0,0 +1 ## +native \ No newline at end of property