001: /*-
002: * See the file LICENSE for redistribution information.
003: *
004: * Copyright (c) 2002,2008 Oracle. All rights reserved.
005: *
006: * $Id: SecondaryDirtyReadTest.java,v 1.12.2.3 2008/01/07 15:14:33 cwl Exp $
007: */
008:
009: package com.sleepycat.je.test;
010:
011: import junit.framework.Test;
012:
013: import com.sleepycat.je.Database;
014: import com.sleepycat.je.DatabaseConfig;
015: import com.sleepycat.je.DatabaseEntry;
016: import com.sleepycat.je.DatabaseException;
017: import com.sleepycat.je.LockMode;
018: import com.sleepycat.je.OperationStatus;
019: import com.sleepycat.je.SecondaryConfig;
020: import com.sleepycat.je.SecondaryCursor;
021: import com.sleepycat.je.SecondaryDatabase;
022: import com.sleepycat.je.SecondaryKeyCreator;
023: import com.sleepycat.je.Transaction;
024: import com.sleepycat.je.junit.JUnitMethodThread;
025: import com.sleepycat.je.util.TestUtils;
026:
027: /**
028: * Tests for multithreading problems when using read-uncommitted with
029: * secondaries. If a primary record is updated while performing a
030: * read-uncommitted (in between reading the secondary and the primary), we need
031: * to be sure that we don't return inconsistent results to the user. For
032: * example, we should not return a primary data value that no longer contains
033: * the secondary key. We also need to ensure that deleting a primary record in
034: * the middle of a secondary read does not appear as a corrupt secondary. In
035: * both of these cases it should appear that the record does not exist, from
036: * the viewpoint of an application using a cursor.
037: *
038: * <p>These tests create two threads, one reading and the other deleting or
039: * updating. The intention is for reading thread and the delete/update thread
040: * to race in operating on the same key (nextKey). If the reading thread reads
041: * the secondary, then the other thread deletes the primary, then the reading
042: * thread tries to read the primary, we've accomplished our goal. Prior to
043: * when we handled that case in SecondaryCursor, that situation would cause a
044: * "secondary corrupt" exception.</p>
045: */
046: public class SecondaryDirtyReadTest extends MultiKeyTxnTestCase {
047:
048: private static final int MAX_KEY = 1000;
049:
050: public static Test suite() {
051: return multiKeyTxnTestSuite(SecondaryDirtyReadTest.class, null,
052: null);
053: //new String[] {TxnTestCase.TXN_NULL});
054: }
055:
056: private int nextKey;
057: private Database priDb;
058: private SecondaryDatabase secDb;
059: private LockMode lockMode = LockMode.READ_UNCOMMITTED;
060:
061: /**
062: * Closes databases, then calls the super.tearDown to close the env.
063: */
064: public void tearDown() throws Exception {
065:
066: if (secDb != null) {
067: try {
068: secDb.close();
069: } catch (Exception e) {
070: }
071: secDb = null;
072: }
073: if (priDb != null) {
074: try {
075: priDb.close();
076: } catch (Exception e) {
077: }
078: priDb = null;
079: }
080: super .tearDown();
081: }
082:
083: /**
084: * Tests that deleting primary records does not cause secondary
085: * read-uncommitted to throw a "secondary corrupt" exception.
086: */
087: public void testDeleteWhileReadingByKey() throws Throwable {
088:
089: doTest("runReadUncommittedByKey", "runPrimaryDelete");
090: }
091:
092: /**
093: * Same as testDeleteWhileReadingByKey but does a scan. Read-uncommitted
094: * for scan and keyed reads are implemented differently, since scanning
095: * moves to the next record when a deletion is detected while a keyed read
096: * returns NOTFOUND.
097: */
098: public void testDeleteWhileScanning() throws Throwable {
099:
100: doTest("runReadUncommittedScan", "runPrimaryDelete");
101: }
102:
103: /**
104: * Tests that updating primary records, to cause deletion of the secondary
105: * key record, does not cause secondary read-uncommitted to return
106: * inconsistent data (a primary datum without a secondary key value).
107: */
108: public void testUpdateWhileReadingByKey() throws Throwable {
109:
110: doTest("runReadUncommittedByKey", "runPrimaryUpdate");
111: }
112:
113: /**
114: * Same as testUpdateWhileReadingByKey but does a scan.
115: */
116: public void testUpdateWhileScanning() throws Throwable {
117:
118: doTest("runReadUncommittedScan", "runPrimaryUpdate");
119: }
120:
121: /**
122: * Runs two threads for the given method names, after populating the
123: * database.
124: */
125: public void doTest(String method1, String method2) throws Throwable {
126:
127: JUnitMethodThread tester1 = new JUnitMethodThread(method1
128: + "-t1", method1, this );
129: JUnitMethodThread tester2 = new JUnitMethodThread(method2
130: + "-t2", method2, this );
131: priDb = openPrimary("testDB");
132: secDb = openSecondary(priDb, "testSecDB", false);
133: addRecords();
134: tester1.start();
135: tester2.start();
136: tester1.finishTest();
137: tester2.finishTest();
138: secDb.close();
139: secDb = null;
140: priDb.close();
141: priDb = null;
142: }
143:
144: /**
145: * Deletes the key that is being read by the other thread.
146: */
147: public void runPrimaryDelete() throws DatabaseException {
148:
149: DatabaseEntry key = new DatabaseEntry();
150: while (nextKey < MAX_KEY - 1) {
151: Transaction txn = txnBegin();
152: key.setData(TestUtils.getTestArray(nextKey));
153: OperationStatus status = priDb.delete(txn, key);
154: if (status != OperationStatus.SUCCESS) {
155: assertEquals(OperationStatus.NOTFOUND, status);
156: }
157: txnCommit(txn);
158: }
159: }
160:
161: /**
162: * Updates the record for the key that is being read by the other thread,
163: * changing the datum to -1 so it will cause the secondary key record to
164: * be deleted.
165: */
166: public void runPrimaryUpdate() throws DatabaseException {
167:
168: DatabaseEntry key = new DatabaseEntry();
169: DatabaseEntry data = new DatabaseEntry();
170: while (nextKey < MAX_KEY - 1) {
171: Transaction txn = txnBegin();
172: key.setData(TestUtils.getTestArray(nextKey));
173: data.setData(TestUtils.getTestArray(-1));
174: OperationStatus status = priDb.put(txn, key, data);
175: assertEquals(OperationStatus.SUCCESS, status);
176: txnCommit(txn);
177: }
178: }
179:
180: /**
181: * Does a read-uncommitted by key, retrying until it is deleted by the
182: * delete/update thread, then moves to the next key. We shouldn't get an
183: * exception, just a NOTFOUND when it is deleted.
184: */
185: public void runReadUncommittedByKey() throws DatabaseException {
186:
187: DatabaseEntry key = new DatabaseEntry();
188: DatabaseEntry pKey = new DatabaseEntry();
189: DatabaseEntry data = new DatabaseEntry();
190: while (nextKey < MAX_KEY - 1) {
191: key.setData(TestUtils.getTestArray(nextKey));
192: OperationStatus status = secDb.get(null, key, pKey, data,
193: lockMode);
194: if (status != OperationStatus.SUCCESS) {
195: assertEquals(OperationStatus.NOTFOUND, status);
196: nextKey++;
197: } else {
198: assertEquals(nextKey, TestUtils.getTestVal(key
199: .getData()));
200: assertEquals(nextKey, TestUtils.getTestVal(pKey
201: .getData()));
202: assertEquals(nextKey, TestUtils.getTestVal(data
203: .getData()));
204: }
205: }
206: }
207:
208: /**
209: * Does a read-uncommitted scan through the whole key range, but moves
210: * forward only after the key is deleted by the delete/update thread. We
211: * shouldn't get an exception or a NOTFOUND, but we may skip values when a
212: * key is deleted.
213: */
214: public void runReadUncommittedScan() throws DatabaseException {
215:
216: DatabaseEntry key = new DatabaseEntry();
217: DatabaseEntry pKey = new DatabaseEntry();
218: DatabaseEntry data = new DatabaseEntry();
219: SecondaryCursor cursor = secDb.openSecondaryCursor(null, null);
220: while (nextKey < MAX_KEY - 1) {
221: OperationStatus status = cursor.getNext(key, pKey, data,
222: lockMode);
223: assertEquals("nextKey=" + nextKey, OperationStatus.SUCCESS,
224: status);
225: int keyFound = TestUtils.getTestVal(key.getData());
226: assertEquals(keyFound, TestUtils.getTestVal(pKey.getData()));
227: assertEquals(keyFound, TestUtils.getTestVal(data.getData()));
228: /* Let the delete/update thread catch up. */
229: nextKey = keyFound;
230: if (nextKey < MAX_KEY - 1) {
231: while (status != OperationStatus.KEYEMPTY) {
232: assertEquals(OperationStatus.SUCCESS, status);
233: status = cursor.getCurrent(key, pKey, data,
234: lockMode);
235: }
236: nextKey = keyFound + 1;
237: }
238: }
239: cursor.close();
240: }
241:
242: /**
243: * Adds records for the entire key range.
244: */
245: private void addRecords() throws DatabaseException {
246:
247: DatabaseEntry key = new DatabaseEntry();
248: DatabaseEntry data = new DatabaseEntry();
249: Transaction txn = txnBegin();
250: for (int i = 0; i < MAX_KEY; i += 1) {
251: byte[] val = TestUtils.getTestArray(i);
252: key.setData(val);
253: data.setData(val);
254: OperationStatus status = priDb.putNoOverwrite(txn, key,
255: data);
256: assertEquals(OperationStatus.SUCCESS, status);
257: }
258: txnCommit(txn);
259: }
260:
261: /**
262: * Opens the primary database.
263: */
264: private Database openPrimary(String name) throws DatabaseException {
265:
266: DatabaseConfig dbConfig = new DatabaseConfig();
267: dbConfig.setTransactional(isTransactional);
268: dbConfig.setAllowCreate(true);
269: Transaction txn = txnBegin();
270: Database priDb;
271: try {
272: priDb = env.openDatabase(txn, name, dbConfig);
273: } finally {
274: txnCommit(txn);
275: }
276: assertNotNull(priDb);
277: return priDb;
278: }
279:
280: /**
281: * Opens the secondary database.
282: */
283: private SecondaryDatabase openSecondary(Database priDb,
284: String dbName, boolean allowDuplicates)
285: throws DatabaseException {
286:
287: SecondaryConfig dbConfig = new SecondaryConfig();
288: dbConfig.setTransactional(isTransactional);
289: dbConfig.setAllowCreate(true);
290: dbConfig.setSortedDuplicates(allowDuplicates);
291: if (useMultiKey) {
292: dbConfig.setMultiKeyCreator(new SimpleMultiKeyCreator(
293: new MyKeyCreator()));
294: } else {
295: dbConfig.setKeyCreator(new MyKeyCreator());
296: }
297: Transaction txn = txnBegin();
298: SecondaryDatabase secDb;
299: try {
300: secDb = env.openSecondaryDatabase(txn, dbName, priDb,
301: dbConfig);
302: } finally {
303: txnCommit(txn);
304: }
305: return secDb;
306: }
307:
308: /**
309: * Creates secondary keys for a primary datum with a non-negative value.
310: */
311: private static class MyKeyCreator implements SecondaryKeyCreator {
312:
313: public boolean createSecondaryKey(SecondaryDatabase secondary,
314: DatabaseEntry key, DatabaseEntry data,
315: DatabaseEntry result) throws DatabaseException {
316:
317: int val = TestUtils.getTestVal(data.getData());
318: if (val >= 0) {
319: result.setData(TestUtils.getTestArray(val));
320: return true;
321: } else {
322: return false;
323: }
324: }
325: }
326: }
|