/* * FDBRecordStoreOpeningTest.java * * This source file is part of the FoundationDB open source project * * Copyright 2015-2020 Apple Inc. and the FoundationDB project authors * * 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. */ package com.apple.foundationdb.record.provider.foundationdb; import com.apple.foundationdb.Range; import com.apple.foundationdb.record.RecordCoreException; import com.apple.foundationdb.record.RecordMetaData; import com.apple.foundationdb.record.RecordMetaDataBuilder; import com.apple.foundationdb.record.RecordMetaDataOptionsProto; import com.apple.foundationdb.record.TestHelpers; import com.apple.foundationdb.record.TestNoIndexesProto; import com.apple.foundationdb.record.TestRecords1EvolvedAgainProto; import com.apple.foundationdb.record.TestRecords1EvolvedProto; import com.apple.foundationdb.record.TestRecords1Proto; import com.apple.foundationdb.record.logging.KeyValueLogMessage; import com.apple.foundationdb.record.logging.LogMessageKeys; import com.apple.foundationdb.record.metadata.Index; import com.apple.foundationdb.record.metadata.MetaDataException; import com.apple.foundationdb.record.metadata.expressions.KeyExpression; import com.apple.foundationdb.record.provider.foundationdb.keyspace.KeySpacePath; import com.apple.foundationdb.subspace.Subspace; import com.apple.foundationdb.tuple.Tuple; import com.apple.test.Tags; import com.google.protobuf.ByteString; import com.google.protobuf.Descriptors; import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.annotation.Nonnull; import javax.annotation.Nullable; import java.nio.charset.StandardCharsets; import java.util.Collections; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionException; import static com.apple.foundationdb.record.metadata.Key.Expressions.concatenateFields; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.hasKey; import static org.hamcrest.Matchers.instanceOf; import static org.hamcrest.Matchers.lessThan; import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; /** * Tests relating to building/opening record stores, or having multiple * copies of the same store on the same context (see: <a href="https://github.com/FoundationDB/fdb-record-layer/issues/489">Issue #489</a>). */ @Tag(Tags.RequiresFDB) public class FDBRecordStoreOpeningTest extends FDBRecordStoreTestBase { private static final Logger logger = LoggerFactory.getLogger(FDBRecordStoreOpeningTest.class); @Test public void open() throws Exception { // This tests the functionality of "open", so doesn't use the same method of opening // the record store that other methods within this class use. Object[] metaDataPathObjects = new Object[]{"record-test", "unit", "metadataStore"}; KeySpacePath metaDataPath; Subspace expectedSubspace; Subspace metaDataSubspace; try (FDBRecordContext context = fdb.openContext()) { metaDataPath = TestKeySpace.getKeyspacePath(metaDataPathObjects); expectedSubspace = path.toSubspace(context); metaDataSubspace = metaDataPath.toSubspace(context); context.ensureActive().clear(Range.startsWith(metaDataSubspace.pack())); context.commit(); } Index newIndex = new Index("newIndex", concatenateFields("str_value_indexed", "num_value_3_indexed")); Index newIndex2 = new Index("newIndex2", concatenateFields("str_value_indexed", "rec_no")); TestRecords1Proto.MySimpleRecord record = TestRecords1Proto.MySimpleRecord.newBuilder() .setRecNo(1066L) .setNumValue2(42) .setStrValueIndexed("value") .setNumValue3Indexed(1729) .build(); // Test open without a MetaDataStore try (FDBRecordContext context = fdb.openContext()) { RecordMetaDataBuilder metaDataBuilder = RecordMetaData.newBuilder().setRecords(TestRecords1Proto.getDescriptor()); FDBRecordStore recordStore = FDBRecordStore.newBuilder().setContext(context).setKeySpacePath(path) .setMetaDataProvider(metaDataBuilder).createOrOpen(); assertEquals(expectedSubspace, recordStore.getSubspace()); assertEquals(recordStore.getRecordStoreState(), recordStore.getRecordStoreState()); assertTrue(recordStore.getRecordStoreState().allIndexesReadable()); assertEquals(metaDataBuilder.getVersion(), recordStore.getRecordMetaData().getVersion()); final int version = metaDataBuilder.getVersion(); metaDataBuilder.addIndex("MySimpleRecord", newIndex); recordStore = recordStore.asBuilder().setMetaDataProvider(metaDataBuilder).open(); assertEquals(expectedSubspace, recordStore.getSubspace()); assertEquals(recordStore.getRecordStoreState(), recordStore.getRecordStoreState()); assertTrue(recordStore.getRecordStoreState().allIndexesReadable()); assertEquals(version + 1, recordStore.getRecordMetaData().getVersion()); recordStore.saveRecord(record); // This stops the index build. final RecordMetaData staleMetaData = metaDataBuilder.getRecordMetaData(); metaDataBuilder.addIndex("MySimpleRecord", newIndex2); recordStore = recordStore.asBuilder().setMetaDataProvider(metaDataBuilder).open(); assertEquals(expectedSubspace, recordStore.getSubspace()); assertEquals(recordStore.getRecordStoreState(), recordStore.getRecordStoreState()); assertEquals(Collections.singleton(newIndex2.getName()), recordStore.getRecordStoreState().getWriteOnlyIndexNames()); assertEquals(version + 2, recordStore.getRecordMetaData().getVersion()); final FDBRecordStore.Builder staleBuilder = recordStore.asBuilder().setMetaDataProvider(staleMetaData); TestHelpers.assertThrows(RecordStoreStaleMetaDataVersionException.class, staleBuilder::createOrOpen, LogMessageKeys.LOCAL_VERSION.toString(), version + 1, LogMessageKeys.STORED_VERSION.toString(), version + 2); } // Test open with a MetaDataStore try (FDBRecordContext context = fdb.openContext()) { FDBMetaDataStore metaDataStore = createMetaDataStore(context, metaDataPath, metaDataSubspace, null); FDBRecordStore.newBuilder().setMetaDataStore(metaDataStore).setContext(context).setKeySpacePath(path) .createOrOpenAsync().handle((store, e) -> { assertNull(store); assertNotNull(e); assertThat(e, instanceOf(CompletionException.class)); Throwable cause = e.getCause(); assertNotNull(cause); assertThat(cause, instanceOf(FDBMetaDataStore.MissingMetaDataException.class)); return null; }).join(); RecordMetaDataBuilder metaDataBuilder = RecordMetaData.newBuilder().setRecords(TestRecords1Proto.getDescriptor()); RecordMetaData origMetaData = metaDataBuilder.getRecordMetaData(); final int version = origMetaData.getVersion(); FDBRecordStore recordStore = FDBRecordStore.newBuilder().setContext(context).setKeySpacePath(path) .setMetaDataStore(metaDataStore).setMetaDataProvider(origMetaData) .createOrOpen(); assertEquals(expectedSubspace, recordStore.getSubspace()); assertEquals(recordStore.getRecordStoreState(), recordStore.getRecordStoreState()); assertTrue(recordStore.getRecordStoreState().allIndexesReadable()); assertEquals(version, recordStore.getRecordMetaData().getVersion()); metaDataBuilder.addIndex("MySimpleRecord", newIndex); metaDataStore.saveAndSetCurrent(metaDataBuilder.getRecordMetaData().toProto()).join(); metaDataStore = createMetaDataStore(context, metaDataPath, metaDataSubspace, TestRecords1Proto.getDescriptor()); recordStore = FDBRecordStore.newBuilder().setContext(context).setKeySpacePath(path) .setMetaDataStore(metaDataStore).setMetaDataProvider(origMetaData) .open(); assertEquals(expectedSubspace, recordStore.getSubspace()); assertEquals(recordStore.getRecordStoreState(), recordStore.getRecordStoreState()); assertTrue(recordStore.getRecordStoreState().allIndexesReadable()); assertEquals(version + 1, recordStore.getRecordMetaData().getVersion()); recordStore.saveRecord(record); final FDBMetaDataStore staleMetaDataStore = metaDataStore; metaDataStore = createMetaDataStore(context, metaDataPath, metaDataSubspace, TestRecords1Proto.getDescriptor()); metaDataBuilder.addIndex("MySimpleRecord", newIndex2); metaDataStore.saveRecordMetaData(metaDataBuilder.getRecordMetaData()); recordStore = FDBRecordStore.newBuilder().setContext(context).setSubspace(expectedSubspace).setMetaDataStore(metaDataStore).open(); assertEquals(expectedSubspace, recordStore.getSubspace()); assertEquals(recordStore.getRecordStoreState(), recordStore.getRecordStoreState()); assertEquals(Collections.singleton(newIndex2.getName()), recordStore.getRecordStoreState().getWriteOnlyIndexNames()); assertEquals(version + 2, recordStore.getRecordMetaData().getVersion()); // The stale meta-data store uses the cached meta-data, hence the stale version exception FDBRecordStore.Builder storeBuilder = FDBRecordStore.newBuilder().setContext(context).setSubspace(expectedSubspace) .setMetaDataStore(staleMetaDataStore); TestHelpers.assertThrows(RecordStoreStaleMetaDataVersionException.class, storeBuilder::createOrOpen, LogMessageKeys.LOCAL_VERSION.toString(), version + 1, LogMessageKeys.STORED_VERSION.toString(), version + 2); } // Test uncheckedOpen without a MetaDataStore try (FDBRecordContext context = openContext()) { RecordMetaDataBuilder metaDataBuilder = RecordMetaData.newBuilder().setRecords(TestRecords1Proto.getDescriptor()); FDBRecordStore recordStore = FDBRecordStore.newBuilder().setContext(context).setKeySpacePath(path) .setMetaDataProvider(metaDataBuilder).uncheckedOpen(); assertEquals(expectedSubspace, recordStore.getSubspace()); assertTrue(recordStore.getRecordStoreState().allIndexesReadable()); assertEquals(metaDataBuilder.getVersion(), recordStore.getRecordMetaData().getVersion()); final int version = metaDataBuilder.getVersion(); metaDataBuilder.addIndex("MySimpleRecord", newIndex); recordStore = recordStore.asBuilder().setMetaDataProvider(metaDataBuilder).uncheckedOpen(); assertEquals(expectedSubspace, recordStore.getSubspace()); assertTrue(recordStore.getRecordStoreState().allIndexesReadable()); assertEquals(version + 1, recordStore.getRecordMetaData().getVersion()); recordStore.saveRecord(record); // This would stop the build if this ran checkVersion. final RecordMetaData staleMetaData = metaDataBuilder.getRecordMetaData(); metaDataBuilder.addIndex("MySimpleRecord", newIndex2); recordStore = FDBRecordStore.newBuilder().setContext(context).setKeySpacePath(path) .setMetaDataProvider(metaDataBuilder).uncheckedOpen(); assertEquals(expectedSubspace, recordStore.getSubspace()); assertTrue(recordStore.getRecordStoreState().allIndexesReadable()); assertEquals(version + 2, recordStore.getRecordMetaData().getVersion()); recordStore = recordStore.asBuilder().setMetaDataProvider(staleMetaData).uncheckedOpen(); assertEquals(expectedSubspace, recordStore.getSubspace()); assertTrue(recordStore.getRecordStoreState().allIndexesReadable()); assertEquals(version + 1, recordStore.getRecordMetaData().getVersion()); } // Test uncheckedOpen with a MetaDataStore try (FDBRecordContext context = fdb.openContext()) { FDBMetaDataStore metaDataStore = createMetaDataStore(context, metaDataPath, metaDataSubspace, null); FDBRecordStore.newBuilder().setContext(context).setKeySpacePath(path) .setMetaDataStore(metaDataStore).uncheckedOpenAsync().handle((store, e) -> { assertNull(store); assertNotNull(e); assertThat(e, instanceOf(CompletionException.class)); Throwable cause = e.getCause(); assertNotNull(cause); assertThat(cause, instanceOf(FDBMetaDataStore.MissingMetaDataException.class)); return null; }).join(); RecordMetaDataBuilder metaDataBuilder = RecordMetaData.newBuilder().setRecords(TestRecords1Proto.getDescriptor()); RecordMetaData origMetaData = metaDataBuilder.getRecordMetaData(); int version = origMetaData.getVersion(); FDBRecordStore recordStore = FDBRecordStore.newBuilder().setContext(context).setKeySpacePath(path) .setMetaDataStore(metaDataStore).setMetaDataProvider(origMetaData) .uncheckedOpen(); assertEquals(expectedSubspace, recordStore.getSubspace()); assertTrue(recordStore.getRecordStoreState().allIndexesReadable()); assertEquals(version, recordStore.getRecordMetaData().getVersion()); metaDataBuilder.addIndex("MySimpleRecord", newIndex); metaDataStore.saveAndSetCurrent(metaDataBuilder.getRecordMetaData().toProto()).join(); metaDataStore = createMetaDataStore(context, metaDataPath, metaDataSubspace, TestRecords1Proto.getDescriptor()); recordStore = FDBRecordStore.newBuilder().setContext(context).setSubspace(expectedSubspace) .setMetaDataStore(metaDataStore).setMetaDataProvider(origMetaData) .uncheckedOpen(); assertEquals(expectedSubspace, recordStore.getSubspace()); assertTrue(recordStore.getRecordStoreState().allIndexesReadable()); assertEquals(version + 1, recordStore.getRecordMetaData().getVersion()); recordStore.saveRecord(record); // This would stop the build if this used checkVersion final FDBMetaDataStore staleMetaDataStore = metaDataStore; metaDataStore = createMetaDataStore(context, metaDataPath, metaDataSubspace, TestRecords1Proto.getDescriptor()); metaDataBuilder.addIndex("MySimpleRecord", newIndex2); metaDataStore.saveAndSetCurrent(metaDataBuilder.getRecordMetaData().toProto()).join(); recordStore = FDBRecordStore.newBuilder().setContext(context).setKeySpacePath(path).setMetaDataStore(metaDataStore).uncheckedOpen(); assertEquals(expectedSubspace, recordStore.getSubspace()); assertTrue(recordStore.getRecordStoreState().allIndexesReadable()); assertEquals(version + 2, recordStore.getRecordMetaData().getVersion()); // The stale meta-data store uses the cached meta-data, hence the old version in the final assert recordStore = FDBRecordStore.newBuilder().setContext(context).setSubspace(expectedSubspace) .setMetaDataStore(staleMetaDataStore).uncheckedOpen(); assertEquals(expectedSubspace, recordStore.getSubspace()); assertTrue(recordStore.getRecordStoreState().allIndexesReadable()); assertEquals(version + 1, recordStore.getRecordMetaData().getVersion()); } } @Test public void testUpdateRecords() { KeySpacePath metaDataPath; Subspace metaDataSubspace; try (FDBRecordContext context = fdb.openContext()) { metaDataPath = TestKeySpace.getKeyspacePath("record-test", "unit", "metadataStore"); metaDataSubspace = metaDataPath.toSubspace(context); context.ensureActive().clear(Range.startsWith(metaDataSubspace.pack())); context.commit(); } try (FDBRecordContext context = fdb.openContext()) { RecordMetaData origMetaData = RecordMetaData.build(TestRecords1Proto.getDescriptor()); final int version = origMetaData.getVersion(); FDBMetaDataStore metaDataStore = createMetaDataStore(context, metaDataPath, metaDataSubspace, TestRecords1Proto.getDescriptor()); FDBRecordStore recordStore = FDBRecordStore.newBuilder().setContext(context).setKeySpacePath(path) .setMetaDataStore(metaDataStore).setMetaDataProvider(origMetaData) .createOrOpen(); assertEquals(version, recordStore.getRecordMetaData().getVersion()); TestRecords1Proto.MySimpleRecord record = TestRecords1Proto.MySimpleRecord.newBuilder() .setRecNo(1066L) .setNumValue2(42) .setStrValueIndexed("value") .setNumValue3Indexed(1729) .build(); recordStore.saveRecord(record); // Update the records without a local descriptor. Storing an evolved record must fail. final TestRecords1EvolvedProto.MySimpleRecord evolvedRecord = TestRecords1EvolvedProto.MySimpleRecord.newBuilder() .setRecNo(1067L) .setNumValue2(43) .setStrValueIndexed("evolved value") .setNumValue3Indexed(1730) .build(); metaDataStore = createMetaDataStore(context, metaDataPath, metaDataSubspace, null); metaDataStore.updateRecords(TestRecords1EvolvedProto.getDescriptor()); // Bumps the version final FDBRecordStore recordStoreWithNoLocalFileDescriptor = FDBRecordStore.newBuilder().setContext(context).setKeySpacePath(path) .setMetaDataStore(metaDataStore).setMetaDataProvider(origMetaData) .open(); assertEquals(version + 1, recordStoreWithNoLocalFileDescriptor.getRecordMetaData().getVersion()); MetaDataException e = assertThrows(MetaDataException.class, () -> recordStoreWithNoLocalFileDescriptor.saveRecord(evolvedRecord)); assertEquals(e.getMessage(), "descriptor did not match record type"); // Update the records with a local descriptor. Storing an evolved record must succeed this time. metaDataStore = createMetaDataStore(context, metaDataPath, metaDataSubspace, TestRecords1EvolvedProto.getDescriptor()); metaDataStore.updateRecords(TestRecords1EvolvedProto.getDescriptor()); // Bumps the version recordStore = FDBRecordStore.newBuilder().setContext(context).setKeySpacePath(path) .setMetaDataStore(metaDataStore).setMetaDataProvider(origMetaData) .open(); assertEquals(version + 2, recordStore.getRecordMetaData().getVersion()); recordStore.saveRecord(evolvedRecord); // Evolve the meta-data one more time and use it for local file descriptor. SaveRecord will succeed. final TestRecords1EvolvedAgainProto.MySimpleRecord evolvedAgainRecord = TestRecords1EvolvedAgainProto.MySimpleRecord.newBuilder() .setRecNo(1066L) .setNumValue2(42) .setStrValueIndexed("value") .setNumValue3Indexed(1729) .build(); metaDataStore = createMetaDataStore(context, metaDataPath, metaDataSubspace, TestRecords1EvolvedAgainProto.getDescriptor()); metaDataStore.updateRecords(TestRecords1EvolvedProto.getDescriptor()); // Bumps the version recordStore = FDBRecordStore.newBuilder().setContext(context).setKeySpacePath(path) .setMetaDataStore(metaDataStore).setMetaDataProvider(origMetaData) .open(); assertEquals(version + 3, recordStore.getRecordMetaData().getVersion()); recordStore.saveRecord(evolvedAgainRecord); } } /** * Test that if a header user field is set that it's value can be read in the same transaction (i.e., that it * supports read-your-writes). */ @Test public void testReadYourWritesWithHeaderUserField() throws Exception { final String userField = "my_key"; try (FDBRecordContext context = openContext()) { openSimpleRecordStore(context); recordStore.setHeaderUserField(userField, ByteString.copyFromUtf8("my_value")); assertEquals("my_value", recordStore.getHeaderUserField(userField).toStringUtf8()); // do not commit to make sure it is *only* updated at commit time } try (FDBRecordContext context = openContext()) { openSimpleRecordStore(context); assertNull(recordStore.getHeaderUserField("my_key")); recordStore.setHeaderUserField(userField, ByteString.copyFromUtf8("my_other_value")); assertEquals("my_other_value", recordStore.getHeaderUserField(userField).toStringUtf8()); // Create a new record store to validate that a new record store in the same transaction also sees the value // when opened after the value has been changed FDBRecordStore secondStore = recordStore.asBuilder().open(); assertEquals("my_other_value", secondStore.getHeaderUserField(userField).toStringUtf8()); secondStore.clearHeaderUserField(userField); assertNull(secondStore.getHeaderUserField(userField)); FDBRecordStore thirdStore = recordStore.asBuilder().open(); assertNull(secondStore.getHeaderUserField(userField)); commit(context); } } /** * This is essentially a bug, but this test exhibits the behavior. Essentially, if you have a two record store * objects opened on the same subspace in the same transaction, and then you update a header user field * in one, then it isn't updated in the other. There might be a solution that involves all of this "shared state" * living in some shared place for all record stores (as the same problem affects, say, index state information), * but that is not what the code does right now. See: * <a href="https://github.com/FoundationDB/fdb-record-layer/issues/489">Issue #489</a>. */ @Test public void testHeaderUserFieldNotUpdatedInRecordStoreOnSameSubspace() throws Exception { try (FDBRecordContext context = openContext()) { openSimpleRecordStore(context); recordStore.setHeaderUserField("user_field", new byte[]{0x42}); commit(context); } try (FDBRecordContext context = openContext()) { openSimpleRecordStore(context); assertArrayEquals(new byte[]{0x42}, recordStore.getHeaderUserField("user_field").toByteArray()); FDBRecordStore secondStore = recordStore.asBuilder().open(); assertArrayEquals(new byte[]{0x42}, secondStore.getHeaderUserField("user_field").toByteArray()); recordStore.setHeaderUserField("user_field", new byte[]{0x10, 0x66}); assertArrayEquals(new byte[]{0x10, 0x66}, recordStore.getHeaderUserField("user_field").toByteArray()); assertArrayEquals(new byte[]{0x42}, secondStore.getHeaderUserField("user_field").toByteArray()); commit(context); } } @Test public void testGetHeaderFieldOnUninitializedStore() throws Exception { final String userField = "some_user_field"; final FDBRecordStore.Builder storeBuilder; try (FDBRecordContext context = openContext()) { openSimpleRecordStore(context); recordStore.setHeaderUserField(userField, "my utf-16 string".getBytes(StandardCharsets.UTF_16)); commit(context); storeBuilder = recordStore.asBuilder(); } try (FDBRecordContext context = openContext()) { // Do *not* call check version FDBRecordStore store = storeBuilder.setContext(context).build(); UninitializedRecordStoreException err = assertThrows(UninitializedRecordStoreException.class, () -> store.getHeaderUserField(userField)); assertThat(err.getLogInfo(), hasKey(LogMessageKeys.KEY_SPACE_PATH.toString())); logger.info(KeyValueLogMessage.of("uninitialized store exception: " + err.getMessage(), err.exportLogInfo())); } } @Test public void storeExistenceChecks() { try (FDBRecordContext context = openContext()) { RecordMetaDataBuilder metaData = RecordMetaData.newBuilder().setRecords(TestRecords1Proto.getDescriptor()); FDBRecordStore.Builder storeBuilder = FDBRecordStore.newBuilder() .setContext(context).setKeySpacePath(path).setMetaDataProvider(metaData); assertThrows(RecordStoreDoesNotExistException.class, storeBuilder::open); recordStore = storeBuilder.uncheckedOpen(); TestRecords1Proto.MySimpleRecord.Builder simple = TestRecords1Proto.MySimpleRecord.newBuilder(); simple.setRecNo(1); recordStore.insertRecord(simple.build()); commit(context); } try (FDBRecordContext context = openContext()) { FDBRecordStore.Builder storeBuilder = recordStore.asBuilder() .setContext(context); assertThrows(RecordStoreNoInfoAndNotEmptyException.class, storeBuilder::createOrOpen); recordStore = storeBuilder.createOrOpen(FDBRecordStoreBase.StoreExistenceCheck.NONE); commit(context); } try (FDBRecordContext context = openContext()) { FDBRecordStore.Builder storeBuilder = recordStore.asBuilder() .setContext(context); assertThrows(RecordStoreAlreadyExistsException.class, storeBuilder::create); recordStore = storeBuilder.open(); assertNotNull(recordStore.loadRecord(Tuple.from(1))); commit(context); } } @Test public void storeExistenceChecksWithNoRecords() throws Exception { RecordMetaData metaData = RecordMetaData.build(TestRecords1Proto.getDescriptor()); FDBRecordStore.Builder storeBuilder = FDBRecordStore.newBuilder() .setKeySpacePath(path) .setMetaDataProvider(metaData); try (FDBRecordContext context = openContext()) { FDBRecordStore store = storeBuilder.setContext(context).create(); // delete the header store.ensureContextActive().clear(getStoreInfoKey(store)); commit(context); } // Should be able to recover from an empty record store try (FDBRecordContext context = openContext()) { storeBuilder.setContext(context); assertThrows(RecordStoreNoInfoAndNotEmptyException.class, storeBuilder::createOrOpen); commit(context); } try (FDBRecordContext context = openContext()) { storeBuilder.setContext(context); FDBRecordStore store = storeBuilder.build(); // do not perform checkVersion yet assertNull(context.ensureActive().get(getStoreInfoKey(store)).get()); assertTrue(store.checkVersion(null, FDBRecordStoreBase.StoreExistenceCheck.ERROR_IF_NO_INFO_AND_HAS_RECORDS_OR_INDEXES).get()); commit(context); } // Delete everything except a value in the index build space try (FDBRecordContext context = openContext()) { FDBRecordStore store = storeBuilder.setContext(context).open(); final Subspace subspace = OnlineIndexer.indexBuildScannedRecordsSubspace(store, metaData.getIndex("MySimpleRecord$str_value_indexed")); context.ensureActive().set(subspace.getKey(), FDBRecordStore.encodeRecordCount(1215)); // set a key in the INDEX_BUILD_SPACE context.ensureActive().clear(store.getSubspace().getKey(), subspace.getKey()); commit(context); } try (FDBRecordContext context = openContext()) { storeBuilder.setContext(context); assertThrows(RecordStoreNoInfoAndNotEmptyException.class, storeBuilder::createOrOpen); } try (FDBRecordContext context = openContext()) { storeBuilder.setContext(context).createOrOpen(FDBRecordStoreBase.StoreExistenceCheck.ERROR_IF_NO_INFO_AND_HAS_RECORDS_OR_INDEXES); commit(context); } // Insert a record, then delete the store header try (FDBRecordContext context = openContext()) { // open as the previous open with the relaxed existence check should have fixed the store header FDBRecordStore store = storeBuilder.setContext(context).open(); store.saveRecord(TestRecords1Proto.MySimpleRecord.newBuilder().setRecNo(1066L).build()); store.ensureContextActive().clear(getStoreInfoKey(store)); commit(context); } try (FDBRecordContext context = openContext()) { storeBuilder.setContext(context); assertThrows(RecordStoreNoInfoAndNotEmptyException.class, storeBuilder::createOrOpen); assertThrows(RecordStoreNoInfoAndNotEmptyException.class, () -> storeBuilder.createOrOpen(FDBRecordStoreBase.StoreExistenceCheck.ERROR_IF_NO_INFO_AND_HAS_RECORDS_OR_INDEXES)); commit(context); } // Delete the record store, then insert a key at an unknown keyspace try (FDBRecordContext context = openContext()) { FDBRecordStore.deleteStore(context, path); Subspace subspace = path.toSubspace(context); context.ensureActive().set(subspace.pack("unknown_keyspace"), Tuple.from("doesn't matter").pack()); commit(context); } try (FDBRecordContext context = openContext()) { storeBuilder.setContext(context); assertThrows(RecordStoreNoInfoAndNotEmptyException.class, storeBuilder::createOrOpen); RecordCoreException err = assertThrows(RecordCoreException.class, () -> storeBuilder.createOrOpen(FDBRecordStoreBase.StoreExistenceCheck.ERROR_IF_NO_INFO_AND_HAS_RECORDS_OR_INDEXES)); assertEquals("Unrecognized keyspace: unknown_keyspace", err.getMessage()); commit(context); } } @Test public void existenceChecks() throws Exception { try (FDBRecordContext context = openContext()) { openSimpleRecordStore(context); TestRecords1Proto.MySimpleRecord.Builder simple = TestRecords1Proto.MySimpleRecord.newBuilder(); simple.setRecNo(1); simple.setNumValue2(111); recordStore.insertRecord(simple.build()); TestRecords1Proto.MyOtherRecord.Builder other = TestRecords1Proto.MyOtherRecord.newBuilder(); other.setRecNo(2); other.setNumValue2(222); recordStore.insertRecord(other.build()); commit(context); } try (FDBRecordContext context = openContext()) { openSimpleRecordStore(context); TestRecords1Proto.MySimpleRecord.Builder simple = TestRecords1Proto.MySimpleRecord.newBuilder(); simple.setRecNo(1); simple.setNumValue2(1111); assertThrows(RecordAlreadyExistsException.class, () -> recordStore.insertRecord(simple.build())); simple.setRecNo(3); simple.setNumValue2(3333); assertThrows(RecordDoesNotExistException.class, () -> recordStore.updateRecord(simple.build())); simple.setRecNo(2); simple.setNumValue2(2222); assertThrows(RecordTypeChangedException.class, () -> recordStore.updateRecord(simple.build())); } try (FDBRecordContext context = openContext()) { openSimpleRecordStore(context); TestRecords1Proto.MySimpleRecord.Builder simple = TestRecords1Proto.MySimpleRecord.newBuilder(); simple.mergeFrom(recordStore.loadRecord(Tuple.from(1L)).getRecord()); assertEquals(111, simple.getNumValue2()); simple.setNumValue2(1111); recordStore.updateRecord(simple.build()); simple.clear(); simple.setRecNo(4); simple.setNumValue2(444); recordStore.insertRecord(simple.build()); commit(context); } } @Test public void metaDataVersionZero() { final RecordMetaDataBuilder metaData = RecordMetaData.newBuilder().setRecords(TestNoIndexesProto.getDescriptor()); metaData.setVersion(0); FDBRecordStore.Builder builder = FDBRecordStore.newBuilder() .setKeySpacePath(path) .setMetaDataProvider(metaData); final FDBRecordStoreBase.UserVersionChecker newStore = (oldUserVersion, oldMetaDataVersion, metaData1) -> { assertEquals(-1, oldUserVersion); assertEquals(-1, oldMetaDataVersion); return CompletableFuture.completedFuture(0); }; try (FDBRecordContext context = openContext()) { recordStore = builder.setContext(context).setUserVersionChecker(newStore).create(); assertTrue(recordStore.getRecordStoreState().getStoreHeader().hasMetaDataversion()); assertTrue(recordStore.getRecordStoreState().getStoreHeader().hasUserVersion()); commit(context); } final FDBRecordStoreBase.UserVersionChecker oldStore = (oldUserVersion, oldMetaDataVersion, metaData12) -> { assertEquals(0, oldUserVersion); assertEquals(0, oldMetaDataVersion); return CompletableFuture.completedFuture(0); }; try (FDBRecordContext context = openContext()) { recordStore = builder.setContext(context).setUserVersionChecker(oldStore).open(); commit(context); } } @Test public void invalidMetaData() { RecordMetaDataHook invalid = metaData -> metaData.addIndex("MySimpleRecord", "no_such_field"); try (FDBRecordContext context = openContext()) { assertThrows(KeyExpression.InvalidExpressionException.class, () -> openSimpleRecordStore(context, invalid)); } } /** * Validate that if the store header changes then an open record store in another transaction is failed with * a conflict. */ @Test public void conflictWithHeaderChange() { RecordMetaData metaData1 = RecordMetaData.build(TestRecords1Proto.getDescriptor()); RecordMetaDataBuilder metaDataBuilder = RecordMetaData.newBuilder().setRecords(TestRecords1Proto.getDescriptor()); metaDataBuilder.addIndex("MySimpleRecord", "num_value_2"); RecordMetaData metaData2 = metaDataBuilder.getRecordMetaData(); assertThat(metaData1.getVersion(), lessThan(metaData2.getVersion())); try (FDBRecordContext context = openContext()) { FDBRecordStore recordStore = FDBRecordStore.newBuilder() .setKeySpacePath(path) .setContext(context) .setMetaDataProvider(metaData1) .create(); assertEquals(metaData1.getVersion(), recordStore.getRecordStoreState().getStoreHeader().getMetaDataversion()); commit(context); } try (FDBRecordContext context1 = openContext(); FDBRecordContext context2 = openContext()) { FDBRecordStore recordStore1 = FDBRecordStore.newBuilder() .setKeySpacePath(path) .setContext(context1) .setMetaDataProvider(metaData1) .open(); assertEquals(metaData1.getVersion(), recordStore1.getRecordStoreState().getStoreHeader().getMetaDataversion()); FDBRecordStore recordStore2 = FDBRecordStore.newBuilder() .setKeySpacePath(path) .setContext(context2) .setMetaDataProvider(metaData2) .open(); assertEquals(metaData2.getVersion(), recordStore2.getRecordStoreState().getStoreHeader().getMetaDataversion()); commit(context2); // Add a write to the first record store to make sure that the conflict are actually checked. recordStore1.saveRecord(TestRecords1Proto.MySimpleRecord.newBuilder() .setRecNo(1066L) .setNumValue2(1415) .build()); assertThrows(FDBExceptions.FDBStoreTransactionConflictException.class, context1::commit); } try (FDBRecordContext context = openContext()) { assertThrows(RecordStoreStaleMetaDataVersionException.class, () -> FDBRecordStore.newBuilder() .setKeySpacePath(path) .setContext(context) .setMetaDataProvider(metaData1) .open()); FDBRecordStore recordStore = FDBRecordStore.newBuilder() .setKeySpacePath(path) .setContext(context) .setMetaDataProvider(metaData2) .open(); assertEquals(metaData2.getVersion(), recordStore.getRecordStoreState().getStoreHeader().getMetaDataversion()); commit(context); } } @Nonnull private FDBMetaDataStore createMetaDataStore(@Nonnull FDBRecordContext context, @Nonnull KeySpacePath metaDataPath, @Nonnull Subspace metaDataSubspace, @Nullable Descriptors.FileDescriptor localFileDescriptor) { FDBMetaDataStore metaDataStore = new FDBMetaDataStore(context, metaDataPath); metaDataStore.setMaintainHistory(false); assertEquals(metaDataSubspace, metaDataStore.getSubspace()); metaDataStore.setDependencies(new Descriptors.FileDescriptor[]{RecordMetaDataOptionsProto.getDescriptor()}); metaDataStore.setLocalFileDescriptor(localFileDescriptor); return metaDataStore; } private static byte[] getStoreInfoKey(@Nonnull FDBRecordStore store) { return store.getSubspace().pack(FDBRecordStoreKeyspace.STORE_INFO.key()); } }